Skip to content
72 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),
Anyone makes an agent by describing it: one box drafts the whole agent (name, job, responsibilities, voice, skills to keep, integrations it needs, model limits and budget) as a card to change and try in a chat beside it before creating it, and an agent changes later by saying what to change, shown as a diff before it's saved. Members can make personal agents, which only they talk to and which spend from their own budget, unless owners turn that off; owners promote one to a workspace agent. The agents guide says how.15 // Their own personal agents too: a DM with one starts here like any other.
16 workspaceAgents.list(slug, viewer, { personal: "mine" }).catch(() => null),
Chat and workspace agents: channels, DMs and named agents you talk to17 ]);
18 const people: Mentionable[] = members?.ok
19 ? members.value.map((member) => ({
20 kind: "user" as const,
21 name: member.username,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar22 display_username: member.display_username ?? null,
23 // As chat names them (`memberName`): display name, else the username as they wrote it.
24 display_name: member.name?.trim() || shownUsername(member),
Chat and workspace agents: channels, DMs and named agents you talk to25 avatar: member.avatar ?? null,
26 }))
27 : [];
28 return { people, agents: agents?.ok ? agents.value.filter((agent) => !agent.archived_at) : [] };
29}
30
31/** An agent as a mention: by its handle, with its role beside it. */
32export function agentMentionable(agent: WorkspaceAgent): Mentionable {
33 return { kind: "agent", name: agent.handle, display_name: agent.display_name, avatar: agent.avatar, role: agent.role };
34}
35
36/** The sidebar, or null when chat does not answer. */
37export async function sidebarOrNull(slug: string, viewer: User): Promise<ChatSidebar | null> {
38 try {
39 const result = await chat.sidebar(slug, viewer);
40 return result.ok ? result.value : null;
41 } catch {
42 return null;
43 }
44}
45
46/**
47 * Members as a form sends them, `user:<username>` or `agent:<id>`, as the
48 * chat service names them: a person by their account id. Anyone who is not
49 * a member of the workspace is left out.
50 */
51export async function principalsFrom(slug: string, viewer: User, fields: string[]): Promise<Principal[]> {
52 const usernames = fields.filter((f) => f.startsWith("user:")).map((f) => f.slice(5).toLowerCase());
53 const agentIds = fields.filter((f) => f.startsWith("agent:")).map((f) => f.slice(6)).filter(Boolean);
54 const members = usernames.length > 0 ? await identity.listMembers(slug, viewer).catch(() => null) : null;
55 const known = new Set(members?.ok ? members.value.map((m) => m.username.toLowerCase()) : []);
56 const people = await Promise.all(
57 usernames
58 .filter((name) => known.has(name) && name !== viewer.username.toLowerCase())
59 .map((name) => identity.userByUsername(name).catch(() => null)),
60 );
61 return [
62 ...people.filter((user): user is User => user != null).map((user) => ({ kind: "user" as const, id: user.id })),
63 ...agentIds.map((id) => ({ kind: "agent" as const, id })),
64 ];
65}
66
67/** The cookie that remembers the last conversation open, so Chat opens there again. */
68export const LAST_CHAT_COOKIE = "g1t_chat";
69
70export function rememberChat(path: string, secure: boolean): string {
71 return `${LAST_CHAT_COOKIE}=${encodeURIComponent(path)}; Path=/; Max-Age=2592000; SameSite=Lax; HttpOnly${secure ? "; Secure" : ""}`;
72}