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.
Each is a done-condition Phase 6 verifies against the running app.
5h · 7d %, color-coded (green <71, amber 71–90, red >90),
refreshing on an interval — with no browser open..elapsed-marker
at the time-elapsed position, plus reset time and color coding.ServiceRunner/LaunchAgent-managed uvicorn on 127.0.0.1:8321, and shows a
clear degraded state if the server is down.pystray tray with no behavioral change.rumps and the required pyobjc frameworks are darwin-only
dependencies; a Windows/Linux install resolves and runs unaffected.uv run python -m pytest passes, including new unit tests for the worst-account summary, the
account-grouping util, the /panel route, and the platform branch selection.The scope-creep firewall. This feature explicitly does NOT:
| Not doing | Why / 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 support | Separate 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 / notarization | Ships inside the existing uv tool install. A signed downloadable app is a possible future, not this. |
| Windows/Linux menu-bar enhancements | macOS-only by request; pystray path is frozen, only regression-guarded. |
| A WidgetKit desktop / Notification-Center widget | Throttled to 15–60 min refresh; wrong tool for live usage. Researched and rejected. |
| Re-implementing the usage bars natively | Would fork the bar component into a second renderer that drifts. We reuse the web component. |
| New usage data pipeline | Reuses existing endpoints/data. No new polling of Anthropic. |
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.
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
NSStatusItem title + color./panel → fetches account + usage data →
renders bars via the shared renderUsageBar() and groupAccountsByLogin(), live-updating
like the dashboard..nonactivatingPanel, status-level,
collectionBehavior = canJoinAllSpaces | stationary | fullScreenAuxiliary, pinned to
NSScreen.visibleFrame right edge, re-pinned on didChangeScreenParametersNotification.| File | New/changed | Responsibility |
|---|---|---|
jacked/service/menubar_mac.py | new | MacMenuBarApp (rumps) + PyObjC NSPopover/NSPanel hosting WKWebViews; pill timer; menu actions; panel edge-pinning + display-change handling; clean shutdown of uvicorn. |
jacked/service/tray.py / launcher | changed | Platform branch: on sys.platform == "darwin" start MacMenuBarApp; else existing pystray Icon. ServiceRunner reused as-is. |
jacked/data/web/panel.html | new | Compact page (dark-glass theme) hosted in popover + panel. |
jacked/data/web/js/components/panel.js | new | Renders the compact view by reusing renderUsageBar(); calls the shared grouping util; live-updates. |
jacked/data/web/js/components/usage.js | reused | Source of truth for renderUsageBar() + .elapsed-marker + elapsed-fraction math. Unchanged (or minimally exposed for reuse). |
jacked/data/web/js/util/account-grouping.js | new | groupAccountsByLogin() — single definition of same-email/different-org grouping, shared by panel (and available to the dashboard). |
jacked/api/routes/… | changed | GET /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.py | changed | Mount/serve the new /panel static page alongside existing assets. |
pyproject.toml | changed | Add rumps + required pyobjc-framework-* (Cocoa, WebKit) as sys_platform == "darwin" deps. |
jacked/cli.py | changed | Wire the macOS agent into the existing service launch path; optional jacked menubar entry for manual start. |
tests/unit/service/… | new | Unit tests: worst-account summary, grouping util behavior (incl. shared-email case), platform-branch selection, /panel route 200 + content. |
uv run python -m pytest): worst-account summary computation;
groupAccountsByLogin() (single org, shared-email/multi-org, provider mix); platform-branch
selects rumps on darwin / pystray elsewhere (mocked sys.platform); /panel returns
200 and includes the bar component./qa on the /panel page (bars + markers +
grouping render; live update); manual native check of the menu-bar pill, dropdown, and pinned panel
(always-on-top, all-Spaces, non-activating, re-pin on display change, clean Quit). Capture screenshots /
output tied to each acceptance criterion./api/analytics/usage-overview for the pill, or add a tiny dedicated
/api/menubar-summary? (Lean: reuse if it already yields per-account 5h/7d cheaply; otherwise a
1-field endpoint to keep the timer light.) — resolved during planning by reading the endpoint.groupAccountsByLogin() now (consistency), or
only the panel this round? (Lean: ship the shared util, wire the panel; dashboard adoption optional/non-blocking.)
jacked spec · feeds superpowers:writing-plans → /dc → subagent execution → /qa → /pr.