Skip to content
57 linesCodeBlameRaw
1/**
2 * An agent's teams on its profiles: the teams it is on and the ones the
3 * viewer may add it to, and adding or taking it off one. An agent is on
4 * teams the way a person is, through team membership in identity, so this
5 * is the team's own add and remove, from the agent's side.
6 */
7import type { PeopleDirectory, Team, User } from "@g1t/contracts";
8
9import type { AgentTeams, TeamChange } from "./people";
10import { identity, workspaceAgents } from "./services.server";
11
12/** The teams `agentId` is on and the teams the viewer may add it to; null when identity didn't answer. */
13export async function agentTeamsFor(viewer: User, slug: string, agentId: string, directory?: PeopleDirectory | null): Promise<AgentTeams | null> {
14 const [listed, people] = await Promise.all([
15 identity.listTeams(viewer, slug).catch(() => null),
16 directory ? Promise.resolve(directory) : identity.peopleDirectory(viewer, slug).then((found) => (found.ok ? found.value : null), () => null),
17 ]);
18 if (!listed?.ok || !people) return null;
19 const manage = new Map<string, Team>(listed.value.map((team) => [team.slug, team]));
20 const on = people.teams
21 .filter((team) => team.agent_ids.includes(agentId))
22 .map((team) => ({
23 slug: team.slug,
24 name: team.name,
25 people: team.people.length,
26 agents: new Set(team.agent_ids).size,
27 lead: team.lead?.kind === "agent" && team.lead.agent_id === agentId,
28 can_manage: manage.get(team.slug)?.can_manage ?? false,
29 }))
30 .sort((a, b) => a.name.localeCompare(b.name));
31 const addable = listed.value
32 .filter((team) => team.can_manage && !on.some((row) => row.slug === team.slug))
33 .map((team) => ({ slug: team.slug, name: team.name }))
34 .sort((a, b) => a.name.localeCompare(b.name));
35 return { on, addable };
36}
37
38/**
39 * `join-team` or `leave-team` from an agent's profile: the agent must be
40 * one of the workspace's (identity knows agents only by id), and a
41 * personal agent joins no team. Null for any other intent.
42 */
43export async function changeAgentTeam(form: FormData, viewer: User, slug: string, handle: string): Promise<TeamChange | null> {
44 const intent = String(form.get("intent") ?? "");
45 if (intent !== "join-team" && intent !== "leave-team") return null;
46 const team = String(form.get("team") ?? "").trim().toLowerCase();
47 if (!team) return { intent, team, error: "Choose a team." };
48 const found = await workspaceAgents.get(slug, handle, viewer).catch(() => null);
49 if (!found?.ok) return { intent, team, error: "That agent couldn't be found." };
50 if (intent === "join-team" && found.value.scope === "personal") return { intent, team, error: "A personal agent is on no team." };
51 const done =
52 intent === "join-team"
53 ? await identity.setTeamAgent(viewer, slug, team, found.value.id).catch(() => null)
54 : await identity.removeTeamAgent(viewer, slug, team, found.value.id).catch(() => null);
55 if (!done) return { intent, team, error: "Teams didn't answer. Try again in a moment." };
56 return done.ok ? { intent, team, error: null } : { intent, team, error: done.error.message };
57}