Add web push notifications for new inbox items
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
- `internal/git`: worktrees, the task changes view (see "Changes"), unmerged-change summaries and squash merges.
|
||||
- `internal/agents`: the agent session manager. It owns the running Claude processes and records their activity as events.
|
||||
- `internal/claude`: the Claude Code stream-json client (see `docs/claude-protocol.md`).
|
||||
- `internal/push`: web push notifications for new inbox items (see "Notifications").
|
||||
- `web/`: the SPA, frameworkless TypeScript compiled file-by-file to ES modules (no bundler). `web/src/*.ts` is the source; `web/src/api.gen.ts` holds types generated from `api/openapi.yaml`, and `web/src/api.ts` is the typed client over them. `go generate ./web` runs `npm ci`, regenerates the types and compiles `web/dist/`, which is committed so `go install` needs no Node. The binary embeds `index.html`, `style.css`, `dist/` and `src/` (for source maps). `byre serve --dev` serves `./web` from disk instead (run from the repo root); pair it with `npm --prefix web exec tsc -- --watch`.
|
||||
|
||||
## Data
|
||||
@@ -103,6 +104,18 @@ Agents ask questions with the `ask_human` MCP tool. Each question carries a `con
|
||||
| `snooze` | any | stays `open`, hidden until `snooze_until` |
|
||||
| `dismiss` | reports, errors | `dismissed` |
|
||||
|
||||
## Notifications
|
||||
|
||||
New inbox items are sent as web push notifications to every browser a user has turned them on in. Web push needs a secure origin and a service worker; byre gets the origin from the `tailscale serve` certificate, and serves the worker at `/sw.js` (compiled from `web/src/sw.ts` with its own `web/tsconfig.sw.json`, since it needs the WebWorker library instead of the DOM one) so its scope is the whole SPA.
|
||||
|
||||
- **Keys.** byre generates a VAPID key pair on its first start and keeps it in the `vapid_keys` table; `GET /api/push/key` returns the public half, which browsers pass to `PushManager.subscribe`. Replacing the keys invalidates every subscription.
|
||||
- **Subscriptions.** `POST /api/me/push-subscriptions` stores a browser's endpoint, `p256dh` and `auth` in `push_subscriptions` under the calling user (re-posting an endpoint replaces it), along with `https://` plus the request's `Host`, which becomes the VAPID `sub` claim. `DELETE /api/me/push-subscriptions?endpoint=` removes one. A push service answering 404 or 410 means the subscription expired, and byre deletes it.
|
||||
- **Preferences.** `users.notification_prefs` holds which item kinds notify the user, returned as `notification_prefs` on `GET /api/me` and set with `PUT /api/me/notifications`: `blocking_questions` (on by default), `questions` (non-blocking), `reports`, `errors` (`session_error`) and `rate_limits`.
|
||||
- **Sending.** `internal/push` watches the event stream for `inbox.created`. The first item starts a 3-second window (`pushWindow` in `internal/server/server.go`); everything that arrives inside it goes out together, so each subscribed user gets at most one notification per window, built from the items their preferences select. One item reads `byre · <task title>` with the question (or the report's first line, the error, or "Rate limited") as the body; several read `byre · N new inbox items` with one `<task title>: <text>` line each. Encryption and VAPID signing are `github.com/SherClockHolmes/webpush-go`. Notifications carry a 24-hour TTL and `Urgency: high` when any item is blocking. Delivery failures are logged, not retried.
|
||||
- **Clicking** a notification opens the first item's task (`/#/task/<id>`): the worker focuses an open byre window and tells it to switch task, or opens a new one.
|
||||
|
||||
The **Settings** tab turns notifications on or off for the current browser (asking for permission on the way) and edits the preferences. Opening it re-registers the browser's existing subscription, which restores one byre deleted as expired.
|
||||
|
||||
In the UI a question offers its options plus Other, whose text box is the only text field; a report has only an OK button, which dismisses it at once; an error offers Dismiss, Reply (`other`) and Snooze.
|
||||
|
||||
Answers reach the agent as one message, listing each answer under the question or error it responds to, once the session is idle and none of its blocking questions are still waiting for the human. An `other` answer to a question tells the agent that if it asks something back, it should answer with `ask_human` using the item's `thread_id`. Every item change is an `inbox.created` or `inbox.updated` event carrying the whole item, including who resolved it.
|
||||
|
||||
+1
-2
@@ -477,8 +477,7 @@ Record transcripts from the spike as the seed of the contract test suite.
|
||||
- **M0 — Skeleton.** launchd service, tailscale serve auth, SQLite schema, OpenAPI spec, projects, tasks, worktree create/delete, event log, SSE stream with resume, embedded SPA shell, basic server observability.
|
||||
- **Status: done**, and running as a service on the Mac Studio. Observability covers queue state and vendor rate-limit status, through `GET /api/admin/status` and the SPA's Admin view; process liveness and stuck-process detection, adapter parse errors, and per-task snapshot counts and disk usage are not done.
|
||||
- **M1 — One agent end to end (Claude).** Sessions, streaming, messaging, the interface coverage for Claude, the `ask_human` MCP tool, turn summaries, inbox with all views and response types, web push notifications, restart-and-resume, concurrency caps.
|
||||
- **Done:** sessions, streaming, messaging, interrupt; `ask_human` with validation; each turn's final message filed to the inbox as its summary; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering; the session instructions from "Session configuration", given as an appended system prompt and a per-turn reminder; per-turn commits by byre; bouncing questions asked in plain prose.
|
||||
- **Not done:** web push notifications. Compaction is done, as a handoff into a fresh session; model switching is done, per message; image input is done, pasted or dropped into the composer and stored as blobs.
|
||||
- **Done:** sessions, streaming, messaging, interrupt; `ask_human` with validation; each turn's final message filed to the inbox as its summary; inbox with global, project, and task views and every response type, answered as one batch and delivered to the agent as one message; restart-and-resume; global and per-vendor caps with queueing; rate limits as their own inbox state; the web UI (two panes, task switcher, transcript, inbox) with Markdown rendering; the session instructions from "Session configuration", given as an appended system prompt and a per-turn reminder; per-turn commits by byre; bouncing questions asked in plain prose; web push notifications through a service worker, with per-user preferences (blocking questions by default, other kinds opt-in) set in a Settings tab, bursts collapsed into one notification, and a click opening the task. Compaction is done, as a handoff into a fresh session; model switching is done, per message; image input is done, pasted or dropped into the composer and stored as blobs.
|
||||
- **Later:** subagent views and Mermaid rendering.
|
||||
- **M2 — Seeing the work.** Temp-index diffs and stats, snapshots, timeline scrubbing, terminals, file browser, previews.
|
||||
- **Done:** temp-index diffs and stats, with a Changes view.
|
||||
|
||||
Reference in New Issue
Block a user