Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9423c01
feat: add dedicated subagent view to TUI
avoidwork Oct 11, 2026
c5dc6e4
feat: add dedicated subagent view to TUI
avoidwork Oct 11, 2026
b4bb8b1
chore: archive tui-subagents-panel change
avoidwork Oct 11, 2026
7944cda
merge: resolve coverage.txt conflict with origin/main
avoidwork Oct 11, 2026
47f30c7
fix: route orchestrator output to conversation, not subagent store
avoidwork Oct 11, 2026
24a7741
fix: use tools: namespace prefix to discriminate subagent events
avoidwork Oct 11, 2026
126bd3d
fix: keep orchestrator tool calls in conversation view
avoidwork Oct 11, 2026
02bd2c9
feat: add /subagents slash command to reach the SUBAGENTS panel
avoidwork Oct 11, 2026
350a07f
fix: allow Escape to return to conversation from SUBAGENTS panel
avoidwork Oct 11, 2026
9496f00
fix: destructure onViewChange prop in SubagentsPanel
avoidwork Oct 11, 2026
3683b34
fix: keep ConversationArea mounted across panel views
avoidwork Oct 11, 2026
304866d
fix: preserve conversation state across panel view round-trip
avoidwork Oct 12, 2026
1e4d25a
fix: skip interrupt for view-switching slash commands
avoidwork Oct 12, 2026
80119bc
fix: coalesce subagent streaming segments into continuous blocks
avoidwork Oct 12, 2026
7fe4bff
feat: make ctrl+r/ctrl+t collapse toggles work in the subagents view
avoidwork Oct 12, 2026
d6d8888
feat: add live autoscroll to the subagents view
avoidwork Oct 12, 2026
cd71921
fix: lift subagent store to App so the panel updates live
avoidwork Oct 12, 2026
fa7856c
fix: give subagent entries an id so the virtualized panel renders
avoidwork Oct 12, 2026
c68c09e
fix(tui): render subagent segments with collapse/expand parity
avoidwork Oct 12, 2026
b9d36f9
fix(tui): render subagent tool calls with wrench glyph and lowercase …
avoidwork Oct 12, 2026
cdccbf4
feat(tui): make subagents panel a read-only expanded view
avoidwork Oct 13, 2026
edb0de0
merge: resolve conflicts with main
avoidwork Oct 13, 2026
901812f
style(tui): collapse subagents panel props to satisfy oxfmt
avoidwork Oct 13, 2026
f1c6f67
style(tests): annotate console.error in mcp-server fixture
avoidwork Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 58 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,12 +226,35 @@ async function callProvider(_name, _providerConfig, message, streamingCallback,
messages: [new HumanMessage(message)],
};

