Skip to content
121 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 /** Agents added to it, by id. Agents whose home team it is are on it too (`agentsOnTeam`). */
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/**
83 * The agents on a team, by id: those added to it, then those whose home
84 * team it is (their profile names it).
85 */
86export function agentsOnTeam(team: { slug: string; agent_ids: readonly string[] }, agents: readonly { id: string; team: string | null }[]): string[] {
87 const ids = [...team.agent_ids];
88 for (const agent of agents) if (agent.team === team.slug && !ids.includes(agent.id)) ids.push(agent.id);
89 return ids;
90}
91
92/** What a team is made of. */
93export type TeamKind = "mixed" | "people" | "agents" | "empty";
94
95export function teamKind(people: number, agents: number): TeamKind {
96 if (people && agents) return "mixed";
97 if (people) return "people";
98 if (agents) return "agents";
99 return "empty";
100}
101
102/** "2 people and 1 agent", "People only", "Agents only", "No one yet". */
103export function teamKindLabel(people: number, agents: number): string {
104 const kind = teamKind(people, agents);
105 if (kind === "people") return "People only";
106 if (kind === "agents") return "Agents only";
107 if (kind === "empty") return "No one yet";
108 return `${people} ${people === 1 ? "person" : "people"} and ${agents} ${agents === 1 ? "agent" : "agents"}`;
109}
110
111/** What someone owns, as identity keeps it: trimmed, each once (ignoring case), at most eight. */
112export function cleanOwns(owns: readonly string[]): string[] {
113 const out: string[] = [];
114 for (const raw of owns) {
115 const phrase = raw.split(/\s+/).filter(Boolean).join(" ").slice(0, MAX_OWNS_LENGTH).replace(/[.,;]+$/, "").trim();
116 if (!phrase || out.some((kept) => kept.toLowerCase() === phrase.toLowerCase())) continue;
117 out.push(phrase);
118 if (out.length === MAX_OWNS) break;
119 }
120 return out;
121}