Files
porthole/@PLAN.md
OpenCode Test e95536c9f1 docs: add project planning and implementation documentation
Add PLAN.md with full project specification and @PLAN.md with
parallel build plan for multi-agent implementation. Also add
IMPLEMENTATION_SUMMARY.md documenting Tasks W, X, Y, Z completion
(rollup drill-down, theme toggle, P0 alerts).
2025-12-24 13:30:35 -08:00

35 KiB
Raw Blame History

@PLAN — ControlTower (Go TUI) parallel build plan

This file breaks the project into independent, parallel tasks that multiple agents can implement with minimal merge conflicts.

  • Source spec: PLAN.md
  • Sub-agent models to use:
    • anthropic/claude-haiku-4-5-20251001
    • github-copilot/claude-haiku-4.5

0) Ground rules (so work composes cleanly)

0.1 File ownership

Each task below “owns” a set of files. Agents should only modify files they own, except for the explicit shared-contract files listed next.

0.2 Shared contracts (edit carefully)

These files are the primary integration points; keep them stable and coordinate changes:

  • internal/model/issue.go (Issue model + enums)
  • internal/collectors/collector.go (collector interface + status)
  • cmd/controltower/main.go (wiring + flags)

0.3 Go module + imports

  • Use standard package layout from PLAN.md.
  • No persistence; acknowledgements are in-memory only.
  • Prefer small packages with narrow responsibilities.

1) Proposed repo layout (target)

  • cmd/controltower/main.go
  • internal/model/ (Issue + enums)
  • internal/store/ (IssueStore)
  • internal/engine/ (scheduler + concurrency)
  • internal/collectors/collector.go (shared interface)
  • internal/collectors/host/ (disk.go, mem.go, load.go, net.go)
  • internal/collectors/k8s/ (client.go, informers.go, issues_*.go, rollup.go, unreachable.go)
  • internal/ui/ (app.go, table.go, details.go, keys.go, styles.go)
  • internal/export/json.go

2) Integration contracts (must align across tasks)

2.1 Issue model contract

internal/model.Issue must support (per PLAN.md):

  • Stable ID for dedupe
  • Category, Priority, State enums
  • Title, Details, Evidence (key/value), SuggestedFix
  • FirstSeen, LastSeen

Sorting defaults:

  • Priority desc → LastSeen desc (recency)

2.2 Collector contract

Create internal/collectors/collector.go with:

  • type Collector interface { Name() string; Interval() time.Duration; Collect(ctx context.Context) ([]model.Issue, Status, error) }
  • type Status describing health (OK/Degraded/Error + message + last success time)

Collectors should:

  • Be fast; respect context cancellation.
  • Return issues that are “currently true” for this tick (the store handles resolve-after).

2.3 Store contract

IssueStore.Upsert(now, issues):

  • Dedupes by Issue.ID
  • Tracks FirstSeen/LastSeen
  • Maintains State (Open/Acknowledged/Resolved)
  • Resolves only after resolveAfter (default 30s) of absence

Ack/unack should be in-memory and keyed by Issue ID.

2.4 Engine contract

Engine runs all collectors concurrently on their intervals with timeouts:

  • Host collectors: collector_timeout_fast (250ms)
  • K8s list/poll: collector_timeout_k8s_list (2s)

Engine produces periodic snapshots for UI/export:

  • []model.Issue sorted per default
  • Collector health summary (Host OK / K8s OK/DEGRADED)

3) Task breakdown (parallelizable)

Each task includes: scope, owned files, external dependencies, and acceptance checks.

Task A — Project scaffold + wiring (1 agent)

Goal: create runnable binary, shared contracts, and minimal plumbing.

  • Owns:
    • go.mod, go.sum
    • cmd/controltower/main.go
    • internal/collectors/collector.go
    • (Optional) internal/buildinfo/ or version constants
  • Provides:
    • CLI flag --export /path/issues.json (calls exporter and exits)
    • Default config constants (refresh interval 1s, resolveAfter 30s, timeouts)
    • K8s enabled only if kubeconfig exists (per spec)
  • Must not implement heavy logic from other tasks; stub as needed.

Acceptance: go test ./... passes; go run ./cmd/controltower starts.


Task B — Core model package (1 agent)

Goal: implement the type system used everywhere.

  • Owns:
    • internal/model/issue.go (+ small helper files if needed)
  • Implements:
    • Enums: Category, Priority, State (stringers / JSON marshal if needed)
    • Issue struct + helpers:
      • func (p Priority) Weight() int (for sorting)
      • func (i Issue) Age(now time.Time) time.Duration
      • func SortIssuesDefault([]Issue)
  • Evidence representation: map[string]string (simple, JSON-friendly).

