Skip to content

feat(cli,kernel): TUI task panel + sub-agent renderer (PR-E)#18

Merged
rtpa25 merged 10 commits into
mainfrom
feat/tui-tasks-subagent
May 16, 2026
Merged

feat(cli,kernel): TUI task panel + sub-agent renderer (PR-E)#18
rtpa25 merged 10 commits into
mainfrom
feat/tui-tasks-subagent

Conversation

@rtpa25

@rtpa25 rtpa25 commented May 15, 2026

Copy link
Copy Markdown
Owner

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

  • TaskPanel above input — compact one-line-per-task list with status glyphs (☐ pending, ▶ in_progress, ✓ complete, ✗ failed, ⊘ cancelled). Hidden when zero tasks. Mounted in the live region between in-flight streaming and the input row.
  • SpawnSubAgentTool custom renderer — state machine: in-flight (🤖 + elapsed timer + prompt) → collapsed completed (✓ + ~tok) → focus-to-expand bordered finalText box → failed state (errorText or output-error). Model badge color-coded (opus=magenta, sonnet=blue, haiku=green). manage_tasks deliberately stays on GenericTool — the panel is the canonical surface.

Transport

New TASKS_UPDATED protocol frame, push-payload (not refetch). Three broadcast triggers:

  • kernel.onConnect — seeds panel state for new connections / thread resumes
  • manage_tasks execute — after DB mutations
  • spawn_sub_agent — after writing task.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)

Side File Change
Protocol packages/protocol/src/index.ts TASKS_UPDATED const + TaskRow + TasksUpdatedFrame + union extension
Kernel apps/kernel/src/broadcasts.ts broadcastTasksUpdated helper
Kernel apps/kernel/src/kernel.ts onConnect: seed panel state
Kernel apps/kernel/src/tools/tasks.ts post-execute: broadcast
Kernel apps/kernel/src/tools/spawn-sub-agent.ts post-result-write: broadcast
TUI apps/cli/src/store/index.ts tasks slice + setTasks action
TUI apps/cli/src/protocol/handle-frame.ts TASKS_UPDATED dispatch
TUI apps/cli/src/components/TaskPanel.tsx new component
TUI apps/cli/src/app.tsx mount panel + clear tasks on thread switch
TUI apps/cli/src/components/tools/SpawnSubAgentTool.tsx new renderer
TUI apps/cli/src/components/tools/registry.ts register spawn_sub_agent
TUI apps/cli/src/hooks/use-app.ts useSetTasks selector hook
Docs docs/superpowers/specs/2026-05-15-tui-tasks-and-subagent-design.md spec
Docs docs/superpowers/plans/2026-05-15-tui-tasks-and-subagent.md plan

Non-goals (deferred)

  • Sub-agent streaming visibility (single-line elapsed indicator only)
  • Compaction-event scrollback indicator
  • Cost / token meter
  • Sub-agent transcript retro view
  • Interactive task editing from the TUI (read-only display)
  • Panel height cap (YAGNI — tasks per thread typically <10)

Subtle wins worth noting

  • Per-field Zustand selectors keep App silent during TASKS_UPDATED frames. 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.
  • Defensive ? glyph fallback for unknown task statuses — protects against future status enum additions reaching the TUI before the map is updated.
  • Failure branch catches both errorText and state === "output-error" — review caught the bare-errorText-only check; format.ts already treats them as equivalent.

Deployed

Version 579ce76f-ea82-490b-a6f9-f87b1d350d47 at https://agent-os.pandaronit25.workers.dev.

Spec: docs/superpowers/specs/2026-05-15-tui-tasks-and-subagent-design.md
Plan: docs/superpowers/plans/2026-05-15-tui-tasks-and-subagent.md

🤖 Generated with Claude Code

rtpa25 and others added 10 commits May 15, 2026 15:34
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>
@rtpa25 rtpa25 merged commit 7be523d into main May 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant