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.
| Chat and workspace agents: channels, DMs and named agents you talk to | 1 | import type { ChatSidebar, Principal, User, WorkspaceAgent } from "@g1t/contracts"; |
| 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, | |
| 21 | display_name: member.name?.trim() || member.username, | |
| 22 | avatar: member.avatar ?? null, | |
| 23 | })) | |
| 24 | : []; | |
| 25 | return { people, agents: agents?.ok ? agents.value.filter((agent) => !agent.archived_at) : [] }; | |
| 26 | } | |
| 27 | ||
| 28 | /** An agent as a mention: by its handle, with its role beside it. */ | |
| 29 | export function agentMentionable(agent: WorkspaceAgent): Mentionable { | |
| 30 | return { kind: "agent", name: agent.handle, display_name: agent.display_name, avatar: agent.avatar, role: agent.role }; | |
| 31 | } | |
| 32 | ||
| 33 | /** The sidebar, or null when chat does not answer. */ | |
| 34 | export async function sidebarOrNull(slug: string, viewer: User): Promise<ChatSidebar | null> { | |
| 35 | try { | |
| 36 | const result = await chat.sidebar(slug, viewer); | |
| 37 | return result.ok ? result.value : null; | |
| 38 | } catch { | |
| 39 | return null; | |
| 40 | } | |
| 41 | } | |
| 42 | ||
| 43 | /** | |
| 44 | * Members as a form sends them, `user:<username>` or `agent:<id>`, as the | |
| 45 | * chat service names them: a person by their account id. Anyone who is not | |
| 46 | * a member of the workspace is left out. | |
| 47 | */ | |
| 48 | export async function principalsFrom(slug: string, viewer: User, fields: string[]): Promise<Principal[]> { | |
| 49 | const usernames = fields.filter((f) => f.startsWith("user:")).map((f) => f.slice(5).toLowerCase()); | |
| 50 | const agentIds = fields.filter((f) => f.startsWith("agent:")).map((f) => f.slice(6)).filter(Boolean); | |
| 51 | const members = usernames.length > 0 ? await identity.listMembers(slug, viewer).catch(() => null) : null; | |
| 52 | const known = new Set(members?.ok ? members.value.map((m) => m.username.toLowerCase()) : []); | |
| 53 | const people = await Promise.all( | |
| 54 | usernames | |
| 55 | .filter((name) => known.has(name) && name !== viewer.username.toLowerCase()) | |
| 56 | .map((name) => identity.userByUsername(name).catch(() => null)), | |
| 57 | ); | |
| 58 | return [ | |
| 59 | ...people.filter((user): user is User => user != null).map((user) => ({ kind: "user" as const, id: user.id })), | |
| 60 | ...agentIds.map((id) => ({ kind: "agent" as const, id })), | |
| 61 | ]; | |
| 62 | } | |
| 63 | ||
| 64 | /** The cookie that remembers the last conversation open, so Chat opens there again. */ | |
| 65 | export const LAST_CHAT_COOKIE = "g1t_chat"; | |
| 66 | ||
| 67 | export function rememberChat(path: string, secure: boolean): string { | |
| 68 | return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`; | |
| 69 | } |