v4.0.0 - Beacon
[4.0.0] Beacon
Beacon is the consolidated dashboard / notifications / insights / integrations release — glanceable dashboard, proactive OS notifications, retrospective insights, outbound + inbound integrations.
Added
- Dashboard aggregate status bar — single muted status line
N working · N needs attention · N idle · N done todaybetween the dashboard header and the project grid; counts derived from existing zustand stores so it updates live with no new gRPC. - Dashboard filter chips — pill chips (
All,Working,Needs attention,Idle,Has ready tasks) with live counts; selection persists inlocalStorage[wf-dashboard-filter]. Predicates shared viagui/src/renderer/src/lib/dashboard-filters.ts. - Elapsed-time badge on running ProjectCards — ticking
Ns / Nm / Nh Mmnext to the agent badge, sourced from a newAgentStatus.started_atproto field stamped inRunningAgent.StartedAt. Flips tovar(--wf-warning)past 30 minutes. - Last-activity timestamp on dashboard cards —
Active now / 5m ago / 4h ago / 2mo agosegment derived from the most recent taskupdated_at. Hand-rolled relative-time formatter ingui/src/renderer/src/lib/relative-time.ts. - Live PTY last-line preview on dashboard cards — latest non-blank terminal line in monospace muted text, throttled to 4 Hz. Singleton subscription manager in
gui/src/renderer/src/stores/agent-preview-store.tsref-counts the underlyingAgentService.SubscribeScreenstream. - Needs-attention treatment for failed tasks — red-tinted card border + header
AlertTrianglechip +N failedsegment in the counts row + red progress segment when any task hasstatus === 'done' && success === false. - Current-task surfacing on running ProjectCards — replaces the misleading
Next:line withWorking: <current task title>(withFlameicon) when the agent is actively running. No proto change — uses the existingAgentStatus.task_title. - Shell-count chip on running ProjectCards — terminal icon + count from
useTerminalStorefiltered by alive sessions for the project; pulses when any session emitted output in the last 2s. Click expands the bottom panel. - Dashboard grid/list layout toggle —
LayoutGrid/Rows3toggle in the header; list mode renders one ~46px row per project. Selection persists inlocalStorage[wf-dashboard-layout]. Per-project rendering ingui/src/renderer/src/views/Dashboard/ProjectRow.tsx. - Notification bus — new
internal/daemon/notifypackage with a typedBus, channel fan-out (slow-consumer drop), stableMakeID(sha256(kind|project_id|task_number|emitted_at_unix)[:8]), and JSONL append to~/.watchfire/logs/<project_id>/notifications.logfor headless fallback. - TASK_FAILED OS notification — fires from
internal/daemon/server/task_failed.go::emitTaskFailedondone && !success. Title<project> — task #NNNN failed, body is the task title + optional failure reason. - RUN_COMPLETE OS notification — fires at the falling edge of every autonomous run (single-task, start-all, wildfire) bounded by a new
RunningAgent.RunStartedAt. BodyN tasks done · M failedover the run window. - Bundled notification sounds —
assets/sounds/task-{done,failed}.wav(mono 22050 Hz, ~25 KB each). PureshouldPlaySound(kind, prefs)decision ingui/src/renderer/src/stores/notifications-sound.ts. OS toast goes silent precisely when the renderer plays its own audio. - Dynamic system tray menu —
internal/daemon/tray/tray.gorebuilds on every project / task / agent / settings change; sections forNeeds attention/Working/IdleplusNotifications (N today) ▸submenu reading the JSONL fallback. Click-through routes via the newDaemonService.SubscribeFocusEventsstream. - Notification preferences UI — TUI (
internal/tui/globalsettings.go) and GUI (gui/src/renderer/src/views/Settings/NotificationsSection.tsx) expose master / per-event / sounds / volume / quiet-hours / per-project mute. Schema underdefaults.notificationsin~/.watchfire/settings.yaml. Gating helpermodels.ShouldNotify. - Inline diff viewer — new
internal/daemon/diffpackage resolves diffs pre-merge (<merge-base>...HEADonwatchfire/<n>) and post-merge (locates the merge commit viagit log --grep). StructuredFileDiffSet; cap at 10000 lines; cache at~/.watchfire/diff-cache/<project_id>/<task_number>.json. GUI Inspect tab + TUI overlay (bound tod). - Per-task metrics capture —
<n>.metrics.yamlsiblings carrying duration, exit reason, agent, tokens, cost. Newinternal/daemon/metricspackage with parsers for Claude Code, Codex, opencode, Gemini, Copilot (stub). Capture from a non-blocking goroutine onhandleTaskChanged. Newwatchfire metrics backfillCLI. - Per-project Insights view —
internal/daemon/insights/project.goaggregates one project's tasks per window. New GUI Insights tab + TUI overlay (bound toi) with KPI strip, stacked-bar tasks-per-day, agent donut, duration histogram.localStorage[wf-insights-window]persists the 7d / 30d / 90d / All selector. - Cross-project Insights rollup —
internal/daemon/insights/global.goaggregates the whole fleet per window; cached at~/.watchfire/insights-cache/_global.json. Dashboard rollup card under the Beacon status bar; TUI fleet overlay bound toCtrl+f. - Report export (CSV + Markdown) — shared
InsightsService.ExportReportRPC withoneofscope (project_id/global/single_task). Markdown templates ininternal/daemon/insights/templates/; CSV uses# section: <name>headers. Single<ExportPill>component on the dashboard + ProjectView headers; TUI bindsCtrl+e. - Weekly digest notification —
digestRunnerschedules with a re-armabletime.Timerfrommodels.DigestSchedule.NextFire(DST-stable, with 24-hour catch-up on daemon start). Markdown rendered to~/.watchfire/digests/<YYYY-MM-DD>.mdregardless of toast suppression. NewWEEKLY_DIGESTnotification kind +FOCUS_TARGET_DIGEST. - Outbound delivery framework + webhook adapter — new
internal/daemon/relaypackage with anAdapterinterface and aDispatchersubscribing tonotify.Bus. Per-adapter retry ([500ms, 2s, 8s]) + circuit breaker (3 failures / 5-minute window). GenericWebhookAdapterPOSTs the canonical payload withX-Watchfire-Signature: sha256=<hex>HMAC. Secrets via OS keyring (internal/config/keyring.go) with file-store fallback. - Slack adapter (Block Kit messages) —
internal/daemon/relay/slack.gorenders threetext/templateBlock Kit envelopes (TASK_FAILED / RUN_COMPLETE / WEEKLY_DIGEST) with header / section / context / actions blocks. Project-color →:large_<color>_square:shortcode map inslack_color.go. - Discord adapter (rich embeds) —
internal/daemon/relay/discord.gorenders three embed envelopes with project-color tinting. SharedhexToInt/rfc3339template helpers. Defensive 4000-rune description trim with single-WARN log on overflow. Newwatchfire integrationsCLI parent withlistandtestsubcommands. - GitHub auto-PR creation — opt-in per project via
github.auto_pr.enabled: true. End-of-task lifecycle ininternal/daemon/git/pr.go::OpenPR:gh auth status→ parse<owner>/<repo>→git push --force-with-lease→ render PR body viapr_body.md.tmpl→gh api -X POST /repos/:owner/:repo/pulls. Sentinel errors distinguish silent fallback (one WARN per project lifetime) from per-attempt failures. - Integrations settings UI (GUI + TUI) — new
IntegrationsServicegRPC service withList/Save/Delete/TestRPCs;Savecarries aoneofpayload, secrets are write-only on the wire. GUIIntegrationsSection.tsxwith per-type detail panels; TUI overlay reachable viaCtrl+I. - Inbound HTTP server framework —
internal/daemon/echo/server.gobindsListenAddr(default127.0.0.1:8765), 5 s graceful shutdown drain, 1 MiB body cap + panic recovery middleware, unauthenticated/echo/health.RegisterProvider(method, path, handler)for plug-in handlers. Bind failure logs ERROR but doesn't crash the daemon. - Signature verification —
internal/daemon/echo/verify.goshipsVerifyGitHub(HMAC-SHA256 againstsha256=<hex>),VerifySlack(HMAC-SHA256 overv0:<timestamp>:<body>with 5-minute drift),VerifyDiscord(Ed25519 overtimestamp || body, same drift) — all constant-time. - Idempotency cache —
internal/daemon/echo/idempotency.gois an LRU+TTL cache (1000 entries / 24h,container/list-backed,sync.Mutex-protected);Seen(key)refreshes TTL on hit. - Per-task lifecycle helpers + command router —
internal/daemon/echo/commands.go::Route(ctx, cmd, subcmd, rest, CommandContext) CommandResponsepowers slash-command transports. Three commands (status/retry <task>/cancel <task>);CommandResponse{text, blocks, ephemeral, in_channel}is transport-agnostic. - Discord interactions endpoint —
internal/daemon/echo/handler_discord.goexposesPOST /echo/discord/interactionswith end-to-end Ed25519 verification + replay window + idempotency. PING → PONG; APPLICATION_COMMAND → dispatch tocommands.Route, render viadiscord_render.go::RenderInteraction. Slash-command registration viawatchfire integrations register-discord <guild_id>(idempotent). - Inbound settings UI (GUI + TUI) —
gui/src/renderer/src/views/Settings/InboundSection.tsxshows a Listening pill polled at 5 s, editableListenAddr+PublicURLwith restart button, Copy-as-<provider>-URL buttons, four write-only secret inputs, per-provider last-delivery timestamps. TUI mirrors via a new "Inbound" tab inside the Integrations overlay.
Changed
- Dashboard auto-sorts projects by activity — replaces raw
positionorder with bucketing into needs-attention → working → has-ready-tasks → idle (input-array index as final tiebreaker for stability). Predicate helpers ingui/src/renderer/src/lib/dashboard-filters.ts. A mutedSorted by activitylabel appears when the activity order differs from the underlying position order.
Fixed
- GUI: switching projects silently killed every running shell in the bottom panel — PTY sessions now live in a global pool keyed by
projectIdand survive navigation; Cmd+` toggles a non-destructivepanelCollapsedflag.destroyProjectSessions(projectId)is called only fromremoveProject.BottomPanel.tsxalways-mounts everyTerminalTabwith avisibleflag so xterm.js scrollback survives React reconciliation. - In-app terminal couldn't find pnpm / volta / fnm-managed binaries (#32) — new shared helper
gui/src/main/login-shell.tsruns$SHELL -l -c env, parses PATH + dev-tool env vars, with a fallback PATH merge against the standard user-install locations. Caches per Electron process. Newdefaults.terminal_shellglobal setting picks the shell binary (X_OK validated). Fixes #32
Migration
- All Beacon features are additive — existing projects upgrade with no behaviour change.
- Notifications: master toggle defaults on,
weekly_digestdefaults off, quiet hours default off. - Outbound integrations: nothing fans out until you configure an integration under Settings → Integrations.
- GitHub auto-PR: opt-in per project. Requires
ghon PATH andgh auth statusreturning 0; missing prerequisites fall back to silent merge with one WARN per project lifetime. - Inbound integrations: empty
InboundConfig= no listener. Concrete handlers return 503 until the per-provider secret is configured.
Known issues
- The dedicated
handler_github.goforpull_request.closedevents did not ship with Beacon — auto-PR loop closes manually for now (filed as v5.0 follow-up). - The Slack HTTP transport on top of the shared
commands.Routedid not ship with Beacon —/watchfire status / retry / cancelworks in Discord but not in Slack (filed as v5.0 follow-up).