macOS menu-bar app + pinned usage side-panel

Status:draft
Date:2026-06-27
Branch:feat/mac-menubar
Type:spec

Goal

On macOS, turn jacked's existing system-tray into a real menu-bar app: a live, glanceable pill in the menu bar (worst account's 5-hour · weekly %, color-coded), a click-down dropdown showing every account's two windows, and an optional side panel pinned to the right edge of the screen that stays visible on every Space — so the user never has to hunt for a browser tab to see usage. The dropdown and panel render the same usage bars as the dashboard (colored fill = quota used, white time-marker = position in the window), so a user can see at a glance when an account is burning faster than the clock.

Acceptance Criteria

Each is a done-condition Phase 6 verifies against the running app.

Non-Goals / Out of Scope

The scope-creep firewall. This feature explicitly does NOT:

Not doingWhy / where it lives instead
Reserve screen-edge space (Dock-style strut)No public macOS API; faking it needs Accessibility window-management. Panel floats on top only. Documented limitation.
OpenAI / Codex account supportSeparate effort with its own spec. The panel is provider-agnostic and will render Codex accounts when that lands, but we do not add OpenAI OAuth/usage here. The mockup's Codex row is illustrative only.
A native Swift app / .app bundle / notarizationShips inside the existing uv tool install. A signed downloadable app is a possible future, not this.
Windows/Linux menu-bar enhancementsmacOS-only by request; pystray path is frozen, only regression-guarded.
A WidgetKit desktop / Notification-Center widgetThrottled to 15–60 min refresh; wrong tool for live usage. Researched and rejected.
Re-implementing the usage bars nativelyWould fork the bar component into a second renderer that drifts. We reuse the web component.
New usage data pipelineReuses existing endpoints/data. No new polling of Anthropic.

Key Decision: reuse the web bars, native shell around them

The rich bars (5h/7d fill, white time-marker, multi-org grouping) render in a WKWebView that loads a new compact page reusing the dashboard's renderUsageBar()not redrawn in native AppKit. The window/menu-bar behavior (status-item live text, always-on-top, all-Spaces, edge-pinning, non-activating) is done natively in PyObjC.

This was chosen on merit, not ease: native redraw would feel marginally crisper and could use true NSVisualEffectView vibrancy, but it forks the bar/marker/threshold/grouping logic into a second renderer that drifts on every dashboard change (a tech-debt liability), and the truly-native version is a separate Swift app requiring Developer-ID signing + notarization — fragmenting the clean pip/uv install. Reuse keeps one source of truth and one distribution. The single real loss (OS vibrancy) is mitigated by a deliberate compact dark-glass CSS theme; the mockups prove the opaque-dark treatment looks premium. Logic that could drift (e.g. account grouping) is centralized in one shared util so the two surfaces share meaning.

Architecture

One service, platform-appropriate face. The existing ServiceRunner (uvicorn in a daemon thread) is unchanged. On macOS the service launcher starts a new rumps-based agent instead of the pystray Icon; on other platforms it starts pystray exactly as today. The rumps agent owns the status item and, via PyObjC (rumps is PyObjC, so this runs in the same Cocoa run loop), an NSPopover (dropdown) and an NSPanel (side panel), each hosting a WKWebView pointed at a new compact /panel page. A timer polls a lightweight summary endpoint to set the live menu-bar title.

flowchart TB
    subgraph svc["jacked service (one process)"]
      SR["ServiceRunner\nuvicorn @ 127.0.0.1:8321\n(unchanged)"]
      subgraph mac["macOS face (new)"]
        APP["MacMenuBarApp (rumps)\nNSStatusItem + timer"]
        POP["NSPopover + WKWebView\n(dropdown)"]
        PAN["NSPanel + WKWebView\n(pinned side panel)"]
      end
      WIN["pystray Icon\n(Windows/Linux, unchanged)"]
    end
    SR -->|serves| WEB["/panel  (compact page)\nreuses renderUsageBar() + .elapsed-marker\n+ groupAccountsByLogin()"]
    SR -->|serves| API["/api/analytics/usage-overview\n/api summary  (worst account)"]
    APP -->|poll summary| API
    APP -->|live title| TITLE["menu-bar pill\nJ ● 96%·78%"]
    POP -->|loads| WEB
    PAN -->|loads| WEB
    WEB -->|fetch + live update| API
  
flowchart TB
    SR[ServiceRunner uvicorn 8321] --> WEB[/panel compact page]
    SR --> API[/api summary + usage-overview]
    APP[MacMenuBarApp rumps] --> API
    APP --> TITLE[menu-bar pill]
    POP[NSPopover WKWebView] --> WEB
    PAN[NSPanel WKWebView] --> WEB
    WEB --> API
  

Data flow

Files & Interfaces

FileNew/changedResponsibility
jacked/service/menubar_mac.pynewMacMenuBarApp (rumps) + PyObjC NSPopover/NSPanel hosting WKWebViews; pill timer; menu actions; panel edge-pinning + display-change handling; clean shutdown of uvicorn.
jacked/service/tray.py / launcherchangedPlatform branch: on sys.platform == "darwin" start MacMenuBarApp; else existing pystray Icon. ServiceRunner reused as-is.
jacked/data/web/panel.htmlnewCompact page (dark-glass theme) hosted in popover + panel.
jacked/data/web/js/components/panel.jsnewRenders the compact view by reusing renderUsageBar(); calls the shared grouping util; live-updates.
jacked/data/web/js/components/usage.jsreusedSource of truth for renderUsageBar() + .elapsed-marker + elapsed-fraction math. Unchanged (or minimally exposed for reuse).
jacked/data/web/js/util/account-grouping.jsnewgroupAccountsByLogin() — single definition of same-email/different-org grouping, shared by panel (and available to the dashboard).
jacked/api/routes/…changedGET /panel route serving the compact page; a lightweight summary endpoint (or reuse /api/analytics/usage-overview) returning the worst-account figure for the pill.
jacked/api/main.pychangedMount/serve the new /panel static page alongside existing assets.
pyproject.tomlchangedAdd rumps + required pyobjc-framework-* (Cocoa, WebKit) as sys_platform == "darwin" deps.
jacked/cli.pychangedWire the macOS agent into the existing service launch path; optional jacked menubar entry for manual start.
tests/unit/service/…newUnit tests: worst-account summary, grouping util behavior (incl. shared-email case), platform-branch selection, /panel route 200 + content.

Testing & Verification

Open Questions


jacked spec · feeds superpowers:writing-plans/dc → subagent execution → /qa/pr.