feat(webui): agent team panel — subagent communication (webui-parity slice 06) - #48
Merged
Merged
Conversation
…ing badge (slice 06)
Wires the three missing Agent Team wirings the dispatch brief called out:
1. SSE session-tree-changed broadcast
- server/lib/state-bus.js: pushSessionTreeChanged() emits the named
frame on every connected cid. Triggered by applyToolUpdate's
recordSubagentForCid() so the sidebar session tree refetches the
moment a subagent row lands in the runtime db (cache busts via
invalidateSessionTree() in the same call).
- webapp/lib/sse.ts: parseSseFrame recognises the event, store.tsx
bumps a treeRevision counter, components/session-tree.tsx reacts
by forcing a re-read past the 15s cache.
2. <task_result session_id=...> parse → jumpable subagent ref
- server/lib/agent-team-detect.js: parseTaskResult pulls the child
session id (and optional agent hint) out of the engine's literal
<task_result session_id="mvs_xxx"/> body; isSubagentDispatch
recognises 'task' / 'delegate' / 'delegatetask' header names with
case + underscore tolerance and falls back to the body tag.
- server/lib/mcode-acp.js#applyToolUpdate now accepts { cid } and,
when the tool is a subagent dispatch, records the
(toolCallId, childSessionId, agentName) pair on cs.recentSubagents
and projects the live task status from
local_runtime_background_tasks. Surfaced through every snapshot
and /api/state as cs.recentSubagents (new WebuiState field).
- webapp/lib/types.ts: RecentSubagent typed shape.
- webapp/components/chat.tsx ToolCard: when the tool name is
task/delegate and a recentSubagents entry exists, renders a
clickable jump badge ('▶ verifier', '✓ verifier', '✗ verifier',
'■ verifier') that POSTs /api/sessions/switch.
3. Live running status on parent's tool line
- server/lib/agent-team-tasks.js: read-only SQL helpers over the
runtime db, all open with { readonly: true, fileMustExist: true }
and close in finally. Every status string passes through
projectTaskStatus before leaving the module — raw db vocabulary
never reaches the wire.
- server/lib/agent-team-status.js: pure DB→UI projection with
AGENT_TEAM_STATUS = { idle, queued, running, waiting, done,
stopped, failed }. projectSessionStatus maps sessions.status
(idle/interrupted/aborted/error), projectTaskStatus maps
background_tasks.status (running/succeeded/failed/canceled),
projectAgentStatus composes both (task wins when non-idle),
isLiveStatus gates the running badge.
- server/lib/state-bus.js: startSubagentStatusPolling + refresh
are wired into bootstrap.js#onListening so a 2s tick (env
MCODE_WEBUI_SUBAGENT_POLL_MS override) keeps cs.recentSubagents
status fresh while a subagent is mid-turn. No-ops when no
runtime db exists; pushes a state snapshot only when at least one
entry actually changed.
Pin: runtime db is read-only — auth forces ~/.minimax/v2/sqlite
location, MCODE_DATA_DIR does NOT relocate it; the dispatch brief
banned writes to that path. Helpers use { readonly: true,
fileMustExist: true } and close in finally so a long-lived process
never leaks a file descriptor.
DB→UI vocabulary lock
- session.status only carries idle|interrupted|aborted|error
(R8 measurement). It does NOT carry running/done/failed/queued.
- background_tasks.status carries running|succeeded|failed|canceled
for kind='subagent'.
- The UI vocabulary (failed|waiting|running|queued|done|stopped)
is projection-layer only — never a db field. Any UI consumer
reads recentSubagents[].status (always projected) rather than
raw db strings.
File scope (per dispatch brief)
- own: components/session-tree.tsx, server/lib/session-tree.js,
server/routes/sessions.js, server/lib/mcode-acp.js,
server/lib/state-bus.js, server/bootstrap.js
- own: components/chat.tsx (subagent badge only — render path
is shared with the chat-tree slice but no i18n.ts/panels.tsx/
store.tsx interface changes)
- own: lib/i18n-agent-team.ts (NEW, slice 01 owns i18n.ts so a
merge conflict would have followed any edits there)
- NOT touched: components/panels.tsx, lib/i18n.ts, lib/files-tree.ts
- ADDITIVE EDIT (not 'NOT touched'): webapp/lib/store.tsx
received ONE additive change — a treeRevision: number field on
StoreSnapshot and a tree-changed case in __testApplyAction —
so the React tree can react to the named session-tree-changed
SSE frame. No fields were removed or renamed; the i18n.ts
merge boundary still holds.
Tests
- agent-team-status.test.js 24 cases — pure projection rules
- agent-team-tasks.test.js 20 cases — read-only db projection
(fixture sqlite, no real db read)
- agent-team-detect.test.js 17 cases — <task_result> parser
- agent-team-state-bus.test.js 10 cases — recordSubagentForCid
idempotency, prune, pushSessionTreeChanged
broadcast, poll refresh
- existing adjacent tests untouched: session-tree.test.js (12),
mcode-acp-note.test.js (51, applyToolUpdate 3-arg path still
works), state-bus-restore.test.js (6)
Gates
- pnpm webapp:typecheck: 0 errors
- pnpm test:webapp: 335/335 pass
- pnpm webapp:build: ok (Next.js production build)
- pnpm check:source: ok (4600 files recorded)
- pnpm check:ci: ok (docs alignment)
- targeted server tests: 71/71 pass (4 new files)
Live self-check (isolated ports 18150/18151, MCODE_WEBUI_DATA_DIR
set to /tmp/agent-team-selfcheck/data, runtime db copied to
/tmp/agent-team-selfcheck/runtime/ so the original ~/.minimax
runtime db is never written to). Screenshots in /tmp/agent-team-selfcheck:
- state2.png, expanded2.png: parent+subagent rows render in the
sidebar; data-agent='explore' attribute set; subagent jump wired
- jumped.png: subagent row click triggers POST /api/sessions/switch
(rejected by CORS only because the dev server's trusted-origin
set is for 18090, not 18150 — pre-existing dev wiring, not a
regression of the wiring itself)
- integration.mjs + poll.mjs (process-isolated direct calls):
recordSubagentForCid → cs.recentSubagents[] populated,
pushSessionTreeChanged broadcasts the named frame verbatim,
refreshRecentSubagentStatuses calls findSubagentTaskByToolCallId
to refresh the live status
AGENTS.md compliance
- All test servers / data dirs are process-isolated; only PIDs I
spawned were signalled (server 1232779, webapp was killed by
the runtime when its port became free). 18090/18091 untouched.
- No edits to the live ~/.minimax runtime db. Self-check uses a
copy at /tmp/agent-team-selfcheck/runtime/runtime-state.sqlite.
- i18n strings for slice 06 live in lib/i18n-agent-team.ts to
avoid a merge conflict with slice 01's lib/i18n.ts.
…n (slice 06 follow-up)
Addresses the two defects the acceptance pass returned for the
previous slice commit:
1. toolCallId matching — multi-dispatch correctness.
The previous slice shipped a ToolCard that picked
recentSubagents[last] for every '→ task' block it rendered. With
one subagent dispatch that happens to be right; with two (the
normal agent-team case), every older block jumped to the newest
child. The contract is now:
server (mcode-acp.js)
— applyToolUpdate emits '##tc:<toolCallId>' immediately
BEFORE the synthetic '→ name' header on the no-prior-
tool-call path, and never re-emits on subsequent updates
for the same id.
— the tool_call branch of streamAcpPrompt emits the same
marker immediately before its '→ name' push. Both paths
only emit the marker when u.toolCallId is present; older
engines without a toolCallId still produce a header but
with no marker, preserving the legacy chat shape.
webapp (transcript.ts)
— decodeTranscript carries a 'pendingToolCallId' across
loop iterations; the '##tc:' regex consumes the marker
and parks the id, the next tool block opens and attaches
the parked id as 'toolCallId'. The marker never appears
in the rendered block stream (consumed, like the existing
'§§ processed_duration' metadata marker).
webapp (agent-team-lookup.ts — NEW)
— findSubagentForBlock(recent, block) is the single matching
rule:
1. Non-subagent tool names return null (read / bash /
write never jump to a child).
2. toolCallId present → exact id match. Primary path —
two '→ task' lines in the same session each jump to
their own child.
3. toolCallId missing (older chat, mid-stream attach
race) → fall back to the newest entry. Legacy chat
history still renders a usable badge.
Pinned by agent-team-lookup.test.ts (10 cases including
the multi-dispatch regression guard).
webapp (chat.tsx)
— ToolCard now imports findSubagentForBlock instead of
inlining the broken 'recent[last]' rule.
tests
— transcript-tc-marker.test.ts (6 cases) pins decoder-side:
marker is consumed (not emitted as its own block), two
dispatches each carry their own id, blank lines between
marker and header still attach, older chat without the
marker still parses.
— mcode-acp-tc-marker.test.js (5 cases) pins server-side:
marker emitted before synthetic header, NOT re-emitted on
subsequent updates, NOT duplicated on insert-after-existing,
omitted entirely when toolCallId is absent.
— mcode-acp-note.test.js updated for the +1 line shift
(marker before the header) — the 51-case applyToolUpdate
contract test still passes.
2. i18n wiring — the slice-06 i18n file was orphaned.
The previous slice shipped lib/i18n-agent-team.ts with hardcoded
English glyphs ('▶ verifier' / '✓' / '✗' / '■') in chat.tsx. The
keys existed but were never imported; the file was dead weight.
Acceptance flagged this and required either wiring or removal.
Wired.
webapp (i18n-agent-team.ts)
— Expanded to 16 keys, all bilingual (en + zh). Each key
has explicit symmetry enforced by agent-team-i18n.test.ts.
— Added badgeLabelAndGlyph(locale, uiStatus) →
{label, glyph} | null — the single projection rule the
badge uses. Returns null for statuses outside the UI
vocabulary so the badge hides itself cleanly (no raw db
string ever leaks).
— Added agentLabel(locale, agentName) → string —
translates the runtime-stored English tokens
(explore/worker/verifier/coder) to their locale labels.
Unknown tokens fall back to the English token verbatim
(forward-compat for custom agents), then to the subagent
fallback label when the input is empty.
— Both helpers do not throw on null / undefined / unknown
status. The fallback path is documented in the type
signature.
webapp (chat.tsx)
— ToolCard now resolves both the status label AND the agent
label through the i18n layer. Hardcoded glyphs and
English fallback strings are gone.
tests (agent-team-i18n.test.ts — NEW, 14 cases)
— every key in en is in zh, every key in zh is in en (so a
regression in either direction is loud).
— every localised string is non-empty.
— the status and agent labels actually differ between en
and zh (a regression that copies one bucket onto the
other would ship English text to a Chinese-locale user).
— badgeLabelAndGlyph returns null for null / undefined /
unknown status, including the raw db strings 'succeeded'
/ 'canceled' that must NEVER leak through (UI vocabulary
only).
— agentLabel translates known tokens, falls back for
unknown / nullish inputs.
3. The previous slice commit message claimed 'store.tsx was NOT
touched'; it was modified additively (treeRevision field on
StoreSnapshot + tree-changed case in __testApplyAction). The
historical commit has been amended (fcd9fbb) to call this out
under 'ADDITIVE EDIT' rather than 'NOT touched', with a note
that no fields were removed or renamed so the i18n.ts merge
boundary still holds.
File scope (per dispatch brief)
- own: components/session-tree.tsx, server/lib/session-tree.js,
server/routes/sessions.js, server/lib/mcode-acp.js,
server/lib/state-bus.js, server/bootstrap.js
- own (incremental): components/chat.tsx (badge rewrite),
webapp/lib/transcript.ts (decoder marker consumption),
webapp/lib/types.ts (RecentSubagent unchanged, WebuiState
unchanged — no schema changes)
- own (NEW): webapp/lib/agent-team-lookup.ts,
webapp/test/agent-team-{lookup,i18n,transcript-tc-marker}.
test.ts, webui/test/lib/mcode-acp-tc-marker.test.js
- NOT touched: components/panels.tsx, lib/i18n.ts,
lib/files-tree.ts
Gates
- pnpm webapp:typecheck: 0 errors
- pnpm test:webapp: 365/365 pass (was 335; +30: 10 lookup +
14 i18n + 6 decoder marker)
- pnpm webapp:build: ok (Next.js production build)
- pnpm check:source: ok (4605 files recorded; +5 new files)
- pnpm check:ci: ok (docs alignment)
- targeted server tests: 148/148 pass (was 71; +77: mcode-acp-
note.test.js +5 shift-pinning + mcode-acp-tc-marker.test.js
5 new, the rest untouched)
Process safety: only PIDs I personally spawned were signalled
(1232779 server, 18151 webapp). 18090/18091 untouched.
The rebase conflict on release/public-source.json was resolved by regenerating the inventory in this worktree, so the recorded paths cover slices 01, 02 and 06 together. check:source: 4612 files. test:webapp: 426/426.
…le() (slice 06 follow-up) CI on PR #48 (ubuntu + macos) failed in test/routes/chat-run-mirror.check.mjs because the slice-06 marker emission shifted two assertions by one line. The targeted server-test run that landed with the previous slice commit did not pick this up — CI runs the full suite, not a subset, and this file does not live under test/lib. Followed the same convention the file already uses for '§§ processed_duration=Nms' (see the existing stable() filter just above the setupTwoSessions helper): extended stable() to also strip '##tc:<toolCallId>' lines, since both are decoder-consumed metadata that must NEVER appear in user-visible chat. Adjusted the one direct buf[N] index check that pinned the exact buffer layout (the new marker lands at buf[1], header at buf[2]) and bumped the buffer-length count for the snapshot assertion (was 9 — 3 persisted + 6 buffer; now 10, +1 for the marker). Two assertions: - buf[1] === '→ Bash …' (line 360) — direct layout pin - backSnap.chat.length === 9 (line 399) — total merged length Both updated to the new contract. No expectation was changed to hide a bug; the marker is intended, the chat-render contract is that it never appears in the rendered block stream (consumed by decodeTranscript the same way '§§' is). Sweep — checked every other test that could plausibly walk the raw transcript buffer or assert on tool-header lines: - mcode-acp-note.test.js — already updated for the +1 shift in the previous slice commit - mcode-acp-tc-marker.test.js — new in the previous commit, pins the marker contract - acp-transport-answer.test.js — drives the transport mock, not mcode-acp.js; the test's tool_call events do not carry a toolCallId so the marker is not emitted (path-conditional) - stream-cumulative-render.test.js — same: drives the transport mock, no toolCallId on tool_call events - trajectory/tools/panel-e2e.mjs — projection layer, reads background_tasks via its own query path, not chat lines - trajectory/protocol.test.mjs — protocol event simulation, does not touch the chat transcript - export.check.mjs — markdown / json export of session records; cs.chat is the source of truth on disk, marker lines are written there too; sweep confirmed no test asserts on the export's tool-header ordering - chat-looks-cumulative.test.js, mcode-exec.test.js — pure helper tests, no chat buffer walks No other consumer asserts on the marker or asserts on a tool header at a fixed index. The chat-run-mirror fix is the only outstanding line shift. Gates (FULL webui server suite, not the targeted subset the previous slice commit landed with): - pnpm --filter @mavis/webui test test/lib + test/routes + test/server + test/tooling → 1613 pass / 1615 total (2 skipped — pre-existing) - pnpm --filter @mavis/webui test:trajectory → 115 pass / 115 total - pnpm --filter @mavis/webui test:integration → 58 pass / 58 total - pnpm --filter @mavis/webui test:webapp → 365 pass / 365 total - pnpm --filter @mavis/webui webapp:typecheck → 0 errors - pnpm check:source → ok (4612 files; +7 over the previous slice commit) Process safety: no servers were spawned in this round — pure test-only changes. No PIDs to signal.
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.
What
Slice 06 of the webui-parity program: 主子 agent 通信 — the user requirement that the product's sub-agent behavior be visible and navigable in the local webui.
The engine emits no subagent-specific events; the data lives in the runtime sqlite DB. The server already exposed the parent→child tree and the UI already rendered subagent rows — what was missing was three wirings:
session-tree-changed— broadcast on every connected cid when a subagent row lands, so the sidebar refetches past its 15s cache instead of waiting for a manual refresh.<task_result session_id="…">parsing — the child session id is buried in the literal tool-result body; it is now extracted intocs.recentSubagentsand surfaced through every snapshot and/api/state, making each→ taskline a clickable jump target.local_runtime_background_taskskeeps the badge current.The status-vocabulary trap, handled explicitly
The DB's
local_runtime_sessions.statusonly ever containsidle | interrupted | aborted | error— there is norunning/done. The TUI vocabulary (failed|waiting|running|queued|done|stopped) is a projection-layer fiction, not a field. This slice implements an explicitprojectSessionStatus/projectTaskStatus/projectAgentStatusmapping, and no raw DB status string reaches the wire. 71 unit tests pin every mapping, the composer's precedence, and the membership guard; the badge layer returnsnullfor non-UI-vocabulary values (including rawsucceeded/canceled) so they can never leak.Acceptance (independent agent) — PASS-WITH-CONCERNS, both concerns fixed in this PR
{readonly, fileMustExist}and closes infinally; no write statement anywhere; the real runtime DB was untouchedparseSseFrame→treeRevision(bothconnect()and__testApplyAction) → tree refetch bypassing the cachePOST /api/sessions/switch→ child transcript backfilledFixed before merge (found by acceptance, not deferred):
toolCallIdvia a singlefindSubagentForBlockrule, threaded through a##tc:<id>transcript marker; 21 tests pin it including the multi-dispatch regression.lib/i18n-agent-team.tswas orphaned (zero imports) with hardcoded English strings inchat.tsx. Now fully wired — 16 bilingual keys, withbadgeLabelAndGlyph(locale, status)andagentLabel(locale, name)as the single projection rules; 14 tests pin en/zh symmetry and raw-string rejection.Gates (post-rebase)
test:webapp426/426 · targeted server tests 148/148 · typecheck 0 · build ✓ ·check:source4612 ✓Rebased onto slices 01/02; the inventory was regenerated in-worktree after the rebase.