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).
35 KiB
@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-20251001github-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.gointernal/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
IDfor dedupe Category,Priority,StateenumsTitle,Details,Evidence(key/value),SuggestedFixFirstSeen,LastSeen
Sorting defaults:
Prioritydesc →LastSeendesc (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 Statusdescribing 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.Issuesorted 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.sumcmd/controltower/main.gointernal/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)
- CLI flag
- 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) Issuestruct + helpers:func (p Priority) Weight() int(for sorting)func (i Issue) Age(now time.Time) time.Durationfunc SortIssuesDefault([]Issue)
- Enums:
- 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.gointernal/store/store_test.go
- Needs:
resolveAfterdefault 30s- Flap suppression: don’t 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.gointernal/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 (1–2 agents)
Split into sub-tasks to avoid conflicts:
E1 Disk collector
- Owns:
internal/collectors/host/disk.go(+ tests) - Reads
/proc/mounts, usesstatfs. - Filters pseudo filesystems.
- Thresholds:
P1if usage or inode usage ≥ 92%P0if ≥ 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/routefor default route. - Reads
/sys/class/net/*/operstatefor “any UP”. P1if no default route AND any non-loopback UP.
Acceptance: each collector has unit tests using fixture files or temp fakes.
Task F — Kubernetes collector MVP (2 agents recommended)
This is the largest area; split by responsibility.
F1 K8s client + connectivity + unreachable grace
- Owns:
internal/collectors/k8s/client.gointernal/collectors/k8s/unreachable.go
- Responsibilities:
- Build client-go config from kubeconfig current context.
- Connectivity tracking: emit
Kubernetes/P0only 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.gointernal/collectors/k8s/issues_pods.gointernal/collectors/k8s/issues_nodes.gointernal/collectors/k8s/issues_workloads.gointernal/collectors/k8s/issues_events.gointernal/collectors/k8s/rollup.go
- Responsibilities:
- Prefer informers (LIST+WATCH caches).
- If watch fails repeatedly, degrade to periodic LIST and emit a
P1degraded issue. - High-signal rules per
PLAN.md. - Rollups:
- group by
(namespace, reason, kind) - if group size ≥ 20 emit rollup issue with samples
- group by
- Cap:
k8s.max_issues = 200after rollups.
Acceptance:
- Unit tests for rule functions (pure logic tests with fake objects)
- Manual run against a cluster (optional) verifies unreachable grace.
Task G — Bubble Tea TUI (2 agents recommended)
Split UI work to reduce merge conflicts.
G1 App model + layout framing
- Owns:
internal/ui/app.gointernal/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.gointernal/ui/details.gointernal/ui/keys.go
- Responsibilities:
- Main table columns:
Pri | Cat | Title | Age | State - Search
/, filterp/c, sorts, focustab - Ack/unack
a - Optional: Expand details with
enter
- Main table columns:
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
--exportand (optional) UI hotkeyE
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
- small wiring fixes in
- Validates:
- Without kubeconfig: app runs, host collectors work
- With kubeconfig but unreachable cluster:
P0appears only after 10s - With reachable cluster: issues populate; rollups protect UI
4) 8-agent assignment (recommended)
This assignment uses exactly 8 agents and keeps overlaps minimal. For each agent, you can use either model:
anthropic/claude-haiku-4-5-20251001github-copilot/claude-haiku-4.5
Phase 1 (parallel, no blocking): Agents 1–6
- 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 E1–E4 (own all
internal/collectors/host/*) - Agent 6 (Exporter): Task H
Phase 2 (parallel, after A+B exist): Agents 7–8
- 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 1–6 immediately.
- Start Agents 7–8 once Agent 1 has created
go.mod+internal/collectors/collector.goand Agent 2 has createdinternal/model/issue.go(so imports compile).
Merge order (min conflicts / keep build green):
- Agent 1 (scaffold/contracts) — establishes module + shared interfaces
- Agent 2 (model) — unblocks compilation for most packages
- Agent 3 (store) + Agent 6 (export) — mostly isolated, compile-time checks
- Agent 4 (engine) — depends on store/model/contracts
- Agent 5 (host collectors) — depends on model/contracts; can land anytime after (1,2)
- Agent 7 (k8s) — depends on model/contracts; may need go.sum updates
- 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 E1–E4 (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.mdAcceptance Criteria. go test ./...passes.go run ./cmd/controltowerworks without kubeconfig.- With kubeconfig and unreachable cluster:
P0only 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(addHelpbinding, update prompts)internal/ui/app.go(add help model + view routing)
Implementation:
- Add
Helpkey binding (?) toKeyMap - Create
help.gowith aModelstruct:- List of keybindings by group (Global, Filters, Navigation, Actions)
render()function to display as overlay
- Add
helpRequestedMsgtype inapp.goto trigger help overlay - Update
keys.go: addHelpbinding with help text - Update
app.go:- Add
showHelp boolfield to Model - Handle
?key: toggle help overlay (hide table/details) - Update
View()to conditionally show help instead of main layout
- Add
Acceptance:
- Press
?shows keybinding list overlay covering main layout - Press
?again orescdismisses 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.goUpdate handler:- Match
keyMatch(msg, m.keys.PriorityP0)→ setm.filterPri = model.PriorityP0 - Match
keyMatch(msg, m.keys.PriorityP1)→ setm.filterPri = model.PriorityP1 - Match
keyMatch(msg, m.keys.PriorityP2)→ setm.filterPri = model.PriorityP2 - Match
keyMatch(msg, m.keys.PriorityP3)→ setm.filterPri = model.PriorityP3 - Keep existing
pcycle behavior foralloption
- Match
Acceptance:
- Pressing
0shows only P0 issues;1shows only P1, etc. - Pressing
pcycles 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(updaterenderHeader())
Implementation:
- Add style in
styles.go:FilterActive lipgloss.Style(e.g., bold + distinct foreground)
- Update
renderHeader()inapp.go:- Apply
FilterActivestyle topri=orcat=portions when filter is active - Keep
allin plainHeaderValstyle
- Apply
Acceptance:
pri=P1orcat=Kubernetesappears highlighted/colored in headerpri=allorcat=allappears 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(updateapplyViewFromSnapshot())
Implementation:
- In
applyViewFromSnapshot()(around row building), whenlen(filtered) == 0:- Set details content to friendly message: “All systems healthy. No issues detected.”
- Optionally: add hint like “Press
rto 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(addAckAllVisible()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, callm.ack(id)for each - Optimistically update
issueByIDand table rows
- Update
Update()handler:- Match
keyMatch(msg, m.keys.AckAll)→ callm.AckAllVisible()
- Match
Acceptance:
- Pressing
Aacknowledges all issues currently visible in table - Header P0/P1/P2/P3 counts remain correct
astill 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/pgdnctrl+u/ctrl+dhome/end
- Ensure these are not overridden in
app.goUpdate 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
- Handle
Acceptance:
- Pressing
gmoves selection to first visible row - Pressing
Gmoves 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
Downand pass tom.table.Update(msg)(bubbles/table handles both arrows and j/k)
- Match
- Update help overlay to document
j/kas alternatives to arrows
Acceptance:
jmoves selection down,kmoves 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(updaterenderHeader())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
- Use icon instead of text:
- 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(updaterenderHeader())
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
- Append warning text to collector health line:
- Only show when near cap; don’t 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.Commandfor linux-only (no new dep)
- Update
app.go:- On
ykey, copy selected issue’sSuggestedFixto clipboard (most useful field) - Show brief confirmation in details pane: “Copied to clipboard”
- On
Acceptance:
- Pressing
ycopies 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(addwideTitlestate + update layout())
Implementation:
- Add binding in
keys.go:ToggleWideTitle: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "toggle wide title"))
- Add
wideTitle boolfield to Model - Update
layout()to compute two widths:- Normal mode: compact Title width (current behavior)
- Wide mode: allocate more space to Title column
- On
ttoggle, recompute layout and rebuild rows
Acceptance:
- Pressing
texpands Title column - Pressing
tagain returns to compact width - Header shows current state (e.g.,
wide=onor 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(addageModefield + update rendering)
Implementation:
- Add binding in
keys.go:ToggleAgeFormat: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "toggle age format"))
- Add
ageMode AgeModefield to Model with values:AgeCompact(default),AgeRelative - Update
table.goformatAge()orapp.gowrapper 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 shows2m ago, details show same - Press
dagain: 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 minimalinternal/ui/app.go(updaterenderIssueDetails()/setDetailsToSelected())
Implementation:
- Add helper
getRollupSamples(iss model.Issue) []string:- Parse
iss.Evidence["samples"]if it exists - Return list of affected IDs/pod names
- Parse
- Update
renderIssueDetails()indetails.go:- Detect rollup (e.g., ID starts with
k8s:rollup:or category=Kubernetes + “rollup” in title) - Append section:
Affected Issueswith list of sample IDs (up to 10)
- Detect rollup (e.g., ID starts with
- 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 user’s 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 ThemeModefield 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=1env var (disable all colors) - On theme toggle, update
styles := defaultStyles()to apply selected theme
Acceptance:
- Default theme is current (light-like)
- Pressing
Ttoggles light/dark NO_COLOR=1disables 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 intfield to Model - In
applyViewFromSnapshot():- Compare
p0withm.lastP0Count - If
p0 > m.lastP0Count: new critical issues appeared - Update
m.lastP0Count = p0
- Compare
- Add alert function:
- Use bubbletea’s
bell(terminal bell) or flash:- Send
tea.BellMsgon P0 increase
- Send
- Optional: check for
NO_BELL=1env var to disable
- Use bubbletea’s
Acceptance:
- When P0 count increases, terminal bell/flash occurs (if not disabled)
- P0 decrease or no change does not trigger alert
NO_BELL=1env 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):
- Agents 9–12 run in parallel (all are independent file changes)
- Agents 13–14 run after 9–12 (may reference new keys/styles)
- Agent 15 runs after 13–14 (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):
- Agents 9–12 run in parallel (all are independent file changes)
- Agents 13–14 run after 9–12 (may reference new keys/styles)
- Agent 15 runs after 13–14 (may reference theme from styles)
Coordinated changes:
keys.gois shared across Agents 9–15; each agent appends new bindings without removing others.app.gois shared; each agent adds new handlers in disjoint methods to avoid merge conflicts.styles.gois shared; agents add new styles without modifying existing.details.gois shared; Agents 13–14 add optional sections.help.gois 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."