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
217 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 to1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import type { ChatMessage, ChatSidebarEntry } from "@g1t/contracts";
5
6import {
7 backoff,
8 blocks,
9 channelName,
10 channelPath,
11 dayLabel,
12 filterEntries,
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.13 formerAgentDm,
14 formerAgentLine,
Chat and workspace agents: channels, DMs and named agents you talk to15 inline,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar16 mentionNames,
Chat and workspace agents: channels, DMs and named agents you talk to17 mentionQuery,
18 mergeMessages,
19 safeHref,
20 sections,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar21 shownHandle,
22 shownName,
Chat and workspace agents: channels, DMs and named agents you talk to23 timeline,
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.24 typingLabel,
Chat and workspace agents: channels, DMs and named agents you talk to25 unreadTotals,
26 type ShownMessage,
27} from "./chat.ts";
28
29const ana = { kind: "user" as const, id: "u1", name: "ana", display_name: "Ana", avatar: null, role: null };
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.30// An old message's author: an agent that no longer exists.
Chat and workspace agents: channels, DMs and named agents you talk to31const bot = { kind: "agent" as const, id: "a1", name: "reviewer", display_name: "Reviewer", avatar: null, role: "Reviews pull requests" };
32
33function message(id: string, at: string, over: Partial<ChatMessage> = {}): ShownMessage {
34 return {
35 id,
36 channel_id: "c1",
37 author: ana,
38 kind: "text",
39 body: "hi",
40 card: null,
41 thread_root: null,
42 reply_count: 0,
43 last_reply_at: null,
44 created_at: at,
45 edited_at: null,
46 deleted_at: null,
47 ...over,
48 };
49}
50
51test("messages from one author close together share a header; days get a rule", () => {
52 const now = new Date("2026-10-08T15:00:00Z");
53 const rows = timeline(
54 [
55 message("01", "2026-10-07T10:00:00Z"),
56 message("02", "2026-10-08T10:00:00Z"),
57 message("03", "2026-10-08T10:02:00Z"),
58 message("04", "2026-10-08T10:03:00Z", { author: bot }),
59 message("05", "2026-10-08T10:30:00Z", { author: bot }),
60 message("06", "2026-10-08T10:31:00Z", { author: bot, kind: "card" }),
61 message("07", "2026-10-08T10:32:00Z", { deleted_at: "2026-10-08T10:33:00Z" }),
62 ],
63 now,
64 "UTC",
65 );
66 assert.deepEqual(
67 rows.map((row) => (row.kind === "day" ? row.label : `${row.message.id}${row.head ? "*" : ""}`)),
68 ["Yesterday", "01*", "Today", "02*", "03", "04*", "05*", "06*"],
69 );
70 assert.equal(dayLabel("2026-09-01T10:00:00Z", now, "UTC"), "Tuesday, September 1");
71});
72
73test("merging keeps one copy of each message, and drops the pending one once saved", () => {
74 const pending = { ...message("tmp-1", "2026-10-08T10:00:00Z"), pending: true, client_id: "tmp-1" };
75 const saved = { ...message("05", "2026-10-08T10:00:01Z"), client_id: "tmp-1" };
76 const merged = mergeMessages([message("02", "2026-10-08T09:00:00Z"), pending], [saved, message("02", "2026-10-08T09:00:00Z", { body: "edited" })]);
77 assert.deepEqual(merged.map((m) => m.id), ["02", "05"]);
78 assert.equal(merged[0]!.body, "edited");
79 // Still pending: last, after everything saved.
80 assert.deepEqual(mergeMessages([pending], [message("09", "2026-10-08T11:00:00Z")]).map((m) => m.id), ["09", "tmp-1"]);
81});
82
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.83test("an @ being typed suggests people", () => {
Chat and workspace agents: channels, DMs and named agents you talk to84 const people = [
85 { kind: "user" as const, name: "ana", display_name: "Ana Lima", avatar: null },
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.86 { kind: "user" as const, name: "rene", display_name: "Rene", avatar: null },
Chat and workspace agents: channels, DMs and named agents you talk to87 { kind: "user" as const, name: "bo", display_name: "Robert Reed", avatar: null },
88 ];
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.89 assert.deepEqual(mentionQuery("hey @re", 7, people)?.options.map((p) => p.name), ["rene", "bo"]);
Chat and workspace agents: channels, DMs and named agents you talk to90 assert.equal(mentionQuery("hey @re", 7, people)?.start, 4);
91 assert.equal(mentionQuery("mail me@an", 10, people), null);
92 assert.deepEqual(mentionQuery("@", 1, people)?.options.length, 3);
93 assert.equal(mentionQuery("@zz", 3, people), null);
94});
95
96test("message text: mentions, references, links and code, never HTML", () => {
97 assert.deepEqual(inline("ask @reviewer about #12 and g1t#3"), [
98 { t: "text", v: "ask " },
99 { t: "mention", name: "reviewer" },
100 { t: "text", v: " about " },
101 { t: "ref", repo: null, number: 12 },
102 { t: "text", v: " and " },
103 { t: "ref", repo: "g1t", number: 3 },
104 ]);
105 assert.deepEqual(inline("see #general, `a <b>` **bold**"), [
106 { t: "text", v: "see " },
107 { t: "channel", name: "general" },
108 { t: "text", v: ", " },
109 { t: "code", v: "a <b>" },
110 { t: "text", v: " " },
111 { t: "strong", c: [{ t: "text", v: "bold" }] },
112 ]);
113 assert.ok(inline("[x](javascript:alert(1))").every((span) => span.t === "text"));
114 assert.deepEqual(inline("https://g1t.sh/a."), [{ t: "link", href: "https://g1t.sh/a", c: [{ t: "text", v: "https://g1t.sh/a" }] }, { t: "text", v: "." }]);
115 assert.deepEqual(inline("me@example.com"), [{ t: "text", v: "me@example.com" }]);
116 assert.equal(safeHref("//evil.example"), null);
117 assert.equal(safeHref("/acme/web/pull/3"), "/acme/web/pull/3");
118});
119
120test("message blocks: fenced code, lists, quotes and paragraphs", () => {
121 const parsed = blocks("Plan:\n- one\n- two\n\n```ts\nconst a = 1;\n```\n> quoted\nlast");
122 assert.deepEqual(parsed.map((block) => block.t), ["p", "list", "code", "quote", "p"]);
123 assert.deepEqual(parsed[2], { t: "code", lang: "ts", v: "const a = 1;" });
124});
125
126function entry(id: string, over: Partial<ChatSidebarEntry> & { kind?: "channel" | "dm"; last?: string } = {}): ChatSidebarEntry {
127 return {
128 channel: {
129 id,
130 workspace_id: "w",
131 kind: over.kind ?? "channel",
132 name: over.kind === "dm" ? null : id,
133 topic: null,
134 private: false,
135 created_by: { kind: "user", id: "u1" },
136 created_at: "2026-10-01T00:00:00Z",
137 archived_at: null,
138 last_message_at: over.last ?? null,
139 },
140 title: over.title ?? id,
141 others: over.others ?? [],
142 starred: over.starred ?? false,
143 muted: over.muted ?? false,
144 unread: over.unread ?? 0,
145 mentions: over.mentions ?? 0,
146 };
147}
148
149test("the sidebar: sections, filters and the rail's count", () => {
150 const all = [
151 entry("random"),
152 entry("general", { starred: true, unread: 2 }),
153 entry("d1", { kind: "dm", title: "reviewer", others: [bot], last: "2026-10-02T00:00:00Z", mentions: 1, unread: 1 }),
154 entry("d2", { kind: "dm", title: "ana", others: [ana], last: "2026-10-05T00:00:00Z" }),
155 entry("noisy", { muted: true, unread: 9 }),
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.156 entry("d3", { kind: "dm", title: "ana, reviewer", others: [ana, bot], last: "2026-10-03T00:00:00Z" }),
157 entry("d4", { kind: "dm", title: "notes", others: [], last: "2026-10-01T00:00:00Z" }),
Chat and workspace agents: channels, DMs and named agents you talk to158 ];
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.159 const { pinned, channels, dms } = sections(all);
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)160 assert.deepEqual(pinned.map((e) => e.title), ["general"]);
Chat and workspace agents: channels, DMs and named agents you talk to161 assert.deepEqual(channels.map((e) => e.title), ["noisy", "random"]);
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.162 assert.deepEqual(dms.map((e) => e.title), ["ana", "ana, reviewer", "reviewer", "notes"]);
163 // An old conversation with an agent alone is left out until searched for or open.
164 assert.deepEqual(filterEntries(all, "unread", "").map((e) => e.title), ["general", "noisy"]);
165 assert.deepEqual(filterEntries(all, "unread", "", "d1").map((e) => e.title), ["general", "reviewer", "noisy"]);
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.166 assert.deepEqual(filterEntries(all, "all", "@Review").map((e) => e.title), ["reviewer", "ana, reviewer"]);
Chat and workspace agents: channels, DMs and named agents you talk to167 assert.deepEqual(unreadTotals(all), { unread: 3, mentions: 1 });
168});
169
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.170test("an old conversation with an agent alone opens read-only, under the name the agent had", () => {
171 const deleted = { ...bot, id: "a9", name: "triage", display_name: "Triage", deleted: true };
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.172 const all = [
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.173 entry("d1", { kind: "dm", title: "triage", others: [deleted], last: "2026-10-01T00:00:00Z" }),
174 // With a person in it, the conversation is theirs to go on with.
175 entry("d2", { kind: "dm", title: "ana, triage", others: [ana, deleted], last: "2026-10-03T00:00:00Z" }),
176 entry("d3", { kind: "dm", title: "ana", others: [ana], last: "2026-10-04T00:00:00Z" }),
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.177 ];
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.178 assert.deepEqual(all.map((e) => formerAgentDm(e)?.name ?? null), ["triage", null, null]);
179 assert.equal(formerAgentLine(deleted), "Triage was an agent and no longer answers here. Everything said here stays to read.");
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.180});
181
Chat and workspace agents: channels, DMs and named agents you talk to182test("addresses, names, money and reconnecting", () => {
183 assert.equal(channelPath("acme", { id: "c1", kind: "channel", name: "general" }), "/acme/-/chat/general");
184 assert.equal(channelPath("acme", { id: "d1", kind: "dm", name: null }), "/acme/-/chat/dm/d1");
185 assert.equal(channelName("#Release Train_2!"), "release-train-2");
186 assert.equal(backoff(0, () => 0.5), 1000);
187 assert.equal(backoff(10, () => 0.5), 30_000);
188});
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar189
190test("a member shows by display name, else by their username as they wrote it", () => {
191 assert.equal(shownName({ name: "ana", display_name: "Ana Lima", display_username: "Ana" }), "Ana Lima");
192 assert.equal(shownName({ name: "ana", display_name: " ", display_username: "Ana" }), "Ana");
193 assert.equal(shownName({ name: "ana", display_name: "" }), "ana");
194 assert.equal(shownHandle({ name: "ana", display_username: "Ana" }), "Ana");
195 assert.equal(shownHandle({ name: "ana", display_username: "Bo" }), "ana");
196});
197
198test("mentions read as the names people know, found by the lowercased handle", () => {
199 const names = mentionNames([
200 { kind: "user", name: "ana", display_username: "Ana", display_name: "Ana Lima", avatar: null },
201 { kind: "user", name: "bo", display_username: "Bo", display_name: "", avatar: null },
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.202 { kind: "user", name: "rene", display_name: "Rene", avatar: null },
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar203 ]);
204 assert.equal(names.get("ana"), "Ana Lima");
205 assert.equal(names.get("bo"), "Bo");
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.206 assert.equal(names.get("rene"), "Rene");
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar207 assert.equal(names.get("cy"), undefined);
208});
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.209
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.210test("the typing line: who is typing", () => {
211 assert.equal(typingLabel([]), null);
212 assert.deepEqual(typingLabel([{ member: ana }]), { who: "Ana", doing: "is typing" });
213 const bo = { ...ana, id: "u2", name: "bo", display_name: "Bo" };
214 assert.deepEqual(typingLabel([{ member: ana }, { member: bo }]), { who: "Ana and Bo", doing: "are typing" });
215 const third = { ...ana, id: "u3", name: "cy", display_name: "Cy" };
216 assert.deepEqual(typingLabel([{ member: ana }, { member: bo }, { member: third }]), { who: "Several people", doing: "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.217});

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