Skip to content
111 linesCodeBlameRaw
1/**
2 * People: a workspace's directory of everyone in it, people and agents,
3 * and each member's place in it: a title, who they report to, and what
4 * they own. Kept by identity beside membership; mirrors
5 * `crates/contracts/src/people.rs`. Agents come from the agents service,
6 * which teams name by id.
7 */
8import type { User } from "./identity";
9import type { Result } from "./result";
10import type { TeamChannel, TeamLead, TeamRef, TeamRole, TeamVisibility } from "./teams";
11
12/** The longest title. */
13export const MAX_TITLE_LENGTH = 80;
14/** The most things one person owns, as their profile lists them, and the longest of them. */
15export const MAX_OWNS = 8;
16export const MAX_OWNS_LENGTH = 60;
17
18/** One member, as the directory, their profile and the org chart show them. */
19export type DirectoryPerson = {
20 user_id: string;
21 username: string;
22 display_username?: string;
23 name: string | null;
24 avatar: string | null;
25 bio: string | null;
26 location: string | null;
27 pronouns: string | null;
28 /** An IANA time zone, for their local time. */
29 timezone: string | null;
30 role: "owner" | "member";
31 /** Their title in this workspace. */
32 title: string | null;
33 /** Who they report to, by username. */
34 manager: string | null;
35 /** What they own: a few short phrases. */
36 owns: string[];
37 joined_at: string;
38};
39
40export type TeamPersonRef = { username: string; role: TeamRole };
41
42/** A team as the directory and the org chart need it. */
43export type DirectoryTeam = {
44 slug: string;
45 name: string;
46 description: string | null;
47 visibility: TeamVisibility;
48 parent: TeamRef | null;
49 lead: TeamLead | null;
50 channel: TeamChannel | null;
51 budget_micros: number | null;
52 /** Its own people. */
53 people: TeamPersonRef[];
54 /** The agents on it, by id. */
55 agent_ids: string[];
56 repos_count: number;
57};
58
59export type PeopleDirectory = {
60 people: DirectoryPerson[];
61 teams: DirectoryTeam[];
62 /** What members get on every repository. */
63 base_permission: "none" | "read" | "write" | "admin";
64 /** Whether the viewer owns the workspace: they set anyone's title, manager and what they own. */
65 can_manage: boolean;
66};
67
68/** What changes on a member's profile; the rest stays. `manager: ""` clears it (owners only). */
69export type MemberProfileChanges = {
70 title?: string;
71 manager?: string;
72 owns?: string[];
73};
74
75export interface PeopleClient {
76 /** Everyone in a workspace and its teams, as the viewer may see them. Members only. */
77 peopleDirectory(viewer: User | null, workspace: string): Promise<Result<PeopleDirectory>>;
78 /** A member's title and what they own (themselves or an owner), and their manager (owners). */
79 setMemberProfile(actor: User, workspace: string, username: string, changes: MemberProfileChanges): Promise<Result<DirectoryPerson>>;
80}
81
82/** What a team is made of. */
83export type TeamKind = "mixed" | "people" | "agents" | "empty";
84
85export function teamKind(people: number, agents: number): TeamKind {
86 if (people && agents) return "mixed";
87 if (people) return "people";
88 if (agents) return "agents";
89 return "empty";
90}
91
92/** "2 people and 1 agent", "People only", "Agents only", "No one yet". */
93export function teamKindLabel(people: number, agents: number): string {
94 const kind = teamKind(people, agents);
95 if (kind === "people") return "People only";
96 if (kind === "agents") return "Agents only";
97 if (kind === "empty") return "No one yet";
98 return `${people} ${people === 1 ? "person" : "people"} and ${agents} ${agents === 1 ? "agent" : "agents"}`;
99}
100
101/** What someone owns, as identity keeps it: trimmed, each once (ignoring case), at most eight. */
102export function cleanOwns(owns: readonly string[]): string[] {
103 const out: string[] = [];
104 for (const raw of owns) {
105 const phrase = raw.split(/\s+/).filter(Boolean).join(" ").slice(0, MAX_OWNS_LENGTH).replace(/[.,;]+$/, "").trim();
106 if (!phrase || out.some((kept) => kept.toLowerCase() === phrase.toLowerCase())) continue;
107 out.push(phrase);
108 if (out.length === MAX_OWNS) break;
109 }
110 return out;
111}