Acceptance: unit tests for sorting + JSON marshalling (if added).


Task C — IssueStore (1 agent)

Goal: dedupe, state transitions, resolve-after, flap suppression.

  • Owns:
    • internal/store/store.go
    • internal/store/store_test.go
  • Needs:
    • resolveAfter default 30s
    • Flap suppression: dont resolve on a single missed tick
    • Acknowledge(id), Unack(id) (in-memory only)
    • Snapshot() returning issues (optionally copy to avoid races)

Acceptance: tests covering:

  • Dedup and timestamp updates
  • Ack persists while issue present
  • Resolve-after triggers only after absence window

Task D — Engine scheduler (1 agent)

Goal: concurrency orchestration + snapshot publication.

  • Owns:
    • internal/engine/engine.go
    • internal/engine/engine_test.go
  • Integrates:
    • Collectors from Task E (host) and Task F (k8s)
    • Store from Task C
    • Produces:
      • periodic snapshots (channel or callback) for UI
      • immediate refresh on demand (key r)
  • Concurrency:
    • Run collectors on their own tickers
    • Per-collector context timeouts
    • Track last error/health per collector

Acceptance: deterministic tests with fake collectors verifying:

  • timeout handling
  • interval scheduling
  • merge/upsert calls

Task E — Host collectors MVP (12 agents)

Split into sub-tasks to avoid conflicts:

E1 Disk collector

  • Owns: internal/collectors/host/disk.go (+ tests)
  • Reads /proc/mounts, uses statfs.
  • Filters pseudo filesystems.
  • Thresholds:
    • P1 if usage or inode usage ≥ 92%
    • P0 if ≥ 98%

E2 Memory + swap collector

  • Owns: internal/collectors/host/mem.go (+ tests)
  • Reads /proc/meminfo.
  • Applies thresholds in PLAN.md.

E3 Load collector

  • Owns: internal/collectors/host/load.go (+ tests)
  • Reads /proc/loadavg + CPU count.
  • Sustained windows: track 120s rolling window.

