Repository navigation
Plannotator Inbox: browser notifications from the open page (step 4) - #1756
Merged
Merged
Conversation
The ask drawn in the page once an item arrives (Not now kept in config.json, so it survives a port change; a moved line after a port change), one Notification per thread tagged by the thread id, silent while the tab is in front, a click opening the thread, and the Settings block (on or off, per section). Proved in Chromium against the compiled binary.
…es one notice, title from the thread's section) Per-section switches removed from the page, the server, config.ts and the spec. Every notification shares one tag; with one thread notified since the tab was in front the banner names it and a click opens it, with more it reads "3 waiting in 2 projects" and a click opens the list. The title follows the thread's list section: stopped on you, else waiting on you. The e2e workflow also runs on packages/shared/config.ts.
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The story
The open Inbox page now raises a desktop notification when an agent stops on the person or asks a question, through the browser's own
NotificationAPI (owner Q8: the page first on every platform, the Mac helper later). The page asks once, in its own words, only after the first thread arrives; "Not now" is kept by the Inbox (config.json), so it survives the Inbox moving to another port. Every Inbox notification shares one tag. If one thread raised since the tab was last in front, the notice names that thread and a click opens it. If more did, the burst becomes one notice, "3 waiting in 2 projects", and a click opens the list. Nothing is raised while the Inbox tab is in front.What changed
packages/inbox/notify.ts(new):useInboxNotifications. It reads the event stream's lines. An item is a new question in an agent's message: a question line at revision 0 and state open, counted once per page. AStopped:line, aHolds up:line or any other question counts; news never does. After the list read those lines trigger, it raises oneNotification, always on the tagplannotator-inbox, so a new one replaces the last. While the tab is away, the page keeps the threads it notified about:window.focus()and opens#thread=.visibilityState === "visible"anddocument.hasFocus().Notification.requestPermission()on the click), Not now, and the Settings switch. A browser grant or block shows without a reload (navigator.permissionschange).packages/inbox/components/NotificationAsk.tsx(new): the one-time line (record 6.1), with its "moved" wording.packages/inbox/components/Settings.tsx: the Notifications block (record 7.2): the drawn row with its one switch.packages/inbox/App.tsx,components/InboxList.tsx(anaskslot above the notice),icons.tsx(the record's bell),inbox.css(ib-askline,ib-srow .ib-d).packages/inbox/api.ts: the event stream hands each parsed line toonRecord(InboxEvent).SettingsModel.notificationsandsaveNotificationsare added.packages/server/inbox.ts: the settings field and nothing else.GET /api/inbox/settingsreturnsnotifications: { enabled, dismissed, allowed_origin }.POSTacceptsnotifications(field by field, validated) besideinbox_tool, behind the same same-origin andserverSessionguards. The values are stored in config.json asinboxNotifications.packages/shared/config.tsadds only that typed key.tests/e2e/notifications.spec.ts(new),tests/e2e/playwright.config.ts(runs both specs),.github/workflows/inbox-e2e.yml(addspackages/shared/config.tsto its paths, and a comment),AGENTS.md(a "Notifications" paragraph in the Inbox section).Record sections this makes real
6.1-ask,6.1-moved.6.2-notification.json,6.2-burst.json,6.2-click-opened-threadand6.2-burst-opened-list. The macOS banner itself is the browser's and cannot be captured headless.6.3-focused-silent.7.2-settings-notifications.Proofs (all run against the compiled binary, under a temp
PLANNOTATOR_DATA_DIRand HOME)Agents write through
plannotator inbox mcpwith the MCP SDK's stdio client (scripts/inbox-sim.ts). The permission is Chromium's own, granted per origin throughcontext.grantPermissions(['notifications'], { origin }).new Notificationis the real constructor; an init script subclasses it only to record the calls.bun run test:e2e:inbox. Build first:bun run --cwd apps/review build && bun run build:hook && bun build apps/hook/server/index.ts --compile --no-compile-autoload-bunfig --define '__CLI_VERSION__="0.0.0-dev"' --outfile .local/plannotator. All 20 pass (the window's 10, this PR's 10):config.jsonhasdismissed: true.enabledandallowedOrigin. After a second port change, the new origin's permission readsdefaultand the "moved" line appears. Turn on calls the browser'srequestPermissiononce for that origin, andallowedOriginfollows.Stopped:line and aHolds up:line) gives exactly one call:{ title: "billing-svc: Pi stopped on you", body: "Which way should the worker go on a Stripe 409?", tag: "plannotator-inbox" }.#thread=<id>with the new question in the pane.Also
packages/server/inbox.test.ts: "notifications save into config.json …" (realfetch). It checks the defaults, refuses bad fields (422, nothing written;sectionsis no longer a setting), and saves field by field.PNGs, light and dark at 1440 by 900, are in
.local/proof/notifications/.Gates run
bun run typecheck: green.bun run check:release-version: green.packages/server/inbox.test.ts,packages/inbox,bun test testsand the full e2e (20 of 20) re-run green.bun test packages apps/hook tests scriptsunder a temp data dir: 5781 pass, 1 fail. The failure isapps/hook/server/annotate-cli.test.ts, "exits 1 with one line naming the port…". It fails the same way on a cleanorigin/maincheckout on this machine (checked), so it is unrelated to this PR.tests/entry-assets.test.tsandtests/favicon-surfaces.test.tsare in that run and pass.packages/uiandpackages/coreare untouched, so no ui smoke was needed.Leaves out
Deviations from the record, with the reason
The independent review (
research/reviews/REVIEW-PLANNOTATOR-INBOX-notifications-2026-10-07.md) had three blockers, all fixed in575205a61:The title now follows the thread's section, and "is holding up work on you" is gone.
The Settings states the record does not draw. 7.2 draws only "Allowed in this browser · for questions and stops" with the switch on. The row's label reports the browser's own answer for this origin, which the person cannot see elsewhere and which decides whether anything can show:
NotificationAPI; the switch is disabled.Each label keeps the drawn row's shape and "for questions and stops".
When the ask shows. Record 6.1 says "the first time something waits on him". The brief says "after the first thread arrives, never on first paint". The ask is raised when a new waiting item arrives through the event stream and the browser has not decided. A reload with waiting rows already present does not show it until the next item arrives.
Title wording. Record 6.2 draws only the stopped title ("billing-svc: Claude Code stopped on you"). A thread in any other waiting section, Holding up work included, reads ": is waiting on you". The burst title is the record's "3 waiting in 2 projects". With one project it reads "2 waiting in billing-svc", as the list's own "1 new in docs-site" does. The burst's body names the projects (not drawn).
The server's
noticestring ("The Inbox moved to a new port…", from the window PR) is still not drawn. The page draws the record's own "moved" line, keyed on the origin where the person turned notifications on.Test seams, named. The spec runs Chromium's new headless mode (
channel: 'chromium'), because the headless shell answers every notification permission with "denied". An init script handles three things a test cannot do with the real browser:visibilityState,hasFocus, and thefocusandvisibilitychangeevents a browser fires), because Playwright reports every page as focused.requestPermissionstill runs and is recorded.Notificationto record its calls.Stacking
This was built on
feat/inbox-windowand rebased onto main after PR 1755 merged (4ef1659f0). It has no other lane dependency. It touchespackages/server/inbox.tsonly in the settings route and two helpers next tosaveInboxTool.