Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
298 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.

Chat and workspace agents: channels, DMs and named agents you talk to1/**
2 * Chat as the site draws it: which messages sit together under one name,
3 * where a day starts, what an `@…` being typed could become, and a
4 * message's text as safe blocks and spans. Pure, so it is tested on its
5 * own (chat.test.ts); the components are in components/chat.
6 *
7 * Message bodies are never HTML: they are parsed here into a small tree
8 * that React renders as text, so nothing anyone writes can run.
9 */
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.10import type { ChatMessage, ChatSidebarEntry, MemberProfile, Principal } from "@g1t/contracts";
Chat and workspace agents: channels, DMs and named agents you talk to11
12/** Two messages from the same author this close together share one header. */
13export const GROUP_GAP_MS = 5 * 60 * 1000;
14
15/** A message on its way: shown at once, then swapped for what the service saved. */
16export type ShownMessage = ChatMessage & { pending?: boolean; failed?: boolean; client_id?: string };
17
18export type TimelineRow =
19 | { kind: "day"; key: string; label: string }
20 | { kind: "message"; key: string; message: ShownMessage; head: boolean };
21
22function sameAuthor(a: ChatMessage["author"], b: ChatMessage["author"]): boolean {
23 return a.kind === b.kind && a.id === b.id;
24}
25
26/** The calendar day of an instant, in the reader's time zone (or `zone`). */
27export function dayKey(at: string | Date, zone?: string): string {
28 const date = typeof at === "string" ? new Date(at) : at;
29 return new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit" }).format(date);
30}
31
32/** "Today", "Yesterday", or the date, for the rule between days. */
33export function dayLabel(at: string, now: Date = new Date(), zone?: string): string {
34 const key = dayKey(at, zone);
35 if (key === dayKey(now, zone)) return "Today";
36 const yesterday = new Date(now.getTime() - 86_400_000);
37 if (key === dayKey(yesterday, zone)) return "Yesterday";
38 const date = new Date(at);
39 const sameYear = dayKey(date, zone).slice(0, 4) === dayKey(now, zone).slice(0, 4);
40 return new Intl.DateTimeFormat("en-US", {
41 timeZone: zone,
42 weekday: "long",
43 month: "long",
44 day: "numeric",
45 year: sameYear ? undefined : "numeric",
46 }).format(date);
47}
48
49/**
50 * The channel's messages, oldest first, as rows: a rule where each day
51 * starts, and each message marked with whether it starts a group (and so
52 * shows its author's name and avatar). A card always starts one.
53 */
54export function timeline(messages: ShownMessage[], now: Date = new Date(), zone?: string): TimelineRow[] {
55 const rows: TimelineRow[] = [];
56 let previous: ShownMessage | null = null;
57 let day: string | null = null;
58 for (const message of messages) {
59 if (message.deleted_at) continue;
60 const today = dayKey(message.created_at, zone);
61 if (today !== day) {
62 rows.push({ kind: "day", key: `day:${today}`, label: dayLabel(message.created_at, now, zone) });
63 day = today;
64 previous = null;
65 }
66 const head =
67 previous == null ||
68 message.kind === "card" ||
69 previous.kind === "card" ||
70 !sameAuthor(previous.author, message.author) ||
71 new Date(message.created_at).getTime() - new Date(previous.created_at).getTime() > GROUP_GAP_MS;
72 rows.push({ kind: "message", key: message.client_id ?? message.id, message, head });
73 previous = message;
74 }
75 return rows;
76}
77
78/**
79 * Messages merged by id, oldest first: what the page had, what arrived
80 * live and what a reload brought. A newer copy (an edit) wins; a pending
81 * message is dropped once the saved one with its `client_id` is there.
82 */
83export function mergeMessages(current: ShownMessage[], incoming: ShownMessage[]): ShownMessage[] {
84 const byId = new Map<string, ShownMessage>();
85 for (const message of [...current, ...incoming]) {
86 const key = message.id;
87 const had = byId.get(key);
88 if (!had || !message.pending || had.pending) byId.set(key, message);
89 }
90 const saved = new Set([...byId.values()].filter((m) => !m.pending).map((m) => m.client_id).filter(Boolean));
91 return [...byId.values()]
92 .filter((message) => !(message.pending && message.client_id && saved.has(message.client_id)))
93 .sort((a, b) => (a.pending === b.pending ? (a.id < b.id ? -1 : a.id > b.id ? 1 : 0) : a.pending ? 1 : -1));
94}
95
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.96/** Someone who can be mentioned: a person, by username. */
Chat and workspace agents: channels, DMs and named agents you talk to97export type Mentionable = {
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.98 kind: "user";
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar99 /** What follows the `@`: lowercased, as it is kept. */
Chat and workspace agents: channels, DMs and named agents you talk to100 name: string;
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar101 /** A person's username as they wrote it, when that differs from `name`. */
102 display_username?: string | null;
Chat and workspace agents: channels, DMs and named agents you talk to103 display_name: string;
104 avatar: string | null;
105};
106
107export type MentionQuery = { start: number; typed: string; options: Mentionable[] };
108
109/** Most suggestions at once. */
110export const MAX_MENTIONS = 6;
111
112/**
113 * The `@…` just before the caret and who it could be: names that start with
114 * what is typed first, then display names with a word that does. Null when
115 * the caret is not in a mention, or nothing fits.
116 */
117export function mentionQuery(text: string, caret: number, people: readonly Mentionable[]): MentionQuery | null {
118 const match = /(?:^|[\s(])@([\w.-]{0,40})$/.exec(text.slice(0, caret));
119 if (!match) return null;
120 const typed = match[1]!.toLowerCase();
121 const start = caret - match[1]!.length - 1;
122 const first: Mentionable[] = [];
123 const then: Mentionable[] = [];
124 for (const person of people) {
125 const name = person.name.toLowerCase();
126 if (name === typed) continue;
127 if (name.startsWith(typed)) first.push(person);
128 else if (typed && person.display_name.toLowerCase().split(/\s+/).some((word) => word.startsWith(typed))) then.push(person);
129 }
130 const options = [...first, ...then].slice(0, MAX_MENTIONS);
131 return options.length > 0 ? { start, typed, options } : null;
132}
133
134// ---------------------------------------------------------------------------
Merge main into Artifacts Phase 2135// A message's text: its Markdown, parsed by the rule every part of g1t reads it with.
Chat and workspace agents: channels, DMs and named agents you talk to136
Merge main into Artifacts Phase 2137export { type Block, type Span, blocks, inline, plainText, safeHref, spansText } from "@g1t/contracts/chat-markdown";
Chat and workspace agents: channels, DMs and named agents you talk to138
139/** Whether a message is only emoji, a few of them: shown larger, as people expect. */
140export function onlyEmoji(text: string): boolean {
141 const trimmed = text.trim();
142 if (!trimmed || trimmed.length > 24) return false;
143 return /^(?:\p{Extended_Pictographic}|\p{Emoji_Component}|‍|️|\s)+$/u.test(trimmed) && /\p{Extended_Pictographic}/u.test(trimmed);
144}
145
146// ---------------------------------------------------------------------------
147// The sidebar.
148
149export type ChatFilter = "all" | "unread" | "mentions";
150
A conversation with an archived agent leaves Chat's list. Until the agent is restored or deleted for good, its direct message is found by typing its name into Jump to channel or person, and it stays listed only while it is the conversation you are in; the chat and agents guides say so.151/**
152 * The conversations the sidebar lists, by filter and by what is typed in
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.153 * Jump to. An old direct message with agents alone (agents no longer
154 * exist) is not listed on its own: it is found by typing the agent's
155 * name, and stays while it is the conversation that is open (`current`,
156 * its channel id).
A conversation with an archived agent leaves Chat's list. Until the agent is restored or deleted for good, its direct message is found by typing its name into Jump to channel or person, and it stays listed only while it is the conversation you are in; the chat and agents guides say so.157 */
158export function filterEntries(entries: ChatSidebarEntry[], filter: ChatFilter, query: string, current: string | null = null): ChatSidebarEntry[] {
Chat and workspace agents: channels, DMs and named agents you talk to159 const q = query.trim().toLowerCase().replace(/^[#@]/, "");
160 return entries.filter((entry) => {
161 if (filter === "unread" && entry.unread === 0 && entry.mentions === 0) return false;
162 if (filter === "mentions" && entry.mentions === 0) return false;
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.163 if (!q) return entry.channel.id === current || !agentsOnlyDm(entry);
Chat and workspace agents: channels, DMs and named agents you talk to164 if (entry.title.toLowerCase().includes(q)) return true;
165 return entry.others.some((member) => member.name.toLowerCase().includes(q) || member.display_name.toLowerCase().includes(q));
166 });
167}
168
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.169/** Whether a direct message is with agents alone (one or several, nobody else): an old one, read-only now. */
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.170export function agentsOnlyDm(entry: { channel: { kind: "channel" | "dm" }; others: readonly Pick<MemberProfile, "kind">[] }): boolean {
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.171 return entry.channel.kind === "dm" && entry.others.length > 0 && entry.others.every((member) => member.kind === "agent");
172}
173
174/**
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.175 * The agent an old direct message was with, when the conversation is with
176 * agents alone: it opens read-only, a note where the composer was. Null for
177 * any other conversation, since a person is there to talk to.
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.178 */
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.179export function formerAgentDm<M extends Pick<MemberProfile, "kind">>(entry: { channel: { kind: "channel" | "dm" }; others: readonly M[] }): M | null {
180 return agentsOnlyDm(entry) ? (entry.others[0] ?? null) : null;
Agents have faces, and are never mistaken for people. Every agent wears a little bot face drawn from a look it owns, shape, colour, eyes, mouth, antenna, accessory and pattern, chosen in its builder and on its Profile tab with a live preview, Shuffle and a way back to the face its seed gives it; the face blinks on its own time, breathes, narrows its eyes while the agent works, shuts them asleep and bounces when it finishes, all of it still for anyone who asked for less motion. Wherever an agent shows, in chat, in a list, on a mention, on a review or a commit, its avatar carries an agent marker, and the people reading it are told so. In Chat, direct messages are two lists: People, and Agents, which also holds the agents you haven't talked to yet; a conversation with both a person and an agent in it is marked in the list, named in the conversation's header, spelled out by the composer and explained once the first time it opens. Agents keep their look in the agents service, which every service passes along. The chat and agents guides say so, and CONTRIBUTING makes the shared avatar the only way to draw an agent.181}
182
183/**
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)184 * The sidebar's sections, in the order they show: what is pinned (the
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.185 * `starred` preference), channels by name, then direct messages, the
186 * latest first.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)187 */
188export function sections(entries: ChatSidebarEntry[]): {
189 pinned: ChatSidebarEntry[];
190 channels: ChatSidebarEntry[];
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.191 dms: ChatSidebarEntry[];
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)192} {
Chat and workspace agents: channels, DMs and named agents you talk to193 const byName = (a: ChatSidebarEntry, b: ChatSidebarEntry) => a.title.localeCompare(b.title);
194 // Direct messages: the latest conversation first, as people scan them.
195 const byRecent = (a: ChatSidebarEntry, b: ChatSidebarEntry) =>
196 (b.channel.last_message_at ?? b.channel.created_at).localeCompare(a.channel.last_message_at ?? a.channel.created_at);
197 return {
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)198 pinned: entries.filter((e) => e.starred).sort(byName),
Chat and workspace agents: channels, DMs and named agents you talk to199 channels: entries.filter((e) => !e.starred && e.channel.kind === "channel").sort(byName),
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.200 // Your direct message with @g1t heads the list, whenever you last talked.
201 dms: entries.filter((e) => !e.starred && e.channel.kind === "dm").sort((a, b) => Number(g1tEntry(b)) - Number(g1tEntry(a)) || byRecent(a, b)),
Chat and workspace agents: channels, DMs and named agents you talk to202 };
203}
204
g1t coordinates the work and the person's own sessions do it: Ask g1t reads g1t with the same tools any agent gets, answers in one conversation shared with the @g1t direct message in Chat, and hands work to sessions in the person's own harness, whose subagents g1t records, ties to issues and pull requests, and lets see each other.205/** Whether a sidebar entry is the viewer's direct message with @g1t. */
206export function g1tEntry(entry: { channel: { kind: "channel" | "dm" }; others: readonly Pick<MemberProfile, "kind" | "id">[] }): boolean {
207 return entry.channel.kind === "dm" && entry.others.length === 1 && entry.others[0]!.kind === "system" && entry.others[0]!.id === "g1t";
208}
209
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.210/**
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.211 * What an old conversation with an agent says in place of its composer:
212 * agents no longer exist, so it is kept to read.
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.213 */
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.214export function formerAgentLine(agent: Pick<MemberProfile, "name" | "display_name"> & { display_username?: string | null }): string {
215 return `${shownName(agent)} was an agent and no longer answers here. Everything said here stays to read.`;
An archived agent can be found, brought back, or deleted for good. The Agents page ends with Archived, folded, where owners see every archived agent and members their own personal ones, each with when and by whom it was archived and what deleting it would take; Restore brings it back, under a new handle when a live agent has taken its old one, and Delete permanently asks for the handle to be typed and then removes its sessions, replies, memories, routines, versions, drafts, skills and its computer's home, leaving its messages in chat under its saved name, the workspace's spend totals and the audit log. In Chat an archived agent is deactivated the way people expect: never offered as someone to message or mention, never woken, a conversation with it kept in the list, dimmed and marked Archived, open to read with a note in place of the composer and a way to restore it for owners; after deletion the conversation still reads, its messages marked as a deleted agent's. The agents, chat and agent budgets guides say so.216}
217
Chat and workspace agents: channels, DMs and named agents you talk to218/** Where a conversation is, under the workspace. */
219export function channelPath(slug: string, channel: { id: string; kind: "channel" | "dm"; name: string | null }): string {
220 return channel.kind === "dm" || !channel.name ? `/${slug}/-/chat/dm/${channel.id}` : `/${slug}/-/chat/${channel.name}`;
221}
222
223/** Unread across the sidebar, leaving out what is muted: the rail's badge. */
224export function unreadTotals(entries: ChatSidebarEntry[]): { unread: number; mentions: number } {
225 let unread = 0;
226 let mentions = 0;
227 for (const entry of entries) {
228 mentions += entry.mentions;
229 if (!entry.muted) unread += entry.unread;
230 }
231 return { unread, mentions };
232}
233
234/** Channel names: lowercase words and digits joined by dashes, as people type them. */
235export function channelName(typed: string): string {
236 return typed
237 .toLowerCase()
238 .replace(/^#/, "")
239 .replace(/[\s_]+/g, "-")
240 .replace(/[^a-z0-9-]/g, "")
241 .replace(/-{2,}/g, "-")
242 .slice(0, 80);
243}
244
245/** Paths under `-/chat/` that are pages, not channels: never a channel's name. */
246export const RESERVED_CHANNEL_NAMES = ["dm", "browse", "live", "api", "new"] as const;
247
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.248/** A principal as a form field carries it: `user:<username>`. */
Chat and workspace agents: channels, DMs and named agents you talk to249export function principalField(kind: Principal["kind"], key: string): string {
250 return `${kind}:${key}`;
251}
252
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar253/**
254 * A member's handle as it shows after `@`: a person's username in its
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.255 * chosen case (an old agent's handle). The same rule as `memberHandle` in
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar256 * @g1t/contracts, kept here so this file stays pure for its tests.
257 */
258export function shownHandle(member: { name: string; display_username?: string | null }): string {
259 const display = member.display_username;
260 return display && display.toLowerCase() === member.name.toLowerCase() ? display : member.name;
261}
262
263/**
264 * The name shown for a member, everywhere in chat (messages, the sidebar,
265 * typing, cards): their display name, else their handle in its chosen
266 * case. The same rule as `memberName` in @g1t/contracts, which the chat
267 * service and its notifications follow.
268 */
269export function shownName(member: Pick<MemberProfile, "display_name" | "name"> & { display_username?: string | null }): string {
270 return member.display_name?.trim() || shownHandle(member);
271}
272
273/**
274 * Who each lowercased handle is, for drawing `@name` as the name people
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.275 * know: the workspace's people.
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar276 */
277export function mentionNames(people: readonly Mentionable[]): Map<string, string> {
278 const names = new Map<string, string>();
279 for (const person of people) names.set(person.name.toLowerCase(), shownName(person));
280 return names;
Chat and workspace agents: channels, DMs and named agents you talk to281}
282
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.283/** Someone shown as typing. */
284export type Typer = { member: Pick<MemberProfile, "display_name" | "name"> & { display_username?: string | null } };
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.285
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.286/** The typing line: who, and that they are typing. Null when nobody is. */
287export function typingLabel(typers: readonly Typer[]): { who: string; doing: string } | null {
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.288 if (!typers.length) return null;
289 const names = typers.map((t) => shownName(t.member));
The site no longer has g1t's own agents. g1t is where the coding agents you already use do their work, so the Agents mode is gone from the rail, with every page under /<workspace>/-/agents: personas, faces, skills, abilities, routines, computers, sessions and agent spend. Customize (skills, connectors, plugins, marketplaces), the context hub, model providers, the AI Gateway page, the Spend page and pill, Ask g1t, and the repository's Agents, Plans and agent settings pages go too, and their addresses now answer 404.290 const who = names.length === 1 ? names[0]! : names.length === 2 ? `${names[0]} and ${names[1]}` : "Several people";
291 return { who, doing: typers.length === 1 ? "is typing" : "are typing" };
In a group message with several agents, a message that names no agent goes to the one you are talking with: the agents that answered you last, else the ones you last mentioned, else, before anyone has been addressed, all of them; a message that mentions agents still goes only to those, and a conversation with one agent is as it was. Each agent is also told, when several are here, who you are talking with, so one that is woken alongside answers only what is clearly for it and does not redo a colleague's work. And an agent shows as typing for as long as it works, not for eight seconds: its desk says so every five seconds until the answer is posted, and after ten seconds the line reads that it is still working on it. The chat and agents guides say so.292}
293
Chat and workspace agents: channels, DMs and named agents you talk to294/** Reconnect delays for the live socket: doubling from one second to thirty, with jitter. */
295export function backoff(attempt: number, random: () => number = Math.random): number {
296 const base = Math.min(30_000, 1000 * 2 ** Math.max(0, attempt));
297 return Math.round(base * (0.75 + random() * 0.5));
298}

This file's history is long; its oldest lines are credited to the oldest commit read.