E4 Network collector

  • Owns: internal/collectors/host/net.go (+ tests)
  • Reads /proc/net/route for default route.
  • Reads /sys/class/net/*/operstate for “any UP”.
  • P1 if no default route AND any non-loopback UP.

Acceptance: each collector has unit tests using fixture files or temp fakes.


This is the largest area; split by responsibility.

F1 K8s client + connectivity + unreachable grace

  • Owns:
    • internal/collectors/k8s/client.go
    • internal/collectors/k8s/unreachable.go
  • Responsibilities:
    • Build client-go config from kubeconfig current context.
    • Connectivity tracking: emit Kubernetes/P0 only after 10s continuous failure.
    • RBAC errors: emit single P2 “Insufficient RBAC …” issue per resource.

F2 Informers / polling fallback + issue rules + rollups

  • Owns:
    • internal/collectors/k8s/informers.go
    • internal/collectors/k8s/issues_pods.go
    • internal/collectors/k8s/issues_nodes.go
    • internal/collectors/k8s/issues_workloads.go
    • internal/collectors/k8s/issues_events.go
    • internal/collectors/k8s/rollup.go
  • Responsibilities:
    • Prefer informers (LIST+WATCH caches).
    • If watch fails repeatedly, degrade to periodic LIST and emit a P1 degraded issue.
    • High-signal rules per PLAN.md.
    • Rollups:
      • group by (namespace, reason, kind)
      • if group size ≥ 20 emit rollup issue with samples
    • Cap: k8s.max_issues = 200 after rollups.

Acceptance:

  • Unit tests for rule functions (pure logic tests with fake objects)
  • Manual run against a cluster (optional) verifies unreachable grace.

Split UI work to reduce merge conflicts.

G1 App model + layout framing

  • Owns:
    • internal/ui/app.go
    • internal/ui/styles.go
  • Responsibilities:
    • Header bar (host/time/age/P0..P3 counts/collector health)
    • Layout: header + table + details pane
    • Wiring to engine snapshot updates (tea.Msg)

G2 Table + details + keybindings

  • Owns:
    • internal/ui/table.go
    • internal/ui/details.go
    • internal/ui/keys.go
  • Responsibilities:
    • Main table columns: Pri | Cat | Title | Age | State
    • Search /, filter p/c, sort s, focus tab
    • Ack/unack a
    • Optional: Expand details with enter

Acceptance:

  • UI runs and remains responsive with 1k synthetic issues.

Task H — Exporter (1 agent)

Goal: JSON snapshot export, no persistence.

  • Owns:
    • internal/export/json.go
  • Implements:
    • WriteIssues(path string, issues []model.Issue) error
    • Used by --export and (optional) UI hotkey E

Acceptance: --export writes valid JSON and exits 0.


Task I — End-to-end integration pass (1 agent)

Goal: compose all pieces, fix mismatches, and ensure acceptance criteria.

  • Owns (only light edits across packages):
    • small wiring fixes in cmd/controltower/main.go
    • integration tweaks in engine/UI
  • Validates:
    • Without kubeconfig: app runs, host collectors work
    • With kubeconfig but unreachable cluster: P0 appears only after 10s
    • With reachable cluster: issues populate; rollups protect UI

This assignment uses exactly 8 agents and keeps overlaps minimal. For each agent, you can use either model:

  • anthropic/claude-haiku-4-5-20251001
  • github-copilot/claude-haiku-4.5

Phase 1 (parallel, no blocking): Agents 16

  • Agent 1 (Scaffold/contracts): Task A
  • Agent 2 (Model): Task B
  • Agent 3 (Store): Task C
  • Agent 4 (Engine): Task D
  • Agent 5 (Host collectors): Task E1E4 (own all internal/collectors/host/*)
  • Agent 6 (Exporter): Task H

Phase 2 (parallel, after A+B exist): Agents 78

  • Agent 7 (Kubernetes collector): Task F1 + F2 (own all internal/collectors/k8s/*)
  • Agent 8 (TUI + integration): Task G1 + G2 + Task I (UI owns internal/ui/*; integration touches wiring only)

Notes:

  • Agent 5 is bundled intentionally to avoid conflicts in internal/collectors/host/.
  • K8s (Agent 7) is bundled intentionally to avoid informer/client contract churn.
  • UI + integration (Agent 8) is bundled so UI/engine message types stay aligned.

Start order (min waiting):

  • Start Agents 16 immediately.
  • Start Agents 78 once Agent 1 has created go.mod + internal/collectors/collector.go and Agent 2 has created internal/model/issue.go (so imports compile).

Merge order (min conflicts / keep build green):

  1. Agent 1 (scaffold/contracts) — establishes module + shared interfaces
  2. Agent 2 (model) — unblocks compilation for most packages
  3. Agent 3 (store) + Agent 6 (export) — mostly isolated, compile-time checks
  4. Agent 4 (engine) — depends on store/model/contracts
  5. Agent 5 (host collectors) — depends on model/contracts; can land anytime after (1,2)
  6. Agent 7 (k8s) — depends on model/contracts; may need go.sum updates
  7. Agent 8 (UI + integration) — land last; touches wiring and stabilizes API edges

If you use a single shared branch:

  • Prefer merging frequently in the above order and rebasing lagging work onto the latest main.
  • If a shared contract must change, have Agent 1 do the change and notify all other agents.

5) Suggested agent prompts (copy/paste)

Use each prompt with model anthropic/claude-haiku-4-5-20251001 or github-copilot/claude-haiku-4.5.

Prompt template

  • Implement only the task scope.
  • Touch only owned files.
  • Add small unit tests where indicated.
  • Keep APIs aligned with the shared contracts.

Agent prompt: Task B (model)

"Implement internal/model/issue.go per @PLAN.md §2.1. Add sorting helpers and minimal tests. Do not modify other packages."

Agent prompt: Task C (store)

"Implement internal/store IssueStore with dedupe, ack/unack, resolve-after (30s), flap suppression, and tests. Import and use internal/model."

Agent prompt: Task D (engine)

"Implement internal/engine scheduler running collectors concurrently with per-collector timeouts and health tracking. Provide snapshot publication for UI; add tests with fake collectors."

Agent prompt: Task E1E4 (host collectors)

"Implement the specified host collector in internal/collectors/host/*.go reading Linux proc/sys files as described. Emit Issues with correct IDs/evidence/priority and add small tests using fixtures/mocks."

Agent prompt: Task F1 (k8s base)

"Implement kubeconfig client creation and unreachable grace logic. Emit a single P0 issue only after 10s continuous failure; emit P2 on RBAC denial. Keep rule logic separate."

Agent prompt: Task F2 (k8s rules + rollups)

"Implement informer/polling collector and rule + rollup logic with caps. Keep rule functions testable. Coordinate only via shared contracts."

Agent prompt: Task G1/G2 (UI)

"Implement Bubble Tea UI pieces per @PLAN.md: header/table/details/keybindings. Subscribe to engine snapshots and keep rendering cheap."

Agent prompt: Task H (export)

"Implement JSON exporter used by --export and optional UI hotkey. Ensure output includes all Issue fields."

Agent prompt: Task I (integration)

"Integrate all packages, resolve API mismatches, and verify acceptance scenarios. Keep changes minimal and avoid refactors."

Agent prompt: Task J (Help overlay)

"Implement in-app help overlay showing all keybindings. Add ? key binding in keys.go and create help.go with overlay model and render logic."

Agent prompt: Task K (Direct priority jump)

"Add numeric bindings 0/1/2/3 to jump directly to P0/P1/P2/P3 filter. Keep p cycle for all."

Agent prompt: Task L (Visual filter indicators)

"Add highlighted style for active filters in header. Apply FilterActive style when pri= or cat= are set."

Agent prompt: Task M (Better empty state)

"Show friendly message 'All systems healthy' when no issues instead of just 'No issues'."

Agent prompt: Task N (Bulk ack all visible)

"Add A key to acknowledge all currently visible issues at once. Optimize updates and refresh counts."

Agent prompt: Task O (Page navigation)

"Document page up/down keys already supported by bubbles/table. Ensure they work correctly in Update handler."

Agent prompt: Task P (Home/End jump)

"Add g/G bindings to jump to first/last row quickly like vim/less."

Agent prompt: Task Q (Vi-style nav)

"Add j/k bindings as alternatives to arrow keys for down/up navigation."

Agent prompt: Task R (Collector health icons)

"Replace text 'ok=1 deg=0 err=Z' with visual icons (✓ ⚠ ✗). Use lipgloss styles."

Agent prompt: Task S (Issue count warning near cap)

"Add warning indicator in header when approaching 200-issue cap (show at >= 180)."

Agent prompt: Task T (Copy to clipboard)

"Add y key to copy SuggestedFix (or Title) to system clipboard. Add atotto/clipboard dependency."

Agent prompt: Task U (Dynamic column width)

"Add t key to toggle between compact and wide Title column widths for long issue titles."

Agent prompt: Task V (Age format toggle)

"Add d key to toggle between compact (2dm) and relative (2m ago) age formats."

Agent prompt: Task W (Rollup drill-down)

"When rollup issue selected, show 'Affected Issues' list with sample IDs in details pane."

Agent prompt: Task X (Light/dark theme toggle)

"Add T key to toggle between light and dark themes. Define two theme sets in styles.go."

Agent prompt: Task Y (Sound/flash on new P0)

"Add terminal bell/flash when P0 count increases (new critical issue detected). Respect NO_BELL env var."

Agent prompt: Task Z (Theme, sound, flash)

"Add theme toggle (T) and P0 alert (bell/flash) with env var support. Test with NO_BELL=1."

6) Definition of done

  • Meets PLAN.md Acceptance Criteria.
  • go test ./... passes.
  • go run ./cmd/controltower works without kubeconfig.
  • With kubeconfig and unreachable cluster: P0 only after 10s.
  • UI remains responsive under high issue counts (rollups + caps).

7) TUI UX Improvements

This section defines UX enhancements that improve discoverability, navigation efficiency, and visual clarity. Tasks are designed for parallel agent execution (independent file ownership).

7.1 Quick wins (high-impact, low-effort)

Task J — Help overlay

Goal: Users can discover all keybindings in-app without external docs.

Agent: 1 (can be same agent that implements Task J in Phase 2)

Owned files:

  • internal/ui/help.go (new)
  • internal/ui/keys.go (add Help binding, update prompts)
  • internal/ui/app.go (add help model + view routing)

Implementation:

  • Add Help key binding (?) to KeyMap
  • Create help.go with a Model struct:
    • List of keybindings by group (Global, Filters, Navigation, Actions)
    • render() function to display as overlay
  • Add helpRequestedMsg type in app.go to trigger help overlay
  • Update keys.go: add Help binding with help text
  • Update app.go:
    • Add showHelp bool field to Model
    • Handle ? key: toggle help overlay (hide table/details)
    • Update View() to conditionally show help instead of main layout

Acceptance:

  • Press ? shows keybinding list overlay covering main layout
  • Press ? again or esc dismisses help
  • go test ./... passes

Task K — Direct priority jump (0/1/2/3)

Goal: Jump directly to P0/P1/P2/P3 filter instead of cycling through all 5 options.

Agent: 1

Owned files:

  • internal/ui/keys.go (add numeric bindings)
  • internal/ui/app.go (update Update handler)

Implementation:

  • Add bindings in keys.go:
    • PriorityP0: key.NewBinding(key.WithKeys("0"), key.WithHelp("0", "P0 only"))
    • PriorityP1: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "P1 only"))
    • PriorityP2: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "P2 only"))
    • PriorityP3: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "P3 only"))
  • Update app.go Update handler:
    • Match keyMatch(msg, m.keys.PriorityP0) → set m.filterPri = model.PriorityP0
    • Match keyMatch(msg, m.keys.PriorityP1) → set m.filterPri = model.PriorityP1
    • Match keyMatch(msg, m.keys.PriorityP2) → set m.filterPri = model.PriorityP2
    • Match keyMatch(msg, m.keys.PriorityP3) → set m.filterPri = model.PriorityP3
    • Keep existing p cycle behavior for all option

Acceptance:

  • Pressing 0 shows only P0 issues; 1 shows only P1, etc.
  • Pressing p cycles through: all → P0 → P1 → P2 → P3 → all → …

Task L — Visual filter indicators

Goal: Make active filters visually obvious in header (highlight vs. plain text).

Agent: 1

Owned files:

  • internal/ui/styles.go (add filter highlight style)
  • internal/ui/app.go (update renderHeader())

Implementation:

  • Add style in styles.go:
    • FilterActive lipgloss.Style (e.g., bold + distinct foreground)
  • Update renderHeader() in app.go:
    • Apply FilterActive style to pri= or cat= portions when filter is active
    • Keep all in plain HeaderVal style

Acceptance:

  • pri=P1 or cat=Kubernetes appears highlighted/colored in header
  • pri=all or cat=all appears in plain text

Task M — Better empty state

Goal: When no issues, show helpful message instead of just “No issues”.

Agent: 1

Owned files:

  • internal/ui/app.go (update applyViewFromSnapshot())

Implementation:

  • In applyViewFromSnapshot() (around row building), when len(filtered) == 0:
    • Set details content to friendly message: “All systems healthy. No issues detected.”
    • Optionally: add hint like “Press r to refresh, / to search past logs”

Acceptance:

  • When snapshot has 0 issues, details pane shows friendly message
  • Header still displays correct P0/P1/P2/P3 counts (all 0)

Task N — Bulk ack all visible

Goal: Allow user to acknowledge all currently visible issues at once.

Agent: 1

Owned files:

  • internal/ui/keys.go (add bulk ack binding)
  • internal/ui/app.go (add AckAllVisible() method + handler)

Implementation:

  • Add binding in keys.go:
    • AckAll: key.NewBinding(key.WithKeys("A"), key.WithHelp("A", "ack all visible")) (shift+a)
  • Add method in app.go:
    • func (m *Model) AckAllVisible()
    • Iterate over m.rowsIDs, call m.ack(id) for each
    • Optimistically update issueByID and table rows
  • Update Update() handler:
    • Match keyMatch(msg, m.keys.AckAll) → call m.AckAllVisible()

Acceptance:

  • Pressing A acknowledges all issues currently visible in table
  • Header P0/P1/P2/P3 counts remain correct
  • a still toggles ack/unack for single selected issue

7.2 Navigation efficiency improvements

Task O — Page navigation (PgUp/PgDn or Ctrl+u/Ctrl+d)

Goal: Faster scrolling through long issue lists (200+ issues with rollups).

Agent: 1

Owned files:

  • internal/ui/table.go (no changes needed; bubbles/table supports page keys by default)

Implementation:

  • Bubbles/table already supports standard page keys:
    • pgup/pgdn
    • ctrl+u/ctrl+d
    • home/end
  • Ensure these are not overridden in app.go Update handler
  • Optionally: update help overlay to document these keys

Acceptance:

  • PageUp/PageDown jumps 10 rows in table
  • Ctrl+u/Ctrl+d jumps half a screen
  • Home/End jump to top/bottom

Task P — Home/End jump (g/G like vim/less)

Goal: Jump to first or last row quickly.

Agent: 1

Owned files:

  • internal/ui/keys.go (add bindings)
  • internal/ui/app.go (add handlers)

Implementation:

  • Add bindings in keys.go:
    • JumpToTop: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "jump to first"))
    • JumpToBottom: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "jump to last"))
  • Update app.go:
    • Handle JumpToTop → set cursor to 0
    • Handle JumpToBottom → set cursor to last row

Acceptance:

  • Pressing g moves selection to first visible row
  • Pressing G moves selection to last visible row

Task Q — Vi-style nav (j/k in addition to arrows)

Goal: Support vim-style navigation for users who prefer hjkl.

Agent: 1

Owned files:

  • internal/ui/keys.go (add bindings)
  • internal/ui/app.go (add handlers)

Implementation:

  • Add bindings in keys.go:
    • Down: key.NewBinding(key.WithKeys("j"), key.WithHelp("j", "down"))
    • Up: key.NewBinding(key.WithKeys("k"), key.WithHelp("k", "up"))
  • Update app.go:
    • Match Down and pass to m.table.Update(msg) (bubbles/table handles both arrows and j/k)
  • Update help overlay to document j/k as alternatives to arrows

Acceptance:

  • j moves selection down, k moves up
  • Arrows continue to work (both styles supported)

7.3 Visual polish & feedback

Task R — Collector health icons

Goal: Replace text “ok=1 deg=0 err=Z” with visual status icons.

Agent: 1

Owned files:

  • internal/ui/app.go (update renderHeader())
  • internal/ui/styles.go (add icon styles if desired)

Implementation:

  • Update renderHeader() health display:
    • Use icon instead of text:
      • for OK
      • for DEGRADED
      • for ERROR
    • Format: collectors: ✓1 ⚠0 ✗0
  • Optional: use lipgloss.SetGlyphWidth() if icons appear misaligned

Acceptance:

  • Header shows icon-based collector health (e.g., collectors: ✓4)
  • Status still readable for screen readers (icons are supplementary)

Task S — Issue count warning near cap

Goal: Warn users when approaching the 200-issue cap (after rollups).

Agent: 1

Owned files:

  • internal/ui/app.go (update renderHeader())

Implementation:

  • In renderHeader(), calculate total issues: total := p0 + p1 + p2 + p3
  • If total >= 180 (90% of cap):
    • Append warning text to collector health line: [~180/200]
    • Use warning color from styles.Error
  • Only show when near cap; dont clutter at low counts

Acceptance:

  • When near 200 issues, header shows warning indicator
  • Below 180 issues, no warning appears

Task T — Copy to clipboard (y on selected issue)

Goal: Quick copy of issue ID, Title, or SuggestedFix.

Agent: 1

Owned files:

  • internal/ui/keys.go (add binding)
  • internal/ui/app.go (add handler + clipboard dep if needed)
  • go.mod (add clipboard dependency)

Implementation:

  • Add binding in keys.go:
    • Copy: key.NewBinding(key.WithKeys("y"), key.WithHelp("y", "copy to clipboard"))
  • Add dependency in go.mod (pick one):
    • github.com/atotto/clipboard (cross-platform, widely used)
    • or use xclip/xsel via exec.Command for linux-only (no new dep)
  • Update app.go:
    • On y key, copy selected issues SuggestedFix to clipboard (most useful field)
    • Show brief confirmation in details pane: “Copied to clipboard”

Acceptance:

  • Pressing y copies SuggestedFix (or Title) to system clipboard
  • Brief confirmation shown in details pane

7.4 Nice-to-have (higher effort)

Task U — Dynamic column width

Goal: Allow widening Title column on demand for long issue titles.

Agent: 1

Owned files:

  • internal/ui/keys.go (add toggle binding)
  • internal/ui/app.go (add wideTitle state + update layout())

Implementation:

  • Add binding in keys.go:
    • ToggleWideTitle: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "toggle wide title"))
  • Add wideTitle bool field to Model
  • Update layout() to compute two widths:
    • Normal mode: compact Title width (current behavior)
    • Wide mode: allocate more space to Title column
  • On t toggle, recompute layout and rebuild rows

Acceptance:

  • Pressing t expands Title column
  • Pressing t again returns to compact width
  • Header shows current state (e.g., wide=on or icon)

Task V — Age format toggle (compact vs. “X ago”)

Goal: Switch between “2dm” (compact) and “2m ago” (relative) formats.

Agent: 1

Owned files:

  • internal/ui/keys.go (add toggle binding)
  • internal/ui/app.go (add ageMode field + update rendering)

Implementation:

  • Add binding in keys.go:
    • ToggleAgeFormat: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "toggle age format"))
  • Add ageMode AgeMode field to Model with values: AgeCompact (default), AgeRelative
  • Update table.go formatAge() or app.go wrapper to support both modes
  • In renderHeader(), keep compact age (0s, Xds, Xdm, Xdh, Xdd)
  • In details pane, show full time with relative suffix

Acceptance:

  • Default: table shows 2dm, details shows “2m ago”
  • Press d: table shows 2m ago, details show same
  • Press d again: revert to compact

Task W — Rollup drill-down in details

Goal: When a rollup issue is selected, show the individual issues it aggregates.

Agent: 1

Owned files:

  • internal/ui/model (add rollup-specific types) — or keep minimal
  • internal/ui/app.go (update renderIssueDetails() / setDetailsToSelected())

Implementation:

  • Add helper getRollupSamples(iss model.Issue) []string:
    • Parse iss.Evidence["samples"] if it exists
    • Return list of affected IDs/pod names
  • Update renderIssueDetails() in details.go:
    • Detect rollup (e.g., ID starts with k8s:rollup: or category=Kubernetes + “rollup” in title)
    • Append section: Affected Issues with list of sample IDs (up to 10)
  • Keep full rollup summary (count, namespace, reason)

Acceptance:

  • Selecting a rollup issue shows “Affected Issues: [k8s:pod:default/x, k8s:pod:default/y, …]” in details
  • Non-rollup issues show normal details without this section

Task X — Light/dark theme toggle

Goal: Respect users theme preference or allow toggling.

Agent: 1

Owned files:

  • internal/ui/styles.go (add theme variants)
  • internal/ui/keys.go (add toggle binding)
  • internal/ui/app.go (add theme state)
  • cmd/controltower/main.go (optional env var support)

Implementation:

  • Define two theme sets in styles.go:
    • LightTheme (current hardcoded colors)
    • DarkTheme (inverted/better contrast)
  • Add themeMode ThemeMode field to Model (ThemeAuto, ThemeLight, ThemeDark)
  • Add binding in keys.go:
    • ToggleTheme: key.NewBinding(key.WithKeys("T"), key.WithHelp("T", "toggle theme"))
  • Optional: respect NO_COLOR=1 env var (disable all colors)
  • On theme toggle, update styles := defaultStyles() to apply selected theme

Acceptance:

  • Default theme is current (light-like)
  • Pressing T toggles light/dark
  • NO_COLOR=1 disables colors entirely

Task Y — Sound/flash on new P0

Goal: Alert user when critical issues appear.

Agent: 1

Owned files:

  • internal/ui/app.go (track P0 count changes + trigger alert)

Implementation:

  • Add lastP0Count int field to Model
  • In applyViewFromSnapshot():
    • Compare p0 with m.lastP0Count
    • If p0 > m.lastP0Count: new critical issues appeared
    • Update m.lastP0Count = p0
  • Add alert function:
    • Use bubbleteas bell (terminal bell) or flash:
      • Send tea.BellMsg on P0 increase
    • Optional: check for NO_BELL=1 env var to disable

Acceptance:

  • When P0 count increases, terminal bell/flash occurs (if not disabled)
  • P0 decrease or no change does not trigger alert
  • NO_BELL=1 env var suppresses all alerts

8-agent assignment for UX tasks

To keep parallelism and avoid conflicts, assign as follows (can be done in Phase 3):

Agent Tasks Owned Files Notes
Agent 9 Task J (Help overlay) internal/ui/help.go, keys.go, app.go High discoverability impact
Agent 10 Tasks K, L, M (Priority jump, visual filters, empty state) keys.go, styles.go, app.go Quick navigation wins
Agent 11 Tasks N, O, P (Bulk ack, page nav, home/end) keys.go, app.go Nav efficiency
Agent 12 Tasks Q, R (Vi nav, collector health icons) keys.go, app.go, styles.go Nav + visual polish
Agent 13 Tasks S, T, V (Count warning, copy clipboard, dynamic width) keys.go, app.go, go.mod, table.go Polish + utility
Agent 14 Tasks W, X (Rollup drill-down, age toggle) app.go, details.go, keys.go Details enhancements
Agent 15 Tasks Y, Z (Theme, sound/flash) styles.go, app.go, keys.go Theme + alerts

Execution order (minimal dependencies):

  1. Agents 912 run in parallel (all are independent file changes)
  2. Agents 1314 run after 912 (may reference new keys/styles)
  3. Agent 15 runs after 1314 (may reference theme from styles)

8-agent assignment (UX improvements, parallel)

Agent Tasks Owned Files Notes
Agent 9 Task J (Help overlay) internal/ui/help.go, keys.go, app.go High discoverability impact
Agent 10 Tasks K, L, M (Priority jump, visual filters, empty state) keys.go, styles.go, app.go Quick navigation wins
Agent 11 Tasks N, O, P (Bulk ack, page nav, home/end) keys.go, app.go Nav efficiency
Agent 12 Tasks Q, R (Vi nav, collector health icons) keys.go, app.go, styles.go Nav + visual polish
Agent 13 Tasks S, T, V (Count warning, copy clipboard, dynamic width) keys.go, app.go, go.mod, table.go Polish + utility
Agent 14 Tasks W, X (Rollup drill-down, age toggle) app.go, details.go, keys.go Details enhancements
Agent 15 Tasks Y, Z (Theme, sound/flash) styles.go, app.go, keys.go Theme + alerts

Execution order (minimal dependencies):

  1. Agents 912 run in parallel (all are independent file changes)
  2. Agents 1314 run after 912 (may reference new keys/styles)
  3. Agent 15 runs after 1314 (may reference theme from styles)

Coordinated changes:

  • keys.go is shared across Agents 915; each agent appends new bindings without removing others.
  • app.go is shared; each agent adds new handlers in disjoint methods to avoid merge conflicts.
  • styles.go is shared; agents add new styles without modifying existing.
  • details.go is shared; Agents 1314 add optional sections.
  • help.go is new (Task J) and owned entirely by Agent 9.

Agent prompt: Task J (Help overlay)

"Implement in-app help overlay showing all keybindings. Add ? key in keys.go, create help.go with Model + render() function. Toggle overlay with ? key and dismiss with ? or esc."

Agent prompt: Task K (Direct priority jump)

"Add numeric bindings 0/1/2/3 in keys.go to jump directly to P0/P1/P2/P3. Keep p cycle for all option. Update app.go Update handler."

Agent prompt: Task L (Visual filter indicators)

"Add FilterActive style in styles.go (highlight color). Update renderHeader() to apply when pri= or cat= filters are active."

Agent prompt: Task M (Better empty state)

"Show friendly message 'All systems healthy' in applyViewFromSnapshot() when len(filtered) == 0."

Agent prompt: Task N (Bulk ack all visible)

"Add AckAll binding (shift+a) in keys.go. Implement AckAllVisible() method in app.go that iterates rowsIDs and calls ack(). Update handler."

Agent prompt: Task O (Page navigation)

"Document that bubbles/table supports page keys (pgup/pgdn, ctrl+u/ctrl+d). Ensure they are not overridden in app.go Update handler. Add to help overlay."

Agent prompt: Task P (Home/End jump)

"Add g/G bindings in keys.go. Add handlers in app.go: g sets cursor to 0, G sets cursor to last row. Add to help overlay."

Agent prompt: Task Q (Vi-style nav)

"Add j/k bindings in keys.go. Update app.go to pass to table.Update(); bubbles/table handles both arrows and j/k. Update help overlay."

Agent prompt: Task R (Collector health icons)

"Update renderHeader() to use icons (✓ for OK, ⚠ for DEGRADED, ✗ for ERROR) instead of text. Format: 'collectors: ✓1 ⚠0 ✗0'."

Agent prompt: Task S (Issue count warning near cap)

"In renderHeader(), calculate total issues. If total >= 180, append warning '[~180/200]' with error color. Show only when near cap."

Agent prompt: Task T (Copy to clipboard)

"Add y binding in keys.go. Add atotto/clipboard to go.mod. On y key, copy selected issue's SuggestedFix to clipboard. Show brief confirmation in details."

Agent prompt: Task U (Dynamic column width)

"Add ToggleWideTitle binding (t) in keys.go. Add wideTitle bool to Model. Update layout() to compute wide vs compact widths. Toggle with t key. Update help overlay."

Agent prompt: Task V (Age format toggle)

"Add ToggleAgeFormat binding (d) in keys.go. Add AgeMode field (Compact/Relative) to Model. Update formatAge() or wrapper in app.go to support both modes. Toggle with d key."

Agent prompt: Task W (Rollup drill-down)

"Add getRollupSamples() helper to parse Evidence['samples']. Update renderIssueDetails() in details.go to detect rollup and append 'Affected Issues' section with sample IDs."

Agent prompt: Task X (Light/dark theme toggle)

"Define LightTheme and DarkTheme in styles.go. Add themeMode field to Model with Auto/Light/Dark. Add ToggleTheme binding (T) in keys.go. Respect NO_COLOR env var."

Agent prompt: Task Y (Sound/flash on new P0)

"Add lastP0Count to Model. In applyViewFromSnapshot(), compare p0 with lastP0Count. If p0 increases, send bubbletea.BellMsg. Check NO_BELL env var."

Agent prompt: Task Z (Theme, sound, flash)

"Combine Task X (theme toggle) and Task Y (P0 alert). Test theme toggle works and P0 bell/flash. Add env var checks for NO_BELL."