for await (const [_namespace, mode, payload] of await agent.stream(input, {
for await (const [namespace, mode, payload] of await agent.stream(input, {
...config,
...options,
streamMode: ["messages", "tools"],
subgraphs: true,
})) {
// With `subgraphs: true`, the root graph yields a namespace like
// `["model_request:<uuid>"]` and each subagent subgraph yields a
// namespace containing a `tools:` segment (e.g.
// `["tools:call_abc123", "model_request:<uuid>"]`). The orchestrator's
// own output has NO `tools:` segment, so a `tools:` prefix check is the
// correct discriminator (mirrors the deepagents library's own
// `isSubagentNamespace`). Subagent events are emitted with a
// `subagent_` prefix and carry the namespace so the TUI can route them
// to a dedicated SUBAGENTS store instead of folding them into the
// orchestrator's assistant bubble.
//
// However, the orchestrator's OWN tool calls are also emitted under a
// `tools:` namespace (e.g. `["tools:<id>"]` — length 1). A subagent
// subgraph always adds a second segment (a nested `model_request:` or
// `tools:`), so a namespace is subagent-scoped only when it has a
// `tools:` segment AND more than one segment. This keeps the
// orchestrator's tool calls in the conversation view while routing
// subagent activity to the SUBAGENTS store.
const isSubagent =
Array.isArray(namespace) &&
namespace.length > 1 &&
namespace.some((part) => part.startsWith("tools:"));

if (mode === "messages") {
const [msg] = payload;
const msgType = msg?._getType ? msg._getType() : msg?.type;
Expand All @@ -244,36 +267,59 @@ async function callProvider(_name, _providerConfig, message, streamingCallback,
// is false, tool text is skipped entirely.
if (msgType === "tool") {
if (showToolResults !== false && streamingCallback) {
streamingCallback({ type: "tool", text, name: msg?.name });
streamingCallback({
type: isSubagent ? "subagent_tool" : "tool",
text,
name: msg?.name,
namespace,
});
}

continue;
}

collectedContent += text;
if (!isSubagent) {
collectedContent += text;
}

if (streamingCallback) {
streamingCallback({ type: "message", text });
streamingCallback({
type: isSubagent ? "subagent_message" : "message",
text,
namespace,
});
}
}

// Capture reasoning content from additional_kwargs (Chat Completions API)
const reasoningContent =
msg?.additional_kwargs?.reasoning_content ?? msg?.additional_kwargs?.reasoning;
if (reasoningContent) {
collectedReasoning += reasoningContent;
if (!isSubagent) {
collectedReasoning += reasoningContent;
}
if (streamingCallback) {
streamingCallback({ type: "reasoning", text: reasoningContent });
streamingCallback({
type: isSubagent ? "subagent_reasoning" : "reasoning",
text: reasoningContent,
namespace,
});
}
}

// Capture reasoning from content blocks (Responses API / block format)
if (Array.isArray(msg?.content)) {
for (const block of msg.content) {
if (block?.type === "reasoning" && block.reasoning) {
collectedReasoning += block.reasoning;
if (!isSubagent) {
collectedReasoning += block.reasoning;
}
if (streamingCallback) {
streamingCallback({ type: "reasoning", text: block.reasoning });
streamingCallback({
type: isSubagent ? "subagent_reasoning" : "reasoning",
text: block.reasoning,
namespace,
});
}
}
}
Expand All @@ -282,16 +328,18 @@ async function callProvider(_name, _providerConfig, message, streamingCallback,
// payload is StreamToolsOutput with event, name, etc.
if (payload?.event === "on_tool_start") {
streamingCallback({
type: "on_tool_start",
type: isSubagent ? "subagent_tool_start" : "on_tool_start",
name: payload.name,
data: { input: payload.input },
namespace,
});
}
if (payload?.event === "on_tool_end") {
streamingCallback({
type: "on_tool_end",
type: isSubagent ? "subagent_tool_end" : "on_tool_end",
name: payload.name,
data: { output: payload.output },
namespace,
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11
41 changes: 41 additions & 0 deletions openspec/changes/archive/2026-10-11-tui-subagents-panel/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Context

The TUI currently renders all stream events into a single assistant bubble. When the orchestrator delegates via the `task` tool, the subagent's reasoning and message segments are flattened into the same bubble as the orchestrator's output because the stream namespace is discarded in `callProvider` (`index.js`). With `subgraphs: true`, `agent.stream()` yields `[namespace, mode, payload]` where the root graph has an empty namespace and each subagent subgraph has a populated namespace (e.g. `["task:<uuid>"]`).

## Goals / Non-Goals

**Goals:**
- Capture the stream namespace in `callProvider` so subagent activity is distinguishable from the root graph.
- Emit dedicated `subagent_*` event types carrying the namespace for subagent reasoning, message, and tool activity.
- Route `subagent_*` events to a dedicated subagent store in the TUI, not the orchestrator's assistant bubble.
- Add a `SUBAGENTS` panel that renders each subagent's reasoning/message segments and tool calls.
- Wire the panel into the App render switch.

**Non-Goals:**
- Not changing the orchestrator's conversation bubble behavior for root-graph events.
- Not adding new npm packages or dependencies.
- Not altering subagent delegation logic in `src/agent/deepAgents.js`.

## Decisions

### Decision 1: Emit subagent event types at the source (`callProvider`)
The namespace is only available at the `agent.stream()` iteration site in `callProvider`. By the time events reach the TUI handler, the namespace is lost. Therefore, the capture must happen in `callProvider`. When `namespace.length > 0`, emit `subagent_message`, `subagent_reasoning`, `subagent_tool_start`, `subagent_tool_end` carrying the namespace. The root graph (empty namespace) continues to emit the existing `message`/`reasoning`/`on_tool_*` event types.

**Alternatives considered:** Routing the namespace through the existing event types (e.g. adding a `namespace` field to `message`/`reasoning`). Rejected because it would require the TUI handler to branch on a field for every event and would not cleanly separate subagent tool activity from orchestrator tool activity.

### Decision 2: Route subagent events to a dedicated store in the TUI
In `createStreamingHandler` (`src/tui/conversationArea.js`), handle the new `subagent_*` event types by routing them to a dedicated subagent store keyed by namespace, not to `streamingMsgIdRef.current`. This keeps the orchestrator's assistant bubble clean while capturing subagent activity separately.

**Alternatives considered:** Folding subagent events into the same bubble with a namespace prefix. Rejected because it defeats the purpose of separating subagent activity from the orchestrator's conversation.

### Decision 3: Dedicated `SUBAGENTS` panel
Add `SUBAGENTS` to `PANELS` and register it in `getPanelOrder()`. Create `src/tui/subagentsPanel.js` rendering each subagent's reasoning/message segments and tool calls, reusing `MessageBubble` or a variant. This is consistent with the existing panel system (SKILLS/MEMORIES/SETTINGS/SESSIONS/PROJECTS).

**Alternatives considered:** Inline collapsible sections in the conversation (busier, harder to scan); a second always-visible message list (more complex layout). The dedicated panel provides clean separation and is consistent with the existing panel architecture.

## Risks / Trade-offs

- [Subagent events emitted before the subagent store is initialized] → The store is created lazily on first subagent event; events for unknown namespaces create a new entry.
- [Multiple subagents in one turn] → The store is keyed by namespace, so each subagent gets its own entry.
- [Nested subagent delegation] → Each subgraph has a distinct namespace, so nested subagents are captured separately.
- [Subagent tool calls with errors] → `subagent_tool_end` carries the namespace; the panel renders tool activity per subagent.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Why

When the orchestrator delegates work via the `task` tool, the subagent's reasoning and message segments are currently flattened into the same assistant bubble as the orchestrator's own output. The stream namespace that identifies which subgraph produced each event is discarded in `callProvider`, so there is no way to distinguish what the orchestrator said from what each subagent did. This makes multi-agent runs hard to follow and audit.

## What Changes

- Capture the stream namespace in `callProvider` (`index.js`) so subagent subgraphs can be distinguished from the root graph.
- When the namespace is populated (subagent activity), emit new event types (`subagent_message`, `subagent_reasoning`, `subagent_tool_start`, `subagent_tool_end`) carrying the namespace, instead of the flat `message`/`reasoning`/`on_tool_*` events.
- Keep the root graph (empty namespace) emitting the existing event types unchanged.
- Route `subagent_*` events in the TUI streaming handler to a dedicated subagent store keyed by namespace, not to the orchestrator's assistant bubble.
- Add a `SUBAGENTS` panel to the TUI that renders each subagent's reasoning/message segments and tool calls.
- Wire the `SUBAGENTS` panel into the App render switch.

## Capabilities

### New Capabilities
- `tui-subagents-panel`: A dedicated TUI panel that captures and renders each subagent's reasoning and message segments separately from the orchestrator's conversation, including subagent tool calls.

### Modified Capabilities
- `tui-streaming`: The streaming handler must route subagent events to a dedicated subagent store instead of the orchestrator's assistant bubble.

## Impact

- `index.js` — `callProvider` captures the stream namespace and emits subagent event types.
- `src/tui/conversationArea.js` — `createStreamingHandler` routes `subagent_*` events to a subagent store.
- `src/tui/panels.js` — `PANELS` enum gains `SUBAGENTS`; `getPanelOrder()` registers it.
- `src/tui/subagentsPanel.js` — new panel rendering subagent activity.
- `src/tui/app.js` — render switch gains a `SUBAGENTS` branch.
- `tests/unit/tui/subagentsPanel.test.js` — new unit tests.

## Non-goals

- Not changing the orchestrator's conversation bubble behavior for root-graph events.
- Not adding new npm packages or dependencies.
- Not altering subagent delegation logic in `src/agent/deepAgents.js`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## ADDED Requirements

### Requirement: Streaming handler SHALL route subagent events to a subagent store
The `createStreamingHandler` function SHALL handle `subagent_message`, `subagent_reasoning`, `subagent_tool_start`, and `subagent_tool_end` event types by routing them to a dedicated subagent store keyed by namespace, instead of updating `streamingMsgIdRef.current`. The orchestrator's assistant bubble SHALL continue to receive the existing `message`, `reasoning`, `on_tool_start`, `on_tool_end`, `tool`, and `on_tool_error` event types.

#### Scenario: Subagent message routed to subagent store
- **WHEN** a `subagent_message` event is received with a namespace
- **THEN** the event is routed to the subagent store for that namespace, not to `streamingMsgIdRef.current`

#### Scenario: Subagent reasoning routed to subagent store
- **WHEN** a `subagent_reasoning` event is received with a namespace
- **THEN** the event is routed to the subagent store for that namespace, not to `streamingMsgIdRef.current`

#### Scenario: Subagent tool start routed to subagent store
- **WHEN** a `subagent_tool_start` event is received with a namespace
- **THEN** the event is routed to the subagent store for that namespace, not to `streamingMsgIdRef.current`

#### Scenario: Subagent tool end routed to subagent store
- **WHEN** a `subagent_tool_end` event is received with a namespace
- **THEN** the event is routed to the subagent store for that namespace, not to `streamingMsgIdRef.current`

#### Scenario: Root graph events continue to update assistant bubble
- **WHEN** a `message` or `reasoning` event is received without a subagent namespace
- **THEN** the event continues to update `streamingMsgIdRef.current`
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## ADDED Requirements

### Requirement: callProvider SHALL capture the stream namespace
`callProvider` in `index.js` SHALL capture the `_namespace` from `agent.stream()` when iterating with `subgraphs: true`. When `namespace.length > 0`, it SHALL emit subagent event types carrying the namespace instead of the flat `message`/`reasoning`/`on_tool_*` events. The root graph (empty namespace) SHALL continue emitting the existing event types.

#### Scenario: Root graph emits existing event types
- **WHEN** `callProvider` iterates a stream event with an empty namespace (`[]`)
- **THEN** it emits the existing `message`, `reasoning`, `on_tool_start`, or `on_tool_end` event types

#### Scenario: Subagent emits subagent_message
- **WHEN** `callProvider` iterates a stream event with a populated namespace (e.g. `["task:<uuid>"]`) and mode `messages` with message text
- **THEN** it emits a `subagent_message` event carrying the namespace and text

#### Scenario: Subagent emits subagent_reasoning
- **WHEN** `callProvider` iterates a stream event with a populated namespace and reasoning content
- **THEN** it emits a `subagent_reasoning` event carrying the namespace and reasoning text

#### Scenario: Subagent emits subagent_tool_start
- **WHEN** `callProvider` iterates a stream event with a populated namespace and mode `tools` with `on_tool_start`
- **THEN** it emits a `subagent_tool_start` event carrying the namespace and tool name

#### Scenario: Subagent emits subagent_tool_end
- **WHEN** `callProvider` iterates a stream event with a populated namespace and mode `tools` with `on_tool_end`
- **THEN** it emits a `subagent_tool_end` event carrying the namespace and tool name

### Requirement: Subagent events SHALL carry the namespace
Each `subagent_*` event emitted by `callProvider` SHALL include the namespace that identifies the subgraph that produced it.

#### Scenario: Event carries namespace
- **WHEN** a `subagent_message` event is emitted
- **THEN** the event includes a `namespace` field equal to the populated namespace array

### Requirement: TUI streaming handler SHALL route subagent events to a subagent store
`createStreamingHandler` in `src/tui/conversationArea.js` SHALL handle `subagent_message`, `subagent_reasoning`, `subagent_tool_start`, and `subagent_tool_end` event types by routing them to a dedicated subagent store keyed by namespace, not to `streamingMsgIdRef.current`.

#### Scenario: Subagent message routed to subagent store
- **WHEN** a `subagent_message` event is received
- **THEN** the event is routed to the subagent store for its namespace, not to the orchestrator's assistant bubble

#### Scenario: Subagent reasoning routed to subagent store
- **WHEN** a `subagent_reasoning` event is received
- **THEN** the event is routed to the subagent store for its namespace, not to the orchestrator's assistant bubble

#### Scenario: Subagent tool start routed to subagent store
- **WHEN** a `subagent_tool_start` event is received
- **THEN** the event is routed to the subagent store for its namespace, not to the orchestrator's assistant bubble

#### Scenario: Subagent tool end routed to subagent store
- **WHEN** a `subagent_tool_end` event is received
- **THEN** the event is routed to the subagent store for its namespace, not to the orchestrator's assistant bubble

### Requirement: SUBAGENTS panel SHALL be registered in PANELS
`PANELS` in `src/tui/panels.js` SHALL include a `SUBAGENTS` entry, and `getPanelOrder()` SHALL include it in the tab navigation order.

#### Scenario: PANELS includes SUBAGENTS
- **WHEN** `PANELS` is inspected
- **THEN** it includes a `SUBAGENTS` entry

#### Scenario: getPanelOrder includes SUBAGENTS
- **WHEN** `getPanelOrder()` is called
- **THEN** the returned order array includes `PANELS.SUBAGENTS`

### Requirement: SUBAGENTS panel SHALL render subagent activity
`src/tui/subagentsPanel.js` SHALL render each subagent's reasoning/message segments and tool calls, keyed by namespace.

#### Scenario: Panel renders subagent message segments
- **WHEN** the SUBAGENTS panel is rendered with a subagent store containing a subagent's message segments
- **THEN** the panel renders the subagent's message content

#### Scenario: Panel renders subagent reasoning segments
- **WHEN** the SUBAGENTS panel is rendered with a subagent store containing a subagent's reasoning segments
- **THEN** the panel renders the subagent's reasoning content

#### Scenario: Panel renders subagent tool calls
- **WHEN** the SUBAGENTS panel is rendered with a subagent store containing a subagent's tool calls
- **THEN** the panel renders the subagent's tool call activity

### Requirement: App SHALL render the SUBAGENTS panel
The App render switch in `src/tui/app.js` SHALL include a `currentView === PANELS.SUBAGENTS` branch that renders the subagents panel, passing the subagent store.

#### Scenario: App renders SUBAGENTS panel
- **WHEN** `currentView === PANELS.SUBAGENTS`
- **THEN** the App renders the subagents panel component
25 changes: 25 additions & 0 deletions openspec/changes/archive/2026-10-11-tui-subagents-panel/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## 1. Capture the namespace in callProvider

- [x] 1.1 In `index.js` `callProvider`, capture `_namespace` from the `agent.stream()` iteration and emit `subagent_message`/`subagent_reasoning`/`subagent_tool_start`/`subagent_tool_end` events carrying the namespace when `namespace.length > 0`
- [x] 1.2 Keep the root graph (empty namespace) emitting the existing `message`/`reasoning`/`on_tool_start`/`on_tool_end` event types unchanged

## 2. Route subagent events in the TUI

- [x] 2.1 In `src/tui/conversationArea.js` `createStreamingHandler`, handle `subagent_message`, `subagent_reasoning`, `subagent_tool_start`, `subagent_tool_end` by routing them to a dedicated subagent store keyed by namespace, not `streamingMsgIdRef.current`

## 3. Add the SUBAGENTS panel

- [x] 3.1 Add `SUBAGENTS` to `PANELS` in `src/tui/panels.js` and register it in `getPanelOrder()`
- [x] 3.2 Create `src/tui/subagentsPanel.js` rendering each subagent's reasoning/message segments and tool calls, reusing `MessageBubble` or a variant

## 4. Wire the panel into App

- [x] 4.1 Add a `currentView === PANELS.SUBAGENTS` branch in the App render switch (`src/tui/app.js`) rendering the new panel, passing the subagent store

## 5. Write unit tests

- [x] 5.1 Add `tests/unit/tui/subagentsPanel.test.js` covering namespace discrimination, event routing, and tool-call redirection

## 6. Verify

- [x] 6.1 Run `npm run test`, `npm run lint`, and `npm run coverage` to confirm everything passes
Loading
Loading