# Tray Popup Dashboard Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship (recommended) or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Replace the overloaded tray menu with a tray-triggered graphical popup dashboard that shows provider-aware account cards with native progress bars, reload controls, default/repair actions, last-updated metadata, and configurable auto-close behavior.

**Architecture:** Add a dedicated transient popup window surface that renders provider-aware account cards and delegates refresh/default/repair actions back to `Indicator`. Keep health fetching and cache persistence in the existing tray process; popup widgets only consume normalized in-memory state updates and never fetch directly.

**Tech Stack:** Python 3, GTK 3 / AyatanaAppIndicator, pytest fake GTK harness.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1 | `account_card.py`, `popup_window.py`, `tests/test_indicator.py` | single task |
| 2 | Task 2 | `indicator.py`, `systray_codex_switcher.py`, `tests/test_indicator.py` | single task |
| 3 | Task 3 | `tests/test_indicator.py`, `tests/test_systray_codex_switcher.py` | single task |

## File Map

- `account_card.py` — GTK widget/controller for one provider-aware account card, including provider badge, progress bars, badges, timestamps, and per-account actions.
- `popup_window.py` — transient popup container, header/footer actions, auto-close behavior, card list, and widget update surface for the indicator.
- `indicator.py` — tray icon lifecycle, popup toggle, lazy refresh orchestration, provider normalization, action callbacks, and popup state syncing.
- `systray_codex_switcher.py` — startup wiring for the updated indicator surface.
- `tests/test_indicator.py` — fake GTK coverage for popup behavior, card updates, refresh orchestration, and auto-close persistence.
- `tests/test_systray_codex_switcher.py` — startup integration coverage if popup wiring changes the build path.

### Task 1: Popup Window And Account Card Surface

**Wave:** 1
**Blocks:** Task 2, Task 3
**Blocked by:** —

**Files:**
- Create: `account_card.py` — one account card with GTK progress bars, badge, timestamps, and action hooks.
- Create: `popup_window.py` — transient popup window with header/footer controls and account-card hosting.
- Modify: `tests/test_indicator.py` — fake GTK types/assertions for popup widgets and account-card updates.

**Contract (pin EXACTLY):**
- `class AccountCard`
  - `__init__(account: Account, gtk_module: Any, on_reload, on_set_default, on_repair) -> None`
  - `widget(self) -> Any`
  - `set_snapshot(self, snapshot: AccountSnapshot | None, now: float | None = None) -> None`
  - `set_last_updated(self, timestamp: float | None, now: float | None = None) -> None`
  - `set_default(self, is_default: bool) -> None`
  - `set_refreshing(self, active: bool) -> None`
- `class AccountPopupWindow`
  - `__init__(indicator: Any, gtk_module: Any, auto_close: bool, on_reload_all, on_toggle_auto_close, on_manage_accounts, on_quit) -> None`
  - `show_near_tray(self) -> None`
  - `hide(self) -> None`
  - `is_visible(self) -> bool`
  - `set_auto_close(self, enabled: bool) -> None`
  - `set_accounts(self, accounts: list[Account], card_factory: Callable[[Account], AccountCard]) -> None`
  - `update_global_status(self, text: str) -> None`
  - `update_account(self, account: Account, snapshot: AccountSnapshot | None, last_fetched: float | None, is_default: bool) -> None`
- Popup header must include literal text `AI Accounts` and a `Reload All` control plus an `Auto-close` toggle.

**Behavior:**
- Account cards render provider badge, alias/plan, healthy or broken badge, two GTK progress bars (`5h`, `7d`) when data exists, last-updated text, and action buttons.
- Broken accounts expose `Repair`; healthy accounts expose `Set Default`.
- Popup window hosts cards in a vertical scroll container and supports auto-close ON/OFF behavior.
- `Escape` always closes the popup, regardless of auto-close mode.
- Outside-click dismissal only applies when auto-close is ON.
- Card copy stays provider-neutral so the same surface can host Codex and Claude Code account data.
- Tests must use fake GTK widgets; no real desktop interaction in the acceptance gate.

