feat(cli,kernel): TUI task panel + sub-agent renderer (PR-E)#18
Merged
Conversation
PR-E is the final slice atop PR-C (tasks) and PR-D (sub-agents): the TUI rendering layer. Two surfaces ship: 1. Task panel above input — compact, always-all-tasks, hidden when empty. Matches Claude Code's pattern. 2. spawn_sub_agent custom renderer — replaces GenericTool fallback with a state machine (in-flight indicator with elapsed timer → collapsed → focus-to-expand → failed). Transport: new TASKS_UPDATED protocol frame, push-payload (not refetch). Broadcast on three triggers: onConnect, manage_tasks execute, spawn_sub_agent task.result write. DB is source of truth; frame is delivery. Non-goals: sub-agent streaming, compaction surfacing, cost meter, retro transcript view, interactive task editing, height cap on the panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks (6 implementation + 1 deploy/smoke). Each task is one file or one tightly-coupled file pair so subagent-driven implementation has clean boundaries and one commit per task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the frame type, TaskRow row shape, and KernelToClient union extension. Wire definition only — no kernel or TUI consumers yet (those land in subsequent tasks). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Queries schema.task by threadId, maps to TaskRow, fans out via broadcastToThread. Single source of truth for task-panel state on the wire. No callers yet — they land in the next task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- onConnect: seed panel state after CF_AGENT_CHAT_MESSAGES send - manage_tasks execute: after DB mutations, before returning - spawn_sub_agent: after task.result write (state change that doesn't go through manage_tasks) Each call site catches broadcast failures: a stale panel is a better outcome than a closed WS or a failed tool call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tasks: TaskRow[] state + setTasks action. handle-frame.ts routes TASKS_UPDATED to setTasks. No UI consumer yet — TaskPanel lands in the next task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Compact one-line-per-task list with status glyphs (☐ pending, ▶ in_progress, ✓ complete, ✗ failed, ⊘ cancelled). Auto-hides when zero tasks. Mounted in the live region between in-flight streaming and the input row. Clears tasks on thread switch — kernel's onConnect re-broadcasts the new thread's state, but a brief window without that clear would render stale tasks during the WS reconnect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
State machine: input-streaming (⏳) → in-flight (🤖 + elapsed timer) → collapsed completed (✓ + ~tok) → focus-to-expand bordered box showing finalText. Failed state takes precedence and shows the errorText with no expand affordance. Model badge color-coded: opus=magenta, sonnet=blue, haiku=green. manage_tasks deliberately stays on GenericTool — the panel (TaskPanel) is the canonical surface; the inline tool call is audit-trail only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review caught three issues: 1. Field name mismatch — kernel input schema names it 'prompt', not 'description'. The prompt line never rendered. Renamed in renderer + spec. 2. output-error state fell through to null. Now folded into the failure branch alongside errorText. Defensive fallback string if state=output-error with no errorText. 3. borderStyle 'single' → 'round' to match sibling CLI components (FileSuggestions, ApprovalStrip, InputBox, ConflictResolver). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The useElapsedSeconds setInterval was triggering ~1 re-render/sec during sub-agent runs. Ink's clearTerminal branch (CSI 2J 3J H + full output re-emit) fires whenever the live region exceeds stdout.rows, and the \x1b[3J 'erase scrollback' sequence is dropped silently in tmux + many terminals. Net effect: every overflowing re-render appended TaskPanel's top lines to scrollback. Replace the live elapsed counter with a static 'running' indicator. ~17 of 18 renders eliminated. Lose: live elapsed display during sub-agent runs. Gain: zero scrollback duplication. Longer-term fix (deferred): upgrade Ink 6→7 + clamp LiveRow height via maxHeight + overflow='hidden' using useStdout(). Tracks the broader 'live region overflows during long Markdown streams' issue that affects more than just this hook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Surfaces PR-C (tasks) and PR-D (sub-agents) backend state in the Ink TUI. Third and final slice of the tasks+sub-agents trilogy.
Two surfaces
output-error). Model badge color-coded (opus=magenta, sonnet=blue, haiku=green).manage_tasksdeliberately stays on GenericTool — the panel is the canonical surface.Transport
New
TASKS_UPDATEDprotocol frame, push-payload (not refetch). Three broadcast triggers:kernel.onConnect— seeds panel state for new connections / thread resumesmanage_tasksexecute — after DB mutationsspawn_sub_agent— after writingtask.result(state change that doesn't go through manage_tasks)Every call site is wrapped in try/catch with a tagged
console.error— broadcast failures don't close the WS or fail the tool call. DB is source of truth; frame is delivery.Files (14 total)
packages/protocol/src/index.tsTASKS_UPDATEDconst +TaskRow+TasksUpdatedFrame+ union extensionapps/kernel/src/broadcasts.tsbroadcastTasksUpdatedhelperapps/kernel/src/kernel.tsapps/kernel/src/tools/tasks.tsapps/kernel/src/tools/spawn-sub-agent.tsapps/cli/src/store/index.tstasksslice +setTasksactionapps/cli/src/protocol/handle-frame.tsTASKS_UPDATEDdispatchapps/cli/src/components/TaskPanel.tsxapps/cli/src/app.tsxapps/cli/src/components/tools/SpawnSubAgentTool.tsxapps/cli/src/components/tools/registry.tsspawn_sub_agentapps/cli/src/hooks/use-app.tsuseSetTasksselector hookdocs/superpowers/specs/2026-05-15-tui-tasks-and-subagent-design.mddocs/superpowers/plans/2026-05-15-tui-tasks-and-subagent.mdNon-goals (deferred)
Subtle wins worth noting
TASKS_UPDATEDframes. Only TaskPanel re-renders (useStore((s) => s.tasks)).setTasks([])on thread switch prevents a brief stale-panel flicker during the WS reconnect window before the new thread's onConnect re-broadcast arrives.?glyph fallback for unknown task statuses — protects against future status enum additions reaching the TUI before the map is updated.errorTextandstate === "output-error"— review caught the bare-errorText-only check;format.tsalready treats them as equivalent.Deployed
Version
579ce76f-ea82-490b-a6f9-f87b1d350d47athttps://agent-os.pandaronit25.workers.dev.Spec:
docs/superpowers/specs/2026-05-15-tui-tasks-and-subagent-design.mdPlan:
docs/superpowers/plans/2026-05-15-tui-tasks-and-subagent.md🤖 Generated with Claude Code