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.
| 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) | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { FeedNotification } from "@g1t/contracts"; | |
| 5 | ||
| 6 | import { applyCounts, totals } from "./counts.ts"; | |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 7 | import { STALE_MS, anyFocused, cleanCard, cleanNotification, decide, levelFor, mergePreferences, pushActions, pushPayload, readPreferences, wants } from "./prefs.ts"; |
| 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) | 8 | |
| 9 | const NOW = 1_800_000_000_000; | |
| 10 | const dms = { level: "dms_mentions" as const, workspaces: {} }; | |
| 11 | ||
| 12 | test("the default hears of what is said to you or waits on you, not everything", () => { | |
| 13 | assert.deepEqual(readPreferences(null), dms); | |
| Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents. | 14 | for (const kind of ["dm", "mention", "thread_reply", "approval"] as const) assert.ok(wants("dms_mentions", kind), kind); |
| 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) | 15 | assert.ok(!wants("dms_mentions", "inbox")); |
| 16 | assert.ok(wants("all", "inbox")); | |
| 17 | for (const kind of ["dm", "mention", "inbox"] as const) assert.ok(!wants("none", kind)); | |
| 18 | }); | |
| 19 | ||
| 20 | test("a workspace's own level wins over the general one", () => { | |
| 21 | const prefs = mergePreferences(dms, { workspaces: { Acme: "none", side: "all" } }); | |
| 22 | assert.equal(levelFor(prefs, "acme"), "none"); | |
| 23 | assert.equal(levelFor(prefs, "side"), "all"); | |
| 24 | assert.equal(levelFor(prefs, "other"), "dms_mentions"); | |
| 25 | }); | |
| 26 | ||
| 27 | test("changes are checked: unknown levels ignored, null clears, the same as the general level is no override", () => { | |
| 28 | let prefs = mergePreferences(dms, { level: "loud", workspaces: { a: "all", b: "bogus" } }); | |
| 29 | assert.deepEqual(prefs, { level: "dms_mentions", workspaces: { a: "all" } }); | |
| 30 | prefs = mergePreferences(prefs, { workspaces: { a: null } }); | |
| 31 | assert.deepEqual(prefs.workspaces, {}); | |
| 32 | prefs = mergePreferences(mergePreferences(dms, { workspaces: { a: "all" } }), { level: "all" }); | |
| 33 | assert.deepEqual(prefs, { level: "all", workspaces: {} }); | |
| 34 | assert.deepEqual(readPreferences("not json"), dms); | |
| 35 | assert.deepEqual(mergePreferences(dms, "nonsense"), dms); | |
| 36 | }); | |
| 37 | ||
| 38 | test("a tab counts as in front of you only while focused and recently heard from", () => { | |
| 39 | assert.ok(anyFocused([{ focused: true, seen_at: NOW - 1000 }], NOW)); | |
| 40 | assert.ok(!anyFocused([{ focused: true, seen_at: NOW - STALE_MS - 1 }], NOW)); | |
| 41 | assert.ok(!anyFocused([{ focused: false, seen_at: NOW }], NOW)); | |
| 42 | assert.ok(!anyFocused([], NOW)); | |
| 43 | }); | |
| 44 | ||
| 45 | test("push only when no tab is focused, there is a browser to push to, and the level wants it", () => { | |
| 46 | const dm = { kind: "dm" as const, workspace: "acme" }; | |
| 47 | const away = [{ focused: false, seen_at: NOW }]; | |
| 48 | const here = [{ focused: true, seen_at: NOW }]; | |
| 49 | assert.deepEqual(decide({ prefs: dms, notification: dm, tabs: away, subscriptions: 1, now: NOW }), { toast: true, push: true }); | |
| 50 | assert.deepEqual(decide({ prefs: dms, notification: dm, tabs: here, subscriptions: 1, now: NOW }), { toast: true, push: false }); | |
| 51 | assert.deepEqual(decide({ prefs: dms, notification: dm, tabs: [], subscriptions: 0, now: NOW }), { toast: true, push: false }); | |
| 52 | // No tab open at all: pushed. | |
| 53 | assert.deepEqual(decide({ prefs: dms, notification: dm, tabs: [], subscriptions: 2, now: NOW }), { toast: true, push: true }); | |
| 54 | // A focused tab gone quiet (the laptop slept): pushed. | |
| 55 | assert.equal(decide({ prefs: dms, notification: dm, tabs: [{ focused: true, seen_at: NOW - STALE_MS * 2 }], subscriptions: 1, now: NOW }).push, true); | |
| 56 | const inbox = { kind: "inbox" as const, workspace: "acme" }; | |
| 57 | assert.deepEqual(decide({ prefs: dms, notification: inbox, tabs: away, subscriptions: 1, now: NOW }), { toast: false, push: false }); | |
| 58 | const muted = mergePreferences(dms, { workspaces: { acme: "none" } }); | |
| 59 | assert.deepEqual(decide({ prefs: muted, notification: dm, tabs: away, subscriptions: 1, now: NOW }), { toast: false, push: false }); | |
| 60 | // A test goes through whatever the preferences and focus. | |
| 61 | assert.deepEqual(decide({ prefs: muted, notification: dm, tabs: here, subscriptions: 1, now: NOW, test: true }), { toast: true, push: true }); | |
| 62 | }); | |
| 63 | ||
| 64 | test("notifications are cleaned: kinds checked, links kept on the site, text trimmed", () => { | |
| 65 | assert.equal(cleanNotification({ id: "x", kind: "shout", title: "t" }), null); | |
| Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents. | 66 | // Workspace agents are gone: what they once sent is no notification now. |
| 67 | assert.equal(cleanNotification({ id: "x", kind: "agent_waiting", title: "t" }), null); | |
| 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) | 68 | assert.equal(cleanNotification({ kind: "dm", title: "t" }), null); |
| 69 | const n = cleanNotification({ | |
| 70 | id: "n1", | |
| 71 | kind: "mention", | |
| 72 | workspace: "Acme", | |
| 73 | title: " Ana in #design ", | |
| 74 | body: "x".repeat(1000), | |
| 75 | href: "//evil.example/", | |
| 76 | actor: { kind: "user", id: "usr_1", name: "Ana", avatar: "abc" }, | |
| 77 | channel_id: "chn_1", | |
| 78 | })!; | |
| 79 | assert.equal(n.workspace, "acme"); | |
| 80 | assert.equal(n.title, "Ana in #design"); | |
| 81 | assert.equal(n.body.length, 300); | |
| 82 | assert.equal(n.href, "/"); | |
| 83 | assert.equal(n.actor.avatar, "abc"); | |
| 84 | assert.equal(n.actor.avatar_seed, null); | |
| 85 | assert.equal(cleanNotification({ id: "n2", kind: "dm", title: "t", href: "/acme/-/chat/dm/chn_1", actor: { kind: "robot" } })!.actor.kind, "system"); | |
| 86 | }); | |
| 87 | ||
| 88 | test("a push is small and collapses by conversation", () => { | |
| 89 | const n: FeedNotification = { | |
| 90 | id: "msg_1", | |
| 91 | kind: "dm", | |
| 92 | workspace: "acme", | |
| 93 | title: "Ana", | |
| 94 | body: "y".repeat(400), | |
| 95 | href: "/acme/-/chat/dm/chn_1", | |
| 96 | actor: { kind: "user", id: "usr_1", name: "Ana" }, | |
| 97 | channel_id: "chn_1", | |
| 98 | created_at: "2026-10-08T00:00:00Z", | |
| 99 | }; | |
| 100 | const payload = pushPayload(n); | |
| 101 | assert.equal(payload.tag, "chat:chn_1"); | |
| 102 | assert.equal(payload.body.length, 240); | |
| 103 | assert.equal(payload.urgent, true); | |
| 104 | assert.equal(pushPayload({ ...n, kind: "inbox", channel_id: null }).tag, "inbox:msg_1"); | |
| 105 | }); | |
| 106 | ||
| 107 | test("counts add up, replace on set, never go below zero, and keep mute", () => { | |
| 108 | let c = applyCounts(null, { channel_id: "c", unread: 1, mentions: 0 }); | |
| 109 | c = applyCounts(c, { channel_id: "c", unread: 1, mentions: 1, muted: true }); | |
| 110 | assert.deepEqual(c, { channel_id: "c", unread: 2, mentions: 1, muted: true }); | |
| 111 | c = applyCounts(c, { channel_id: "c", unread: -5, mentions: -5 }); | |
| 112 | assert.deepEqual(c, { channel_id: "c", unread: 0, mentions: 0, muted: true }); | |
| 113 | c = applyCounts(c, { channel_id: "c", unread: 3, mentions: 0, set: true }); | |
| 114 | assert.deepEqual(c, { channel_id: "c", unread: 3, mentions: 0, muted: true }); | |
| 115 | }); | |
| 116 | ||
| 117 | test("totals leave muted unread out, as the rail does, but count their mentions", () => { | |
| 118 | const t = totals( | |
| 119 | "acme", | |
| 120 | [ | |
| 121 | { channel_id: "a", unread: 3, mentions: 1, muted: false }, | |
| 122 | { channel_id: "b", unread: 5, mentions: 2, muted: true }, | |
| 123 | { channel_id: "c", unread: 0, mentions: 0, muted: false }, | |
| 124 | ], | |
| 125 | 4, | |
| 126 | true, | |
| 127 | ); | |
| 128 | assert.deepEqual(t, { | |
| 129 | workspace: "acme", | |
| 130 | chat_unread: 3, | |
| 131 | chat_mentions: 3, | |
| 132 | inbox_unread: 4, | |
| 133 | per_channel: [ | |
| 134 | { channel_id: "a", unread: 3, mentions: 1 }, | |
| 135 | { channel_id: "b", unread: 5, mentions: 2 }, | |
| 136 | ], | |
| 137 | complete: true, | |
| 138 | }); | |
| 139 | }); | |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 140 | |
| 141 | const capped = { | |
| 142 | channel_id: "chn_1", | |
| 143 | message_id: "msg_card", | |
| 144 | actions: [ | |
| 145 | { id: "approve", label: "Approve more", style: "primary", input: { kind: "money", label: "New cap", initial: "4.00" } }, | |
| 146 | { id: "stop", label: "Stop", style: "danger", confirm: "Stop this session and everything under it?" }, | |
| Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents. | 147 | { id: "open", label: "Open", href: "/acme/web/pull/1" }, |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 148 | ], |
| 149 | }; | |
| 150 | ||
| 151 | test("a notification's card is checked: its place, its actions, links kept on the site", () => { | |
| 152 | const card = cleanCard(capped)!; | |
| 153 | assert.equal(card.message_id, "msg_card"); | |
| 154 | assert.deepEqual( | |
| 155 | card.actions.map((a) => [a.id, a.style, a.input?.kind ?? null, a.href ?? null]), | |
| 156 | [ | |
| 157 | ["approve", "primary", "money", null], | |
| 158 | ["stop", "danger", null, null], | |
| Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents. | 159 | ["open", "default", null, "/acme/web/pull/1"], |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 160 | ], |
| 161 | ); | |
| 162 | assert.equal(card.actions[1].confirm, "Stop this session and everything under it?"); | |
| 163 | assert.equal(cleanCard({ ...capped, actions: [{ id: "x", label: "X", href: "https://evil.example/" }] })!.actions[0].href, null); | |
| 164 | assert.equal(cleanCard({ ...capped, message_id: "" }), null); | |
| 165 | assert.equal(cleanCard({ ...capped, actions: [{ id: "", label: "" }] }), null); | |
| 166 | assert.equal(cleanCard("nope"), null); | |
| 167 | // Carried on the notification when it is one; left off otherwise. | |
| 168 | const n = cleanNotification({ id: "approval:ses_1:2000000", kind: "approval", workspace: "acme", title: "g1t needs more budget", card: capped })!; | |
| 169 | assert.equal(n.card?.actions.length, 3); | |
| 170 | assert.equal("card" in cleanNotification({ id: "n", kind: "dm", title: "t" })!, false); | |
| 171 | }); | |
| 172 | ||
| 173 | test("a push shows only the card's actions that need nothing typed or confirmed, two at most", () => { | |
| 174 | const card = cleanCard(capped)!; | |
| 175 | // Stop asks first in the app; a push can't, so it isn't offered there. | |
| Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents. | 176 | assert.deepEqual(pushActions(card), [{ id: "open", label: "Open", href: "/acme/web/pull/1" }]); |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 177 | assert.deepEqual(pushActions(null), []); |
| 178 | const payload = pushPayload({ | |
| 179 | id: "approval:ses_1:2000000", | |
| 180 | kind: "approval", | |
| 181 | workspace: "acme", | |
| 182 | title: "g1t needs more budget", | |
| 183 | body: "", | |
| Chat, artifacts and notifications no longer serve g1t's own agents. Direct messages and conversation invites take people only, and a space or folio is shared with people and teams. Chat stops waking agents, drops the RPCs agents posted, read, searched and handed off through, and stops answering buttons on agent cards; old cards keep their links. An agent's old messages still show, under its saved name or as "A deleted agent". Artifacts drops the folio and docs RPCs agents read and edited pages with, and an old agent member of a space can still be removed. Notifications drop "an agent is waiting on you". A deployment no longer trusts a pull request with secrets just because g1t's agent opened it, and the sign-up card no longer promises a team of agents. | 184 | href: "/acme/web/pull/1", |
| Write a thread up in Docs from chat; cards' buttons in notifications; a session's card stays at the top of its thread | 185 | actor: { kind: "agent", id: "a1", name: "g1t" }, |
| 186 | channel_id: "chn_1", | |
| 187 | card, | |
| 188 | created_at: "2026-10-08T00:00:00Z", | |
| 189 | }); | |
| 190 | assert.deepEqual(payload.card, { channel_id: "chn_1", message_id: "msg_card" }); | |
| 191 | assert.equal(payload.workspace, "acme"); | |
| 192 | // Its own notification: a later message in the conversation does not replace it. | |
| 193 | assert.equal(payload.tag, "approval:approval:ses_1:2000000"); | |
| 194 | }); | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 195 | |
| 196 | test("a person with only the phone app is pushed to: the feed counts browsers and phones together", () => { | |
| 197 | const dm = { kind: "dm" as const, workspace: "acme" }; | |
| 198 | const browsers = 0; | |
| 199 | const phones = 1; | |
| 200 | assert.deepEqual(decide({ prefs: dms, notification: dm, tabs: [], subscriptions: browsers + phones, now: NOW }), { toast: true, push: true }); | |
| 201 | // The same rules hold: not while a tab is in front of them, nor in Do Not Disturb. | |
| 202 | assert.equal(decide({ prefs: dms, notification: dm, tabs: [{ focused: true, seen_at: NOW }], subscriptions: browsers + phones, now: NOW }).push, false); | |
| 203 | assert.equal(decide({ prefs: dms, notification: dm, tabs: [], subscriptions: browsers + phones, now: NOW, dnd: true }).push, false); | |
| 204 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.