Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 1 | import { type ChatSidebar, type Principal, type User, type WorkspaceAgent, shownUsername } from "@g1t/contracts"; |
| Chat and workspace agents: channels, DMs and named agents you talk to | 2 | |
| 3 | import type { Mentionable } from "./chat"; | |
| 4 | import { chat, identity, workspaceAgents } from "./services.server"; | |
| 5 | ||
| 6 | /** | |
| 7 | * Who can be talked to in a workspace: its people and its agents, for | |
| 8 | * mentions and for starting a direct message. Each list is empty, not an | |
| 9 | * error, when its service does not answer: chat and agents may arrive a | |
| 10 | * little after the site does. | |
| 11 | */ | |
| 12 | export async function workspacePeople(slug: string, viewer: User): Promise<{ people: Mentionable[]; agents: WorkspaceAgent[] }> { | |
| 13 | const [members, agents] = await Promise.all([ | |
| 14 | identity.listMembers(slug, viewer).catch(() => null), | |
| 15 | workspaceAgents.list(slug, viewer).catch(() => null), | |
| 16 | ]); | |
| 17 | const people: Mentionable[] = members?.ok | |
| 18 | ? members.value.map((member) => ({ | |
| 19 | kind: "user" as const, | |
| 20 | name: member.username, | |
| Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar | 21 | display_username: member.display_username ?? null, |
| 22 | // As chat names them (`memberName`): display name, else the username as they wrote it. | |
| 23 | display_name: member.name?.trim() || shownUsername(member), | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 24 | avatar: member.avatar ?? null, |
| 25 | })) | |
| 26 | : []; | |
| 27 | return { people, agents: agents?.ok ? agents.value.filter((agent) => !agent.archived_at) : [] }; | |
| 28 | } | |
| 29 | ||
| 30 | /** An agent as a mention: by its handle, with its role beside it. */ | |
| 31 | export function agentMentionable(agent: WorkspaceAgent): Mentionable { | |
| 32 | return { kind: "agent", name: agent.handle, display_name: agent.display_name, avatar: agent.avatar, role: agent.role }; | |
| 33 | } | |
| 34 | ||
| 35 | /** The sidebar, or null when chat does not answer. */ | |
| 36 | export async function sidebarOrNull(slug: string, viewer: User): Promise<ChatSidebar | null> { | |
| 37 | try { | |
| 38 | const result = await chat.sidebar(slug, viewer); | |
| 39 | return result.ok ? result.value : null; | |
| 40 | } catch { | |
| 41 | return null; | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | /** | |
| 46 | * Members as a form sends them, `user:<username>` or `agent:<id>`, as the | |
| 47 | * chat service names them: a person by their account id. Anyone who is not | |
| 48 | * a member of the workspace is left out. | |
| 49 | */ | |
| 50 | export async function principalsFrom(slug: string, viewer: User, fields: string[]): Promise<Principal[]> { | |
| 51 | const usernames = fields.filter((f) => f.startsWith("user:")).map((f) => f.slice(5).toLowerCase()); | |
| 52 | const agentIds = fields.filter((f) => f.startsWith("agent:")).map((f) => f.slice(6)).filter(Boolean); | |
| 53 | const members = usernames.length > 0 ? await identity.listMembers(slug, viewer).catch(() => null) : null; | |
| 54 | const known = new Set(members?.ok ? members.value.map((m) => m.username.toLowerCase()) : []); | |
| 55 | const people = await Promise.all( | |
| 56 | usernames | |
| 57 | .filter((name) => known.has(name) && name !== viewer.username.toLowerCase()) | |
| 58 | .map((name) => identity.userByUsername(name).catch(() => null)), | |
| 59 | ); | |
| 60 | return [ | |
| 61 | ...people.filter((user): user is User => user != null).map((user) => ({ kind: "user" as const, id: user.id })), | |
| 62 | ...agentIds.map((id) => ({ kind: "agent" as const, id })), | |
| 63 | ]; | |
| 64 | } | |
| 65 | ||
| 66 | /** The cookie that remembers the last conversation open, so Chat opens there again. */ | |
| 67 | export const LAST_CHAT_COOKIE = "g1t_chat"; | |
| 68 | ||
| 69 | export function rememberChat(path: string, secure: boolean): string { | |
| 70 | return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`; | |
| 71 | } |