Skip to content
272 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 {
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.7 agentsToTalkTo,
8 archivedAgentLine,
9 archivedAgentWhen,
10 archivedAgentsDm,
Chat and workspace agents: channels, DMs and named agents you talk to11 backoff,
12 blocks,
13 channelName,
14 channelPath,
15 dayLabel,
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.16 dmPlaceholder,
Chat and workspace agents: channels, DMs and named agents you talk to17 filterEntries,
18 inline,
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.19 mixedDm,
20 mixedNotice,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar21 mentionNames,
Chat and workspace agents: channels, DMs and named agents you talk to22 mentionQuery,
23 mergeMessages,
24 safeHref,
25 sections,
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar26 shownHandle,
27 shownName,
Chat and workspace agents: channels, DMs and named agents you talk to28 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.29 typingLabel,
Chat and workspace agents: channels, DMs and named agents you talk to30 unreadTotals,
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.31 WORKING_AFTER_MS,
32 agentDoing,
Chat and workspace agents: channels, DMs and named agents you talk to33 type ShownMessage,
34} from "./chat.ts";
35
36const ana = { kind: "user" as const, id: "u1", name: "ana", display_name: "Ana", avatar: null, role: null };
37const bot = { kind: "agent" as const, id: "a1", name: "reviewer", display_name: "Reviewer", avatar: null, role: "Reviews pull requests" };
38
39function message(id: string, at: string, over: Partial<ChatMessage> = {}): ShownMessage {
40 return {
41 id,
42 channel_id: "c1",
43 author: ana,
44 kind: "text",
45 body: "hi",
46 card: null,
47 thread_root: null,
48 reply_count: 0,
49 last_reply_at: null,
50 created_at: at,
51 edited_at: null,
52 deleted_at: null,
53 ...over,
54 };
55}
56
57test("messages from one author close together share a header; days get a rule", () => {
58 const now = new Date("2026-10-08T15:00:00Z");
59 const rows = timeline(
60 [
61 message("01", "2026-10-07T10:00:00Z"),
62 message("02", "2026-10-08T10:00:00Z"),
63 message("03", "2026-10-08T10:02:00Z"),
64 message("04", "2026-10-08T10:03:00Z", { author: bot }),
65 message("05", "2026-10-08T10:30:00Z", { author: bot }),
66 message("06", "2026-10-08T10:31:00Z", { author: bot, kind: "card" }),
67 message("07", "2026-10-08T10:32:00Z", { deleted_at: "2026-10-08T10:33:00Z" }),
68 ],
69 now,
70 "UTC",
71 );
72 assert.deepEqual(
73 rows.map((row) => (row.kind === "day" ? row.label : `${row.message.id}${row.head ? "*" : ""}`)),
74 ["Yesterday", "01*", "Today", "02*", "03", "04*", "05*", "06*"],
75 );
76 assert.equal(dayLabel("2026-09-01T10:00:00Z", now, "UTC"), "Tuesday, September 1");
77});
78
79test("merging keeps one copy of each message, and drops the pending one once saved", () => {
80 const pending = { ...message("tmp-1", "2026-10-08T10:00:00Z"), pending: true, client_id: "tmp-1" };
81 const saved = { ...message("05", "2026-10-08T10:00:01Z"), client_id: "tmp-1" };
82 const merged = mergeMessages([message("02", "2026-10-08T09:00:00Z"), pending], [saved, message("02", "2026-10-08T09:00:00Z", { body: "edited" })]);
83 assert.deepEqual(merged.map((m) => m.id), ["02", "05"]);
84 assert.equal(merged[0]!.body, "edited");
85 // Still pending: last, after everything saved.
86 assert.deepEqual(mergeMessages([pending], [message("09", "2026-10-08T11:00:00Z")]).map((m) => m.id), ["09", "tmp-1"]);
87});
88
89test("an @ being typed suggests people and agents", () => {
90 const people = [
91 { kind: "user" as const, name: "ana", display_name: "Ana Lima", avatar: null },
92 { kind: "agent" as const, name: "reviewer", display_name: "Reviewer", avatar: null },
93 { kind: "user" as const, name: "bo", display_name: "Robert Reed", avatar: null },
94 ];
95 assert.deepEqual(mentionQuery("hey @re", 7, people)?.options.map((p) => p.name), ["reviewer", "bo"]);
96 assert.equal(mentionQuery("hey @re", 7, people)?.start, 4);
97 assert.equal(mentionQuery("mail me@an", 10, people), null);
98 assert.deepEqual(mentionQuery("@", 1, people)?.options.length, 3);
99 assert.equal(mentionQuery("@zz", 3, people), null);
100});
101
102test("message text: mentions, references, links and code, never HTML", () => {
103 assert.deepEqual(inline("ask @reviewer about #12 and g1t#3"), [
104 { t: "text", v: "ask " },
105 { t: "mention", name: "reviewer" },
106 { t: "text", v: " about " },
107 { t: "ref", repo: null, number: 12 },
108 { t: "text", v: " and " },
109 { t: "ref", repo: "g1t", number: 3 },
110 ]);
111 assert.deepEqual(inline("see #general, `a <b>` **bold**"), [
112 { t: "text", v: "see " },
113 { t: "channel", name: "general" },
114 { t: "text", v: ", " },
115 { t: "code", v: "a <b>" },
116 { t: "text", v: " " },
117 { t: "strong", c: [{ t: "text", v: "bold" }] },
118 ]);
119 assert.ok(inline("[x](javascript:alert(1))").every((span) => span.t === "text"));
120 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: "." }]);
121 assert.deepEqual(inline("me@example.com"), [{ t: "text", v: "me@example.com" }]);
122 assert.equal(safeHref("//evil.example"), null);
123 assert.equal(safeHref("/acme/web/pull/3"), "/acme/web/pull/3");
124});
125
126test("message blocks: fenced code, lists, quotes and paragraphs", () => {
127 const parsed = blocks("Plan:\n- one\n- two\n\n```ts\nconst a = 1;\n```\n> quoted\nlast");
128 assert.deepEqual(parsed.map((block) => block.t), ["p", "list", "code", "quote", "p"]);
129 assert.deepEqual(parsed[2], { t: "code", lang: "ts", v: "const a = 1;" });
130});
131
132function entry(id: string, over: Partial<ChatSidebarEntry> & { kind?: "channel" | "dm"; last?: string } = {}): ChatSidebarEntry {
133 return {
134 channel: {
135 id,
136 workspace_id: "w",
137 kind: over.kind ?? "channel",
138 name: over.kind === "dm" ? null : id,
139 topic: null,
140 private: false,
141 created_by: { kind: "user", id: "u1" },
142 created_at: "2026-10-01T00:00:00Z",
143 archived_at: null,
144 last_message_at: over.last ?? null,
145 },
146 title: over.title ?? id,
147 others: over.others ?? [],
148 starred: over.starred ?? false,
149 muted: over.muted ?? false,
150 unread: over.unread ?? 0,
151 mentions: over.mentions ?? 0,
152 };
153}
154
155test("the sidebar: sections, filters and the rail's count", () => {
156 const all = [
157 entry("random"),
158 entry("general", { starred: true, unread: 2 }),
159 entry("d1", { kind: "dm", title: "reviewer", others: [bot], last: "2026-10-02T00:00:00Z", mentions: 1, unread: 1 }),
160 entry("d2", { kind: "dm", title: "ana", others: [ana], last: "2026-10-05T00:00:00Z" }),
161 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.162 // Ana and the reviewer together: a person is in it, so it lists under People, marked as mixed.
163 entry("d3", { kind: "dm", title: "ana, reviewer", others: [ana, bot], last: "2026-10-03T00:00:00Z" }),
164 entry("d4", { kind: "dm", title: "notes", others: [], last: "2026-10-01T00:00:00Z" }),
Chat and workspace agents: channels, DMs and named agents you talk to165 ];
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 const { pinned, channels, agentDms, people, agents } = 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)167 assert.deepEqual(pinned.map((e) => e.title), ["general"]);
Chat and workspace agents: channels, DMs and named agents you talk to168 assert.deepEqual(channels.map((e) => e.title), ["noisy", "random"]);
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.169 // Direct messages in two buckets, each the latest first: people (mixed ones and your notes among them), and agents alone.
170 assert.deepEqual(people.map((e) => e.title), ["ana", "ana, reviewer", "notes"]);
171 assert.deepEqual(agents.map((e) => e.title), ["reviewer"]);
172 assert.equal(agentDms.get("a1")?.title, "reviewer", "the agent's own conversation, not the mixed one");
173 assert.deepEqual(all.map(mixedDm), [false, false, false, false, false, true, false]);
Chat and workspace agents: channels, DMs and named agents you talk to174 assert.deepEqual(filterEntries(all, "unread", "").map((e) => e.title), ["general", "reviewer", "noisy"]);
175 assert.deepEqual(filterEntries(all, "mentions", "").map((e) => e.title), ["reviewer"]);
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.176 // Jump to finds the agent's own conversation and the mixed one it is in.
177 assert.deepEqual(filterEntries(all, "all", "@Review").map((e) => e.title), ["reviewer", "ana, reviewer"]);
Chat and workspace agents: channels, DMs and named agents you talk to178 assert.deepEqual(unreadTotals(all), { unread: 3, mentions: 1 });
179});
180
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.181test("an archived agent's conversation leaves the list until searched for or open; the agent itself is no longer offered", () => {
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.182 const archived = { ...bot, id: "a2", name: "scribe", display_name: "Scribe", archived_at: "2026-10-08T09:00:00Z", archived_by: "ana" };
183 const all = [
184 entry("d1", { kind: "dm", title: "reviewer", others: [bot], last: "2026-10-02T00:00:00Z" }),
185 entry("d2", { kind: "dm", title: "scribe", others: [archived], last: "2026-10-05T00:00:00Z" }),
186 // With a person in it, the conversation is theirs to go on with: not an archived one.
187 entry("d3", { kind: "dm", title: "ana, scribe", others: [ana, archived], last: "2026-10-03T00:00:00Z" }),
188 // Two agents, one archived: the live one still answers.
189 entry("d4", { kind: "dm", title: "reviewer, scribe", others: [bot, archived], last: "2026-10-04T00:00:00Z" }),
190 // Deleted for good: the conversation stays readable, under the name it had.
191 entry("d5", { kind: "dm", title: "triage", others: [{ ...bot, id: "a9", name: "triage", display_name: "Triage", deleted: true }], last: "2026-10-01T00:00:00Z" }),
192 ];
193 const { agents, agentDms } = sections(all);
194 assert.deepEqual(agents.map((e) => e.title), ["scribe", "reviewer, scribe", "reviewer", "triage"]);
195 assert.deepEqual(all.map((e) => archivedAgentsDm(e)?.name ?? null), [null, "scribe", null, null, "triage"]);
196 assert.equal(archivedAgentsDm(all[4]!)?.deleted, true);
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.197 // Not listed on their own; found by name; kept while open.
198 assert.deepEqual(filterEntries(all, "all", "").map((e) => e.title), ["reviewer", "ana, scribe", "reviewer, scribe"]);
199 assert.deepEqual(filterEntries(all, "all", "scri").map((e) => e.title), ["scribe", "ana, scribe", "reviewer, scribe"]);
200 assert.deepEqual(filterEntries(all, "all", "", "d5").map((e) => e.title), ["reviewer", "ana, scribe", "reviewer, scribe", "triage"]);
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.201 // The agents you haven't talked to yet: live ones without a conversation, g1t first; never an archived one.
202 const roster = [
203 { id: "a1", handle: "reviewer", display_name: "Reviewer", role: "Reviews", builtin: false, archived_at: null },
204 { id: "a2", handle: "scribe", display_name: "Scribe", role: "Writes", builtin: false, archived_at: "2026-10-08T09:00:00Z" },
205 { id: "a3", handle: "zed", display_name: "Zed", role: "Ships", builtin: false, archived_at: null },
206 { id: "a4", handle: "g1t", display_name: "g1t", role: "Orchestrates", builtin: true, archived_at: null },
207 { id: "a5", handle: "old", display_name: "Old", role: "Nothing", builtin: false, archived_at: "2026-09-01T00:00:00Z" },
208 ];
209 assert.deepEqual(agentsToTalkTo(roster, agentDms).map((a) => a.handle), ["g1t", "zed"]);
210 assert.deepEqual(agentsToTalkTo(roster, agentDms, "@ship").map((a) => a.handle), ["zed"]);
211 assert.deepEqual(agentsToTalkTo(roster, new Map()).map((a) => a.handle), ["g1t", "reviewer", "zed"]);
212 const now = new Date("2026-10-10T12:00:00Z");
213 assert.equal(archivedAgentLine(archived, now), "Scribe was archived 2 days ago by @ana. It no longer answers here.");
214 assert.equal(archivedAgentLine({ ...archived, archived_by: null, archived_at: "2026-10-10T11:30:00Z" }, now), "Scribe was archived 30 minutes ago. It no longer answers here.");
215 assert.equal(archivedAgentLine({ ...archived, archived_by: null, archived_at: null }, now), "Scribe was archived. It no longer answers here.");
216 assert.equal(archivedAgentWhen(archived, now), "Archived 2 days ago by @ana");
217 assert.equal(archivedAgentWhen({ archived_at: "2026-10-10T11:00:00Z", archived_by: null }, now), "Archived 1 hour ago");
218});
219
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.220test("a mixed conversation says who the agents are, in the composer and above its first message", () => {
221 assert.equal(dmPlaceholder([ana, bot]), "Message @ana and @reviewer (agent)");
222 assert.equal(dmPlaceholder([{ ...ana, display_username: "Ana" }, bot, { ...bot, id: "a2", name: "scribe", display_name: "Scribe" }]), "Message @Ana, @reviewer (agent) and @scribe (agent)");
223 assert.equal(dmPlaceholder([]), "Message yourself");
224 assert.equal(mixedNotice([ana, bot]), "Reviewer is an agent and reads everything here; it can act on what you say.");
225 assert.equal(mixedNotice([ana, bot, { ...bot, id: "a2", name: "scribe", display_name: "Scribe" }]), "Reviewer and Scribe are agents and read everything here; they can act on what you say.");
226 assert.equal(mixedNotice([ana]), "");
227});
228
Chat and workspace agents: channels, DMs and named agents you talk to229test("addresses, names, money and reconnecting", () => {
230 assert.equal(channelPath("acme", { id: "c1", kind: "channel", name: "general" }), "/acme/-/chat/general");
231 assert.equal(channelPath("acme", { id: "d1", kind: "dm", name: null }), "/acme/-/chat/dm/d1");
232 assert.equal(channelName("#Release Train_2!"), "release-train-2");
233 assert.equal(backoff(0, () => 0.5), 1000);
234 assert.equal(backoff(10, () => 0.5), 30_000);
235});
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar236
237test("a member shows by display name, else by their username as they wrote it", () => {
238 assert.equal(shownName({ name: "ana", display_name: "Ana Lima", display_username: "Ana" }), "Ana Lima");
239 assert.equal(shownName({ name: "ana", display_name: " ", display_username: "Ana" }), "Ana");
240 assert.equal(shownName({ name: "ana", display_name: "" }), "ana");
241 assert.equal(shownHandle({ name: "ana", display_username: "Ana" }), "Ana");
242 assert.equal(shownHandle({ name: "ana", display_username: "Bo" }), "ana");
243});
244
245test("mentions read as the names people know, found by the lowercased handle", () => {
246 const names = mentionNames([
247 { kind: "user", name: "ana", display_username: "Ana", display_name: "Ana Lima", avatar: null },
248 { kind: "user", name: "bo", display_username: "Bo", display_name: "", avatar: null },
249 { kind: "agent", name: "reviewer", display_name: "Reviewer", avatar: null },
250 ]);
251 assert.equal(names.get("ana"), "Ana Lima");
252 assert.equal(names.get("bo"), "Bo");
253 assert.equal(names.get("reviewer"), "Reviewer");
254 assert.equal(names.get("cy"), undefined);
255});
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.256
257test("the typing line: people type, an agent does what its phrase says, then is still working on it", () => {
258 const now = 100_000;
259 const mike = { kind: "agent" as const, id: "a2", name: "mike", display_name: "Mike", avatar: null, role: null };
260 assert.equal(typingLabel([], now), null);
261 assert.deepEqual(typingLabel([{ member: ana, since: now - 60_000 }], now), { who: "Ana", doing: "is typing" }, "a person is never 'working'");
262 assert.deepEqual(typingLabel([{ member: bot, since: now - 2_000 }], now), { who: "Reviewer", doing: agentDoing("a1") });
263 assert.match(agentDoing("a1"), /^is /);
264 assert.deepEqual(typingLabel([{ member: bot, since: now - WORKING_AFTER_MS }], now), { who: "Reviewer", doing: "is still working on it" });
265 // Mike and @g1t on one reply each: on it, then still working, as the user saw them vanish after eight seconds before.
266 assert.deepEqual(typingLabel([{ member: mike, since: now - 3_000 }, { member: bot, since: now - 1_000 }], now), { who: "Mike and Reviewer", doing: "are on it" });
267 assert.deepEqual(typingLabel([{ member: mike, since: now - 30_000 }, { member: bot, since: now - 1_000 }], now), { who: "Mike and Reviewer", doing: "are still working on it" });
268 assert.deepEqual(typingLabel([{ member: ana, since: now }, { member: bot, since: now - 30_000 }], now), { who: "Ana and Reviewer", doing: "are typing" }, "people and agents together");
269 const third = { ...ana, id: "u3", name: "bo", display_name: "Bo" };
270 assert.deepEqual(typingLabel([{ member: ana, since: now }, { member: bot, since: now }, { member: third, since: now }], now), { who: "Several people", doing: "are typing" });
271 assert.deepEqual(typingLabel([{ member: mike, since: now }, { member: bot, since: now }, { member: { ...mike, id: "a3", name: "sam", display_name: "Sam" }, since: now }], now), { who: "Several agents", doing: "are on it" });
272});

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