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.
| Docs: the inbox, what lands in it and why, its tabs and actions | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import type { InboxItem } from "@g1t/contracts"; | |
| 5 | ||
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 6 | import { |
| 7 | EMAIL_REASONS, | |
| 8 | REASON_FILTERS, | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 9 | SECURITY_DIGEST_CHOICES, |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 10 | REASON_LABEL, |
| 11 | bellCount, | |
| 12 | emailReasonsFromForm, | |
| 13 | emptyFor, | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 14 | securityDigestFromForm, |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 15 | inboxReason, |
| 16 | inboxTab, | |
| 17 | inboxView, | |
| 18 | markFromForm, | |
| 19 | needsYou, | |
| 20 | severityOf, | |
| 21 | snoozeUntil, | |
| 22 | subscriptionFromForm, | |
| 23 | subscriptionLine, | |
| 24 | tabCount, | |
| 25 | updatesLabel, | |
| 26 | watchFromForm, | |
| 27 | watchLabel, | |
| 28 | whenShort, | |
| 29 | } from "./inbox.ts"; | |
| Docs: the inbox, what lands in it and why, its tabs and actions | 30 | |
| 31 | const NOW = Date.parse("2026-10-07T12:00:00.000Z"); | |
| 32 | ||
| 33 | test("tabs read from the address, and each shows one severity", () => { | |
| 34 | assert.equal(inboxTab("needs"), "needs"); | |
| 35 | assert.equal(inboxTab("nonsense"), "all"); | |
| 36 | assert.equal(inboxTab(null), "all"); | |
| 37 | assert.equal(severityOf("needs"), "warning"); | |
| 38 | assert.equal(severityOf("all"), null); | |
| 39 | assert.equal(inboxView("done"), "done"); | |
| 40 | assert.equal(inboxView("archive"), "inbox"); | |
| 41 | }); | |
| 42 | ||
| 43 | test("counts are what is unread under each tab", () => { | |
| 44 | const counts = { unread: 15, error: 3, warning: 0, success: 8, info: 4 }; | |
| 45 | assert.equal(tabCount(counts, "all"), 15); | |
| 46 | assert.equal(tabCount(counts, "error"), 3); | |
| 47 | assert.equal(tabCount(counts, "needs"), 0); | |
| 48 | assert.equal(tabCount(null, "all"), 0); | |
| 49 | }); | |
| 50 | ||
| 51 | test("the bell shows a number up to 99", () => { | |
| 52 | assert.equal(bellCount(0), ""); | |
| 53 | assert.equal(bellCount(null), ""); | |
| 54 | assert.equal(bellCount(7), "7"); | |
| 55 | assert.equal(bellCount(140), "99+"); | |
| 56 | }); | |
| 57 | ||
| 58 | test("times are said in a few characters", () => { | |
| 59 | assert.equal(whenShort("2026-10-07T11:59:30.000Z", NOW), "just now"); | |
| 60 | assert.equal(whenShort("2026-10-07T11:45:00.000Z", NOW), "15m ago"); | |
| 61 | assert.equal(whenShort("2026-10-07T09:00:00.000Z", NOW), "3h ago"); | |
| 62 | assert.equal(whenShort("2026-10-06T09:00:00.000Z", NOW), "Yesterday"); | |
| 63 | assert.equal(whenShort("2026-10-03T12:00:00.000Z", NOW), "4d ago"); | |
| 64 | assert.equal(whenShort("2026-09-20T12:00:00.000Z", NOW), "Sep 20"); | |
| 65 | }); | |
| 66 | ||
| 67 | test("a snooze is for later, by one of a few choices", () => { | |
| 68 | assert.equal(snoozeUntil("3h", NOW), "2026-10-07T15:00:00.000Z"); | |
| 69 | assert.equal(snoozeUntil("week", NOW), "2026-10-14T12:00:00.000Z"); | |
| 70 | assert.equal(snoozeUntil("forever", NOW), null); | |
| 71 | }); | |
| 72 | ||
| 73 | test("forms ask for one mark on one item, or read for every item", () => { | |
| 74 | const form = (fields: Record<string, string>) => { | |
| 75 | const data = new FormData(); | |
| 76 | for (const [name, value] of Object.entries(fields)) data.set(name, value); | |
| 77 | return data; | |
| 78 | }; | |
| 79 | assert.deepEqual(markFromForm(form({ intent: "done", id: "ntf_1" }), NOW), { mark: "done", ids: ["ntf_1"] }); | |
| 80 | assert.deepEqual(markFromForm(form({ intent: "snooze", id: "ntf_1", for: "tomorrow" }), NOW), { | |
| 81 | mark: "snooze", | |
| 82 | ids: ["ntf_1"], | |
| 83 | until: "2026-10-08T12:00:00.000Z", | |
| 84 | }); | |
| 85 | assert.deepEqual(markFromForm(form({ intent: "read_all", tab: "error" }), NOW), { mark: "read", all: true, severity: "error" }); | |
| 86 | assert.deepEqual(markFromForm(form({ intent: "read_all" }), NOW), { mark: "read", all: true, severity: null }); | |
| 87 | assert.equal(markFromForm(form({ intent: "snooze", id: "ntf_1", for: "never" }), NOW), null); | |
| 88 | assert.equal(markFromForm(form({ intent: "delete", id: "ntf_1" }), NOW), null); | |
| 89 | assert.equal(markFromForm(form({ intent: "done" }), NOW), null); | |
| 90 | }); | |
| 91 | ||
| 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. | 92 | test("what needs you puts what waits on you before a failure, and leaves out the rest", () => { |
| Docs: the inbox, what lands in it and why, its tabs and actions | 93 | const item = (id: string, severity: InboxItem["severity"], createdAt: string, readAt: string | null = null): InboxItem => ({ |
| 94 | id, | |
| 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. | 95 | reason: "review_requested", |
| Docs: the inbox, what lands in it and why, its tabs and actions | 96 | severity, |
| 97 | title: id, | |
| 98 | body: "", | |
| 99 | repo: "acme/rocket", | |
| 100 | workspace: "acme", | |
| 101 | subject: "pull", | |
| 102 | number: 1, | |
| 103 | url: "/acme/rocket/pull/1", | |
| 104 | actor: null, | |
| 105 | createdAt, | |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 106 | updatedAt: createdAt, |
| 107 | event: null, | |
| 108 | count: 1, | |
| Docs: the inbox, what lands in it and why, its tabs and actions | 109 | readAt, |
| 110 | doneAt: null, | |
| 111 | saved: false, | |
| 112 | snoozedUntil: null, | |
| 113 | }); | |
| 114 | const needs = needsYou( | |
| 115 | [ | |
| 116 | item("failed-new", "error", "2026-10-07T11:00:00.000Z"), | |
| 117 | item("waiting-old", "warning", "2026-10-06T11:00:00.000Z"), | |
| 118 | item("waiting-new", "warning", "2026-10-07T10:00:00.000Z"), | |
| 119 | item("merged", "success", "2026-10-07T11:30:00.000Z"), | |
| 120 | item("failed-read", "error", "2026-10-07T11:40:00.000Z", "2026-10-07T11:50:00.000Z"), | |
| 121 | ], | |
| 122 | 2, | |
| 123 | ); | |
| 124 | assert.deepEqual( | |
| 125 | needs.items.map((entry) => entry.id), | |
| 126 | ["waiting-new", "waiting-old"], | |
| 127 | ); | |
| 128 | assert.equal(needs.total, 3); | |
| 129 | }); | |
| 130 | ||
| 131 | test("every tab says something when it is empty", () => { | |
| 132 | assert.equal(emptyFor("all").title, "You're all caught up"); | |
| 133 | assert.equal(emptyFor("needs").title, "Nothing needs you"); | |
| 134 | assert.equal(emptyFor("all", "saved").title, "Nothing saved"); | |
| 135 | }); | |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 136 | |
| 137 | test("every reason has words, a filter and an email choice", () => { | |
| 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. | 138 | // Every reason has words, an old item's `agent` too; it is no longer sent, so it is neither a filter nor an email choice. |
| 139 | assert.equal(Object.keys(REASON_LABEL).length, 12); | |
| 140 | const reasons = Object.keys(REASON_LABEL).filter((reason) => reason !== "agent"); | |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 141 | assert.deepEqual(REASON_FILTERS.slice(1).map((entry) => entry.reason), reasons); |
| 142 | assert.equal(REASON_FILTERS[0].label, "Any reason"); | |
| 143 | assert.equal(REASON_FILTERS.find((entry) => entry.reason === "review_requested")?.label, "Review requested"); | |
| 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. | 144 | // Every reason someone can be told for can be emailed. |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 145 | assert.deepEqual( |
| 146 | EMAIL_REASONS.map((entry) => entry.reason).sort(), | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 147 | reasons.sort(), |
| Inbox on the site: reasons, a reason filter, Subscribe, Watch and settings | 148 | ); |
| 149 | assert.equal(inboxReason("mention"), "mention"); | |
| 150 | assert.equal(inboxReason("gossip"), null); | |
| 151 | assert.equal(inboxReason(null), null); | |
| 152 | }); | |
| 153 | ||
| 154 | test("a thread says how much happened on it once more than one thing has", () => { | |
| 155 | assert.equal(updatesLabel(1), ""); | |
| 156 | assert.equal(updatesLabel(null), ""); | |
| 157 | assert.equal(updatesLabel(4), "4 updates"); | |
| 158 | }); | |
| 159 | ||
| 160 | test("watching is read from the menu, and a custom watch of nothing is the default", () => { | |
| 161 | const form = (fields: [string, string][]) => { | |
| 162 | const data = new FormData(); | |
| 163 | for (const [name, value] of fields) data.append(name, value); | |
| 164 | return data; | |
| 165 | }; | |
| 166 | assert.deepEqual(watchFromForm(form([["level", "all"]])), { level: "all", events: [] }); | |
| 167 | assert.deepEqual(watchFromForm(form([["level", "custom"], ["event", "deployments"], ["event", "pulls"], ["event", "releases"]])), { | |
| 168 | level: "custom", | |
| 169 | events: ["pulls", "deployments"], | |
| 170 | }); | |
| 171 | assert.deepEqual(watchFromForm(form([["level", "custom"]])), { level: "participating", events: [] }); | |
| 172 | assert.equal(watchFromForm(form([["level", "loud"]])), null); | |
| 173 | assert.equal(watchLabel("all"), "Watching"); | |
| 174 | assert.equal(watchLabel("ignore"), "Ignoring"); | |
| 175 | assert.equal(watchLabel("participating"), "Watch"); | |
| 176 | }); | |
| 177 | ||
| 178 | test("the subscribe button says whether, and why", () => { | |
| 179 | const sub = (changes: object) => ({ subscribed: true, ignored: false, reason: null, repo: null, number: 7, updatedAt: null, ...changes }); | |
| 180 | assert.equal(subscriptionLine(sub({ reason: "assign" }), "issue"), "You're subscribed because you were assigned."); | |
| 181 | assert.equal(subscriptionLine(sub({ reason: "author" }), "pull"), "You're subscribed because you opened this pull request, or asked g1t for it."); | |
| 182 | assert.match(subscriptionLine(sub({ subscribed: false }), "issue"), /still hear if you're mentioned/); | |
| 183 | assert.match(subscriptionLine(sub({ subscribed: false, ignored: true }), "issue"), /ignore this issue/); | |
| 184 | assert.match(subscriptionLine(null, "issue"), /^Subscribe/); | |
| 185 | const intent = (value: string) => { | |
| 186 | const data = new FormData(); | |
| 187 | data.set("intent", value); | |
| 188 | return subscriptionFromForm(data); | |
| 189 | }; | |
| 190 | assert.deepEqual(intent("subscribe"), { subscribed: true, ignored: false }); | |
| 191 | assert.deepEqual(intent("unsubscribe"), { subscribed: false, ignored: false }); | |
| 192 | assert.deepEqual(intent("ignore"), { subscribed: false, ignored: true }); | |
| 193 | assert.deepEqual(intent("default"), { subscribed: null, ignored: false }); | |
| 194 | assert.equal(intent("explode"), null); | |
| 195 | }); | |
| 196 | ||
| 197 | test("email reasons are read from the settings form in rank order", () => { | |
| 198 | const data = new FormData(); | |
| 199 | for (const value of ["mention", "nope", "agent", "mention"]) data.append("email", value); | |
| 200 | assert.deepEqual(emailReasonsFromForm(data), ["agent", "mention"]); | |
| 201 | assert.deepEqual(emailReasonsFromForm(new FormData()), []); | |
| 202 | }); | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 203 | |
| 204 | test("security alerts that are not urgent reach people as they choose, daily until they do", () => { | |
| 205 | assert.deepEqual( | |
| 206 | SECURITY_DIGEST_CHOICES.map((choice) => choice.cadence), | |
| 207 | ["daily", "weekly", "at_once", "off"], | |
| 208 | ); | |
| 209 | const data = new FormData(); | |
| 210 | data.set("security_digest", "weekly"); | |
| 211 | assert.equal(securityDigestFromForm(data), "weekly"); | |
| 212 | data.set("security_digest", "hourly"); | |
| 213 | assert.equal(securityDigestFromForm(data), null); | |
| 214 | assert.equal(securityDigestFromForm(new FormData()), null); | |
| 215 | // Security alerts are among the reasons one can be emailed for. | |
| 216 | assert.ok(EMAIL_REASONS.some((entry) => entry.reason === "security_alert")); | |
| 217 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.