**Acceptance (one executable check):**
- Run: `pytest tests/test_indicator.py -q -k "popup or account_card or auto_close"`
- Expected: PASS — popup/card tests cover widget creation, provider badge rendering, update methods, and dismissal rules.

- [ ] Write tests covering the behavior above (implementer writes the test code)
- [ ] Implement to satisfy the contract + acceptance (implementer writes the body)
- [ ] Run acceptance check → expected output above
- [ ] Commit: `git add account_card.py popup_window.py tests/test_indicator.py && git commit -m "feat: add tray popup dashboard widgets"`

### Task 2: Indicator Integration And Lazy Refresh Flow

**Wave:** 2
**Blocks:** Task 3
**Blocked by:** Task 1

**Files:**
- Modify: `indicator.py` — replace menu-heavy account presentation with popup toggle, state sync, lazy refresh on open, and popup action callbacks.
- Modify: `systray_codex_switcher.py` — ensure indicator startup still builds/runs with popup-based UI.
- Modify: `tests/test_indicator.py` — popup-toggle, refresh, per-account reload, and default/repair action coverage.

**Contract (pin EXACTLY):**
- `Indicator._toggle_popup() -> None`
- `Indicator._set_popup_auto_close(enabled: bool) -> None`
- `Indicator._reload_all_accounts() -> None`
- `Indicator._reload_account(account: Account) -> None`
- `Indicator._ensure_popup() -> Any`
- `Indicator._update_popup() -> None`
- `Indicator` must keep existing health/cache responsibilities and continue writing `~/.codex-tray/health_cache.json`.

**Behavior:**
- Tray click toggles popup open/closed instead of relying on multiline account rows for quota visibility.
- Opening the popup renders current cached/in-memory values immediately, then starts refresh work only for stale or missing accounts.
- `Reload All` forces all accounts to refresh.
- Per-account reload forces only that account.
- Snapshot updates must update popup cards in place as each async fetch completes.
- Provider-specific payloads must normalize into one popup card contract so Claude Code data can land without a second UI path.
- Existing default-account switching and broken-account repair flows remain intact.
- Auto-close setting must apply immediately when toggled in the popup.

**Acceptance (one executable check):**
- Run: `pytest tests/test_indicator.py -q -k "toggle_popup or reload_all or reload_account or lazy_refresh"`
- Expected: PASS — popup toggle, refresh dispatch, and in-place card updates work without regressions.

- [ ] Write tests covering the behavior above (implementer writes the test code)
- [ ] Implement to satisfy the contract + acceptance (implementer writes the body)
- [ ] Run acceptance check → expected output above
- [ ] Commit: `git add indicator.py systray_codex_switcher.py tests/test_indicator.py && git commit -m "feat: wire tray popup dashboard into indicator"`

### Task 3: Verification, Persistence, And UX Polish

**Wave:** 3
**Blocks:** —
**Blocked by:** Task 1, Task 2

**Files:**
- Modify: `tests/test_indicator.py` — final acceptance coverage for timestamps, default/broken card states, and auto-close persistence.
- Modify: `tests/test_systray_codex_switcher.py` — startup/build integration only if popup wiring changes are visible there.

**Contract (pin EXACTLY):**
- Auto-close preference is tray-local and persists across indicator rebuild/restart.
- Popup cards show last-updated metadata from `last_fetched`.
- Focused verification covers popup startup, lazy refresh, reload actions, default/repair actions, and dismissal semantics.

**Behavior:**
- Do not regress existing health fetch, cache write, or repair flows.
- Keep the tray icon process long-lived; popup close must not stop background refresh completion.
- If a test expectation needs updating because the popup replaces the old menu contract, update only assertions tied to the new UI shape.

**Acceptance (one executable check):**
- Run: `pytest tests/test_account_registry.py tests/test_systray_codex_switcher.py tests/test_indicator.py tests/test_cld.py -q`
- Expected: PASS with no warnings.

- [ ] Write or adjust tests covering the behavior above (implementer writes the test code)
- [ ] Implement any required persistence/polish changes to satisfy the contract + acceptance
- [ ] Run acceptance check → expected output above
- [ ] Commit: `git add tests/test_indicator.py tests/test_systray_codex_switcher.py && git commit -m "test: verify tray popup dashboard behavior"`
