Skip to content

feat: menu-bar item as live activity monitor - #13

Open
deehw wants to merge 2 commits into
mainfrom
feat/menu-bar-activity-monitor
Open

feat: menu-bar item as live activity monitor#13
deehw wants to merge 2 commits into
mainfrom
feat/menu-bar-activity-monitor

Conversation

@deehw

@deehw deehw commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Reworks the macOS menu-bar item from a lone, low-contrast dominant-status dot into a live activity monitor that mirrors the dashboard — it surfaces the single most important thing across all live sessions.

Behavior (priority: needs-you → working → idle → empty)

State Menu-bar shows
Empty a legible hollow ring (the old circle.dotted was nearly invisible over a wallpaper)
Idle ● 5 — dot + fleet count
Working (1) ● Bash · xcodebuild…
Working (N, or with idle) ● 5 Agent · audit retry paths — fleet count + freshest session's live tool
Waiting 🔔 approve Bash · rm -rf build — orange pill
Waiting + others 🔔 3 approve Bash · … — fleet count stays visible
Needs you (N) ◉ 2 need you — red pill
Error ◉ Bash failed — red pill

What's here

  • AggregateActivity — pure, Equatable, unit-tested view-model (make(from:now:)) that composes the line. 18 tests.
  • AggregateMenuBarLabel — thin renderer: icon + fleet-count badge + truncated activity text + urgency pill. No animations (menu bar stays calm).
  • Fixes the empty-until-opened bugboot() only ran from the dropdown's .task, so the provider never started (menu bar showed empty) until the popover was first opened. Now boots from the always-present label task.

Review-driven fixes (high-effort code review)

  • Fleet count surfaced (badge when >1, and always in the a11y label) so background sessions aren't hidden.
  • Lead session chosen by freshest active-tool startedAt (a coreEqual field), not updatedAt (which uiEqual ignores) — the invariant CLAUDE.md warns about.
  • Idle icon uses the dominant status glyph, so an alive paused/stopped/unknown session keeps its own icon.
  • Width cap applied to the text only; redundant boot() removed; minute-suffix format shared via ElapsedFormatter.minuteSuffix.
  • Elapsed advancement: the reviewer suggested a timer; a TimelineView in a MenuBarExtra label hangs the app at launch (caught by the test host — "test runner hung before establishing connection"), and the codebase intentionally keeps timers off the menu bar. So elapsed stays minute-granularity, advancing on store republish (frequent during active work); trade-off documented inline.

Verification

scripts/verify.sh green: build + full suite (232 tests, 0 failures) + perf gate (0.24s < 0.45s) + snapshot render. Every state eyeballed in light + dark via headless renders (synthetic data — no personal info).

🤖 Generated with Claude Code

deehw and others added 2 commits July 3, 2026 14:39
The aggregate menu-bar item was a lone dominant-status dot + count —
low-contrast, no identity, and collapsing every session to one dot.
Rework it into a live activity monitor that surfaces the single most
important thing across all sessions, mirroring the dashboard's priority:
needs-you → working → idle → empty.

- AggregateActivity: pure, Equatable view-model (make(from:now:)) that
  composes the one line to show — approval target when waiting, failed
  tool on error, the freshest working session's live tool + preview +
  minute-elapsed, or a calm idle count. All formatting is here, unit
  tested with a pinned now.
- AggregateMenuBarLabel: thin renderer — icon + count badge + truncated
  activity text, urgency tint/pill for needs-you, and a legible empty
  glyph (the old circle.dotted was nearly invisible over a wallpaper).
  Width-capped so a long preview can't push the clock off-screen.
- Fix empty-until-opened bug: boot() ran only from the dropdown
  content's .task, so the provider never started (and the menu bar
  showed empty) until the user first opened the popover. Boot from the
  always-present label task instead; boot() is idempotent.

Fields the label reads (activeTools, recentTools, status, aiTitle) are
already in EnrichedSession.coreEqual, so the store republishes on tool
changes and the line stays current without a timer. No animations
(StaticStatusIcon) — the menu bar stays calm.

Verified: 229 tests green, perf gate OK, headless renders of every
state in light + dark under /tmp/menubar-snapshots/agg-all-*.png.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fleet count: surface the total live-session count as a badge (when > 1)
  in idle/working/needs-you-single, and always in the accessibility
  label, so background sessions aren't hidden when one session is the
  headline. Neutral color when calm, tinted only when urgent, so a
  working "5" doesn't misread as "5 busy".
- Lead selection: pick the freshest working session by its most-recent
  active-tool startedAt (a coreEqual field) instead of updatedAt, which
  SessionStore.uiEqual deliberately ignores — keying on it could show a
  stale lead. This is the exact coreEqual/uiEqual invariant CLAUDE.md
  warns about.
- Idle icon: use the dominant status glyph, not a hardcoded green dot, so
  an alive paused/stopped/unknown session keeps its own icon instead of
  masquerading as healthy-idle (preserves the .unknown forward-compat
  signal).
- Accessibility: announce the session count in every non-empty state.
- Width cap: apply the 220pt cap to the growable text only, not the whole
  label, so empty/idle can't reserve a wide item.
- Remove the redundant boot() call from DashboardView's .task (the
  always-present label task already boots).
- Share the minute-suffix format via ElapsedFormatter.minuteSuffix so the
  menu-bar line and per-session row can't drift.

Elapsed advancement (review finding): a TimelineView in the MenuBarExtra
*label* hangs the app at launch (caught by the test host: "test runner
hung before establishing connection"), and the codebase intentionally
keeps timers off the menu bar. So the elapsed stays minute-granularity,
advancing on store republish (frequent during active work); documented
the trade-off inline.

232 tests green, perf gate OK, headless renders refreshed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant