# 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:

```sql
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_provider` → `with_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.
