Skip to content

v4.0.0 - Beacon

Choose a tag to compare

@github-actions github-actions released this 02 May 23:58

[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 today between 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 in localStorage[wf-dashboard-filter]. Predicates shared via gui/src/renderer/src/lib/dashboard-filters.ts.
  • Elapsed-time badge on running ProjectCards — ticking Ns / Nm / Nh Mm next to the agent badge, sourced from a new AgentStatus.started_at proto field stamped in RunningAgent.StartedAt. Flips to var(--wf-warning) past 30 minutes.
  • Last-activity timestamp on dashboard cardsActive now / 5m ago / 4h ago / 2mo ago segment derived from the most recent task updated_at. Hand-rolled relative-time formatter in gui/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.ts ref-counts the underlying AgentService.SubscribeScreen stream.
  • Needs-attention treatment for failed tasks — red-tinted card border + header AlertTriangle chip + N failed segment in the counts row + red progress segment when any task has status === 'done' && success === false.
  • Current-task surfacing on running ProjectCards — replaces the misleading Next: line with Working: <current task title> (with Flame icon) when the agent is actively running. No proto change — uses the existing AgentStatus.task_title.
  • Shell-count chip on running ProjectCards — terminal icon + count from useTerminalStore filtered 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 toggleLayoutGrid / Rows3 toggle in the header; list mode renders one ~46px row per project. Selection persists in localStorage[wf-dashboard-layout]. Per-project rendering in gui/src/renderer/src/views/Dashboard/ProjectRow.tsx.
  • Notification bus — new internal/daemon/notify package with a typed Bus, channel fan-out (slow-consumer drop), stable MakeID (sha256(kind|project_id|task_number|emitted_at_unix)[:8]), and JSONL append to ~/.watchfire/logs/<project_id>/notifications.log for headless fallback.
  • TASK_FAILED OS notification — fires from internal/daemon/server/task_failed.go::emitTaskFailed on done && !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. Body N tasks done · M failed over the run window.
  • Bundled notification soundsassets/sounds/task-{done,failed}.wav (mono 22050 Hz, ~25 KB each). Pure shouldPlaySound(kind, prefs) decision in gui/src/renderer/src/stores/notifications-sound.ts. OS toast goes silent precisely when the renderer plays its own audio.
  • Dynamic system tray menuinternal/daemon/tray/tray.go rebuilds on every project / task / agent / settings change; sections for Needs attention / Working / Idle plus Notifications (N today) ▸ submenu reading the JSONL fallback. Click-through routes via the new DaemonService.SubscribeFocusEvents stream.
  • 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 under defaults.notifications in ~/.watchfire/settings.yaml. Gating helper models.ShouldNotify.
  • Inline diff viewer — new internal/daemon/diff package resolves diffs pre-merge (<merge-base>...HEAD on watchfire/<n>) and post-merge (locates the merge commit via git log --grep). Structured FileDiffSet; cap at 10000 lines; cache at ~/.watchfire/diff-cache/<project_id>/<task_number>.json. GUI Inspect tab + TUI overlay (bound to d).
  • Per-task metrics capture<n>.metrics.yaml siblings carrying duration, exit reason, agent, tokens, cost. New internal/daemon/metrics package with parsers for Claude Code, Codex, opencode, Gemini, Copilot (stub). Capture from a non-blocking goroutine on handleTaskChanged. New watchfire metrics backfill CLI.
  • Per-project Insights viewinternal/daemon/insights/project.go aggregates one project's tasks per window. New GUI Insights tab + TUI overlay (bound to i) 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 rollupinternal/daemon/insights/global.go aggregates the whole fleet per window; cached at ~/.watchfire/insights-cache/_global.json. Dashboard rollup card under the Beacon status bar; TUI fleet overlay bound to Ctrl+f.
  • Report export (CSV + Markdown) — shared InsightsService.ExportReport RPC with oneof scope (project_id / global / single_task). Markdown templates in internal/daemon/insights/templates/; CSV uses # section: <name> headers. Single <ExportPill> component on the dashboard + ProjectView headers; TUI binds Ctrl+e.
  • Weekly digest notificationdigestRunner schedules with a re-armable time.Timer from models.DigestSchedule.NextFire (DST-stable, with 24-hour catch-up on daemon start). Markdown rendered to ~/.watchfire/digests/<YYYY-MM-DD>.md regardless of toast suppression. New WEEKLY_DIGEST notification kind + FOCUS_TARGET_DIGEST.
  • Outbound delivery framework + webhook adapter — new internal/daemon/relay package with an Adapter interface and a Dispatcher subscribing to notify.Bus. Per-adapter retry ([500ms, 2s, 8s]) + circuit breaker (3 failures / 5-minute window). Generic WebhookAdapter POSTs the canonical payload with X-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.go renders three text/template Block Kit envelopes (TASK_FAILED / RUN_COMPLETE / WEEKLY_DIGEST) with header / section / context / actions blocks. Project-color → :large_<color>_square: shortcode map in slack_color.go.
  • Discord adapter (rich embeds)internal/daemon/relay/discord.go renders three embed envelopes with project-color tinting. Shared hexToInt / rfc3339 template helpers. Defensive 4000-rune description trim with single-WARN log on overflow. New watchfire integrations CLI parent with list and test subcommands.
  • GitHub auto-PR creation — opt-in per project via github.auto_pr.enabled: true. End-of-task lifecycle in internal/daemon/git/pr.go::OpenPR: gh auth status → parse <owner>/<repo>git push --force-with-lease → render PR body via pr_body.md.tmplgh 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 IntegrationsService gRPC service with List / Save / Delete / Test RPCs; Save carries a oneof payload, secrets are write-only on the wire. GUI IntegrationsSection.tsx with per-type detail panels; TUI overlay reachable via Ctrl+I.
  • Inbound HTTP server frameworkinternal/daemon/echo/server.go binds ListenAddr (default 127.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 verificationinternal/daemon/echo/verify.go ships VerifyGitHub (HMAC-SHA256 against sha256=<hex>), VerifySlack (HMAC-SHA256 over v0:<timestamp>:<body> with 5-minute drift), VerifyDiscord (Ed25519 over timestamp || body, same drift) — all constant-time.
  • Idempotency cacheinternal/daemon/echo/idempotency.go is an LRU+TTL cache (1000 entries / 24h, container/list-backed, sync.Mutex-protected); Seen(key) refreshes TTL on hit.
  • Per-task lifecycle helpers + command routerinternal/daemon/echo/commands.go::Route(ctx, cmd, subcmd, rest, CommandContext) CommandResponse powers slash-command transports. Three commands (status / retry <task> / cancel <task>); CommandResponse{text, blocks, ephemeral, in_channel} is transport-agnostic.
  • Discord interactions endpointinternal/daemon/echo/handler_discord.go exposes POST /echo/discord/interactions with end-to-end Ed25519 verification + replay window + idempotency. PING → PONG; APPLICATION_COMMAND → dispatch to commands.Route, render via discord_render.go::RenderInteraction. Slash-command registration via watchfire integrations register-discord <guild_id> (idempotent).
  • Inbound settings UI (GUI + TUI)gui/src/renderer/src/views/Settings/InboundSection.tsx shows a Listening pill polled at 5 s, editable ListenAddr + PublicURL with 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 position order with bucketing into needs-attention → working → has-ready-tasks → idle (input-array index as final tiebreaker for stability). Predicate helpers in gui/src/renderer/src/lib/dashboard-filters.ts. A muted Sorted by activity label 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 projectId and survive navigation; Cmd+` toggles a non-destructive panelCollapsed flag. destroyProjectSessions(projectId) is called only from removeProject. BottomPanel.tsx always-mounts every TerminalTab with a visible flag 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.ts runs $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. New defaults.terminal_shell global 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_digest defaults 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 gh on PATH and gh auth status returning 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.go for pull_request.closed events 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.Route did not ship with Beacon — /watchfire status / retry / cancel works in Discord but not in Slack (filed as v5.0 follow-up).