| 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 | */ |
| 7 | import type { PeopleDirectory, Team, User } from "@g1t/contracts"; |
| 8 | |
| 9 | import type { AgentTeams, TeamChange } from "./people"; |
| 10 | import { 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. */ |
| 13 | export 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 | */ |
| 43 | export 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 | } |