PicoBot/docs/superpowers/specs/2026-08-13-sub-agent-activity-webui-design.md

9.5 KiB

Sub-Agent Activity WebUI Design

Goal

Restructure the WebUI around sub-agent activity while moving sub-agent definition management into the settings page. Concretely:

  1. Move sub-agent definition management (CRUD, enable/disable, role prompt) out of the agents navigation item into a new "子代理" tab on the settings page, and enlarge the role-prompt editing area.
  2. Turn the agents navigation item into an activity monitor that shows running sub-agents and historical run results.
  3. Add a read-only, chat-like detail view for a single sub-agent run, including a full, incrementally streamed message transcript.
  4. Reduce the tasks page to scheduled-task information only; move the background-task listing into the sub-agent activity page.

Non-goals

  • No changes to sub-agent orchestration semantics (delegation, budgets, signals, run admission).
  • No WebUI routing framework; the detail view is an in-page sub-view.
  • No persistence of the main agent's chat history changes; the transcript work is scoped to sub-agent runs.

Backend

Schema (v9)

Add a new agent_run_messages table for incrementally streamed sub-agent transcripts:

CREATE TABLE IF NOT EXISTS agent_run_messages (
    id                 TEXT PRIMARY KEY,
    run_id             TEXT NOT NULL,
    seq                INTEGER NOT NULL,
    role               TEXT NOT NULL,
    content            TEXT NOT NULL,
    reasoning_content  TEXT,
    tool_call_id       TEXT,
    tool_name          TEXT,
    tool_calls_json    TEXT,
    created_at         INTEGER NOT NULL,
    FOREIGN KEY (run_id) REFERENCES agent_runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_agent_run_messages_run_seq ON agent_run_messages(run_id, seq);
  • Bump SCHEMA_VERSION from 8 to 9 in src/storage/mod.rs.
  • Add the table DDL to src/storage/agent_run.rs::AGENT_SCHEMA_STATEMENTS.
  • Version-gate the existing agent-table drops. migrate_schema currently drops agent_runs/agent_inbox_events/agent_run_groups unconditionally whenever current < SCHEMA_VERSION; on a v8→v9 upgrade this would destroy run history. Gate those drops (and the background_tasks drop) on current < 8 — the batch-group removal is a pre-v8 concern — so a v8→v9 upgrade only adds the new table and preserves existing runs. When the drops do run (pre-v8), drop agent_run_messages before agent_runs so foreign-key enforcement never blocks the implicit row delete.
  • Update the v8 schema test that hardcodes assert_eq!(version, 8) (fresh_database_creates_schema_v8_agent_tables) to v9 and rename it accordingly.

We deliberately do not add a transcript column to agent_runs. Incremental append rows in agent_run_messages are the single source of truth for transcripts.

Incremental capture

  • Add an optional sink to AgentLoop:
    • field transcript_sink: Option<tokio::sync::mpsc::UnboundedSender<ChatMessage>>
    • builder with_transcript_sink(sender) -> Self
    • at every site where a message is appended to emitted_messages (assistant messages and tool result messages), forward a clone to the sink. Convert append_steering_messages from an associated function to a method so it can forward too (sub-agents never use steering, but the sink must be consistent for future use).
  • src/agent/sub_agent.rs::execute_resolved:
    • create an mpsc::unbounded_channel
    • pass the sender via build_sub_agent_with_providerwith_transcript_sink
    • spawn a writer task that owns the receiver and a seq counter; for each message it strips provider_state (set to None) and does INSERT INTO agent_run_messages
    • restructure so the sender is dropped and the writer task is awaited in every exit path, including the tokio::select! cancellation arm that currently returns early without process_with_context returning; only after the writer drains does the terminal commit run
    • the writer uses self.storage (the manager already holds Option<Arc<Storage>>); if storage is absent, the writer becomes a no-op

The task prompt is not duplicated into the table; the detail view renders run.task as the leading user bubble.

Storage API

Add to src/storage/agent_run.rs:

  • append_agent_run_message(run_id, seq, &message, now) -> Result<()> — single-row insert.
  • list_agent_run_messages(run_id, limit) -> Result<Vec<AgentRunMessageRecord>> — ordered by seq. The transcript is naturally bounded: one run emits at most a handful of messages per tool iteration and iterations are capped by the definition's limits.max_iterations (default 99); default limit of 10_000 is a generous ceiling, not a pagination contract. get_agent_run uses this same default limit when loading the transcript.

Two distinct types to avoid a storage/protocol collision:

  • AgentRunMessageRecord (storage, in src/storage/agent_run.rs): carries the raw columns — id, run_id, seq, role, content, reasoning_content, tool_call_id, tool_name, tool_calls_json, created_at.
  • AgentTranscriptMessage (protocol, in src/protocol.rs): the serialized shape — same fields except tool_calls_json is parsed into Vec<providers::ToolCall>; a From<AgentRunMessageRecord> impl performs the parse.

HTTP API

  • GET /api/agent-runs/{id} currently returns { "run": AgentRunView }. Extend the response to { "run": ..., "session_id": ..., "transcript": [ ... ] } where:
    • session_id is run.root_session_id (the AgentRunView deliberately omits it, so it is added at this endpoint's response level)
    • transcript is the ordered list of AgentTranscriptMessage rows (exposing reasoning_content but never provider_state)
  • GET /api/agent-runs/{id}/events is unchanged (signals/completions).
  • GET /api/tasks is unchanged and already lists all runs in any status; the activity page consumes it.

Frontend

Settings page (SettingsPage.svelte)

  • Add a "子代理定义" tab to the existing vertical Tabs (named to disambiguate from the "子代理" activity nav item). Move the definition list and the editor modal from AgentsPage.svelte here verbatim, then:
    • enlarge the role-prompt textarea (min-height ~360px, full-width, monospace)
    • widen the editor modal (min(920px, 94vw)) and put the role-prompt field on its own row
    • keep the existing API calls (/api/agents, /api/agents/options, POST/DELETE) unchanged

Sub-agent activity page (AgentsPage.svelte, rewritten)

  • List view:
    • "活动中" section: runs in queued/running/waiting_children status, with a pulse indicator, agent_id, prompt excerpt, mode/depth, and elapsed time.
    • "历史活动" section: terminal runs (completed/failed/timed_out/cancelled/interrupted), newest first, with StatusBadge, agent_id, prompt excerpt, tool_calls_count/iterations, timestamps, and a "详情" action.
    • Poll GET /api/tasks?limit=200 every 5s while mounted.
  • Detail view (in-page sub-view, back button, read-only):
    • metadata header: agent_id, StatusBadge, provider/model, mode, depth, tool_calls_count, iterations, session_id, started_at/finished_at, duration, parent_run_id if present
    • leading "task" bubble from run.task
    • transcript messages rendered like the chat page: Markdown for content, collapsible reasoning block, ToolCallCard for tool calls. Pair each assistant message's tool_calls with its tool-role result by tool_call_id (mirroring ChatPage's toolResult()), so calls and results remain independently collapsible.
    • signal cards from GET /api/agent-runs/{id}/events (same rendering as the chat page's agent-event cards)
    • final error card on failure
    • while status is non-terminal, poll GET /api/agent-runs/{id} + /events every 2s to stream the growing transcript; stop polling on terminal status
  • No composer, no input, no interactive actions other than navigation/collapse.

Tasks page (TasksPage.svelte)

  • Remove the "后台任务" tab and the Tabs wrapper; keep only the scheduled-job list (jobs + runs dots), which already comes from /api/jobs and /api/jobs/{id}/runs.

Navigation (App.svelte)

  • Update the agents page description to reflect activity monitoring ("查看活动中的子代理与历史运行"); keep the nav label "子代理".
  • Update the tasks page description to scheduled tasks only ("管理定时任务").

Error handling

  • Missing/empty transcript: detail view renders the task bubble + metadata + error (or a "无转录" placeholder for failed runs).
  • Storage write failures in the transcript writer are logged and the run still commits its terminal status; a broken transcript never fails the run.
  • API auth/5xx reuse the existing api() error handling and notify toast path.

Testing

  • Rust: v9 migration from a v8 database preserves existing agent_runs rows (version-gated drops), and the pre-v8 legacy rebuild path still works; append_agent_run_message + list_agent_run_messages round-trip with seq ordering; provider_state is stripped from persisted transcript rows; sink drains on success, timeout, and cancellation before terminal commit (including the early-return cancellation arm); get_agent_run returns the transcript and session_id; AgentRunView list responses remain transcript-free; the v8 hardcoded schema-version assertion is updated to v9.
  • Run cargo test --lib and cargo clippy --all-targets --all-features -- -D warnings.
  • Frontend: cd webui && npm run check && npm run build, then cargo build to verify the OUT_DIR embedding path. No external runtime dependency; the browser stays on /ws and the existing HTTP endpoints.