# Tray Popup Dashboard Design

Audience: AI coding agents first.

## Goal

Replace the overloaded multiline tray menu with a transient graphical popup window anchored from the tray icon. The popup must show compact account cards with real GTK progress bars, support `Reload All` plus per-account reload, allow `Set Default` and `Repair`, display healthy/broken state, show last-updated metadata, lazily refresh account limits when the tray icon is clicked, and stay provider-aware so Codex data and Claude Code data can share the same surface.

## Approaches

### Approach 1: Tray-anchored transient GTK popup window

Keep the tray icon as the entry point, but replace the account-data-heavy menu with a small popup-style GTK window that opens near the tray and auto-closes on outside click or `Escape`.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Reuses the current long-lived GTK process and health client, with fewer widget hacks than the current menu-label approach. |
| Long-term | Clean path for adding more controls without forcing them into menu items. |
| Scalability | Handles a modest account list well; cards can scroll inside the popup without turning the tray menu itself into a scrolling control. |
| Performance | Cheap; refresh work stays async and only triggers on open or explicit reload. |
| Reversibility | Two-way door — the popup is a new surface layered on the tray, not a rewrite of core account logic. |

**Weakness:** GTK popup positioning and focus-dismiss behavior are a little finicky across desktop environments.

### Approach 2: Keep the menu, add custom row widgets inside it

Continue using the tray menu as the only surface, but replace text-only rows with embedded boxes/progress widgets and action rows.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Fragile; status menus are not a good host for rich controls and their rendering/interaction is toolkit-dependent. |
| Long-term | Poor — every enhancement fights menu semantics. |
| Scalability | Weak; many accounts still push the menu into awkward heights. |
| Performance | Fine, but UI quality degrades before performance does. |
| Reversibility | One-way-ish in practice — once rich behavior is baked into menu internals, extracting it later is annoying. |

**Weakness:** Solves the current visual issue incompletely and keeps the wrong UI surface.

### Approach 3: Full standalone window now, tray becomes launcher only

Promote the account UI to a normal window immediately and reduce the tray to launch/status actions.

| Dimension | Assessment |
|-----------|------------|
| Robustness | Strong end-state architecture. |
| Long-term | Best extensibility for future features. |
| Scalability | Best room for richer controls and diagnostics. |
| Performance | Fine; larger surface but still trivial workload. |
| Reversibility | One-way door — changes product shape now, before the need is proven. |

**Weakness:** Overbuilds the current requirement and adds product surface the user explicitly does not need yet.

**Recommended: Approach 1** — it gives the user a graphical, button-capable UI now without prematurely turning the tool into a standalone app. It fixes the tray menu ergonomics problem at the correct layer and keeps the path to a future full window open.

## UX Contract

### Open/close behavior

- Clicking the tray icon opens the popup.
- If already open, clicking the tray icon toggles it closed.
- `Auto-close` is a user-toggleable setting stored in tray-local config.
- When `Auto-close` is ON:
  - clicking outside the popup closes it
  - pressing `Escape` closes it
- When `Auto-close` is OFF:
  - outside click does not close it
  - `Escape` still closes it
- The popup is transient and never persists as an independent app window in task switching.

### Popup layout

The popup is a compact vertical panel with three regions:

1. Header row
   - title: `AI Accounts`
   - `Reload All` button
   - `Auto-close` toggle
   - last global refresh summary (`Updated just now`, `Updated 2m ago`, `Refresh failed`, etc.)
2. Account list
   - one card per account
   - vertically stacked in a scroll container if needed
3. Footer row
   - `Manage Accounts`
   - `Quit`

### Account card

Each account card shows:

- provider badge (`Codex`, `Claude`, or other normalized source label)
- alias and plan
- healthy/broken badge
- default marker or `Set Default` action
- two progress rows:
  - `5h`
  - `7d`
- per-account last updated timestamp
- per-account actions:
  - reload icon button
  - `Set Default`
  - `Repair` (only enabled/visible for broken accounts)

### Progress presentation

- Use native GTK `ProgressBar` widgets, not ASCII bars.
- Bars always show real percentages.
- Bars clamp display to `0..100`.
- Time-left text appears only when reset metadata exists and is in the future.
- No local guessing when reset metadata is absent.
- Progress semantics are provider-agnostic; the UI consumes normalized percentage/reset fields and does not hard-code Codex-only wording into labels or empty states.

### Lazy refresh behavior

- Opening the popup starts refresh work for accounts whose data is stale or missing.
- Existing cached values render immediately if available; cards update in place as each async fetch completes.
- If the popup opens and fresh data is already present, do not force a reload automatically.
- `Reload All` always forces all accounts to refresh immediately.
- Per-account reload always forces only that account.

## Architecture

### Recommended module split

- `indicator.py`
  - owns tray icon, open/close wiring, popup lifecycle, high-level refresh orchestration, and provider-to-card normalization
- `popup_window.py`
  - owns the transient popup window/widget tree and event handling
- `account_card.py`
  - owns one account card’s GTK subtree and update methods
- `health_client.py`
  - unchanged responsibility: fetch and cache account health snapshots

This split keeps the tray controller separate from the richer GTK layout surface.

## Component seams

### Popup controller seam

`Indicator._toggle_popup() -> None`

Behavior:
- opens popup if absent/hidden
- closes popup if already visible
- triggers lazy refresh evaluation on open

`Indicator._set_popup_auto_close(enabled: bool) -> None`

Behavior:
- updates in-memory popup behavior immediately
- persists the preference in tray-local config

### Popup window seam

`AccountPopupWindow(indicator: Indicator, gtk_module: Any, accounts: list[Account])`

Required methods:
- `show_near_tray() -> None`
- `hide() -> None`
- `is_visible() -> bool`
- `update_global_status(text: str) -> None`
- `update_account(account: Account, snapshot: AccountSnapshot | None, last_fetched: float | None, is_default: bool) -> None`

Behavior:
- Popup copy and grouping stay provider-neutral.
- If multiple providers are present, each card must visibly identify its provider.

### Account card seam

`AccountCard(account: Account, gtk_module: Any, on_reload, on_set_default, on_repair)`

Required methods:
- `set_snapshot(snapshot: AccountSnapshot | None, now: float | None = None) -> None`
- `set_last_updated(timestamp: float | None, now: float | None = None) -> None`
- `set_default(is_default: bool) -> None`
- `set_refreshing(active: bool) -> None`

Behavior:
- Card chrome must not assume Codex-only terminology.
- Provider label comes from normalized account metadata, not hard-coded UI strings.

## Data flow

1. User clicks tray icon.
2. `Indicator` toggles popup visibility.
3. Popup renders immediately from current in-memory `snapshots` and `last_fetched`.
4. `Indicator` determines which accounts are stale/missing and schedules refreshes.
5. Provider-specific fetchers return per-account snapshots asynchronously.
6. `Indicator._apply_snapshot(...)` normalizes provider-specific payloads into one card shape and pushes account-level updates into the popup/card widgets.
7. `health_client.write_cache(...)` persists the updated snapshot set.

## Error handling

- Failed/stale fetch for one account must not blank other cards.
- A refresh failure leaves prior healthy data visible if it exists.
- Broken accounts show a broken badge and enabled `Repair`.
- Missing snapshot data renders a loading/unknown state instead of collapsing the card.
- Popup close while refresh is in flight is allowed; background refresh completes and updates cached state without requiring the popup to remain open.
- `Auto-close` OFF must not block explicit close via tray click or `Escape`.

## Testing strategy

Add focused GTK fake-widget tests for:

- popup toggle open/close behavior
- outside-click / `Escape` dismissal logic with `Auto-close` ON and OFF
- lazy refresh on popup open
- `Reload All` and per-account reload dispatch
- provider badge rendering and provider-neutral copy
- account card updates when snapshots arrive asynchronously
- broken-account action state
- default-account badge/action state
- timestamp rendering and stale/fresh behavior
- config persistence for the `Auto-close` preference

Retain existing health-client and cache compatibility coverage.

## Architecture Decisions

- Accepted: move rich account presentation out of the tray menu and into a transient popup window. The menu remains only for low-density utility actions if needed.
- Accepted: keep health fetch/caching logic in the existing tray process; no new daemon or HTTP layer is needed now.
- Rejected: rich custom menu rows. Reason: wrong widget surface, high UI fragility, poor future extensibility.
- Rejected: full standalone window now. Reason: more product surface than needed before popup-based UX is proven.
