Skip to content
71 linesCodeBlameRaw

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 calendar1import { type ChatSidebar, type Principal, type User, type WorkspaceAgent, shownUsername } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to2
3import type { Mentionable } from "./chat";
4import { 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 */
12export 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 calendar21 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 to24 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. */
31export 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. */
36export 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 */
50export 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. */
67export const LAST_CHAT_COOKIE = "g1t_chat";
68
69export function rememberChat(path: string, secure: boolean): string {
70 return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`;
71}