feat(web): render AgentSwarm as an inline tool card - #1425
Conversation
Replace the bottom SwarmCard footer and the messagesToTurns live-skip with one dedicated inline tool card for AgentSwarm. The card shows a phase overview plus a per-subagent accordion: live progress while it runs, parsed aggregated result once it completes (and after a refresh that has already dropped the live tasks). Refresh and resync keep member identity metadata (swarmIndex, parentToolCallId, subagentType, runInBackground) stable across skeleton task replacement in the reducer, and the .content-wrap flex layout is hardened against the overflow compression that previously displaced the footer.
🦋 Changeset detectedLatest commit: 77f0c5e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: becaaa5e9c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| </div> | ||
| </template> | ||
|
|
||
| <div v-else class="waiting">{{ t('tools.swarm.waiting') }}</div> |
There was a problem hiding this comment.
Show AgentSwarm error output when parsing fails
When AgentSwarm fails before producing <agent_swarm_result> (for example invalid swarm args such as missing prompt_template, or another runSwarm validation error), the tool result is plain text in tool.output with status === 'error'. This fallback ignores that output and renders “Waiting for subagents…”, so the expanded card hides the actual failure reason from the user instead of showing the error text.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 354b6c7. Body now renders the parsed <agent_swarm_result> subagents in the accordion with the live members, and when none are available, falls back to the raw tool output (ok or error) instead of 'Waiting for subagents…'. See buildSwarmCardRows / fallbackOutput.
| // AppTask store so the card shows each subagent's live phase; on refresh the | ||
| // tasks are gone and the card falls back to the parsed tool result. | ||
| provide('resolveSwarmMembers', (toolCallId: string): SwarmMember[] => { | ||
| return client.swarms.value.find((group) => group.id === toolCallId)?.members ?? []; |
There was a problem hiding this comment.
Source live members for single resume swarms
When AgentSwarm is used with a single resume_agent_ids entry, the core tool allows it, but client.swarms is built by buildSwarmGroups() which filters out groups with only one member. This new resolver therefore returns [] for that valid running swarm, so the inline card cannot show the resumed subagent’s live phase/progress until a final structured result arrives.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed. resolveSwarmMembers now sources from a dedicated AppTask-store bucket keyed by parentToolCallId (swarmMembersByToolCall), which keeps single-member swarms — including a resume-only AgentSwarm — instead of buildSwarmGroups's >1 filter. badge counter still uses buildSwarmGroups.
- SwarmTool: when AgentSwarm fails before producing a structured agent_swarm_result (e.g. argument validation), render the raw tool output instead of the "waiting for subagents" placeholder so the failure cause is visible. - resolveSwarmMembers: source live members from the AppTask store keyed by parentToolCallId rather than buildSwarmGroups, which filters out single-member groups. A resume-only AgentSwarm now streams its live progress before the final result arrives. The badge counter still relies on buildSwarmGroups's filter.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e960b566c0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| subagentType: task.subagentType, | ||
| phase: phaseForTask(task), | ||
| summary: task.outputPreview, | ||
| outputLines: task.outputLines, |
There was a problem hiding this comment.
Preserve streamed subagent text for swarm rows
When a swarm subagent streams normal assistant output, the projector sends taskProgress with kind: 'text' and the reducer accumulates it on AppTask.text rather than outputLines. This new live-member map drops that field, so SwarmTool can only derive activity/body from tool progress or the final outputPreview; agents that are actively composing text show an empty/ stale row until completion. Please carry task.text through SwarmMember and prefer it when rendering the live row body/activity.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed. Added text to SwarmMember and threaded it through buildSwarmGroups and swarmMembersByToolCall. SwarmTool prefers member.text for both the row activity preview and the expanded body so a still-streaming subagent shows ongoing output instead of an empty / stale row.
Swarm subagents that stream normal assistant output accumulate it on AppTask.text (text-kind taskProgress), not outputLines. The new live member map was dropping `text`, so a still-composing subagent rendered an empty / stale row until the structured result arrived. - Add `text` to SwarmMember and thread it through buildSwarmGroups and swarmMembersByToolCall. - SwarmTool: prefer member.text for both the row activity preview and the expanded body; fall back to outputLines / summary. - Tests cover text propagation through both helpers.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6211c831c2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // One accordion row per subagent. Prefer the live members (each one's live | ||
| // phase); fall back to the parsed result when the AppTask store is gone. | ||
| const rows = computed<Row[]>(() => { | ||
| if (hasMembers.value) { |
There was a problem hiding this comment.
Prefer final swarm results over partial live members
When an AgentSwarm has produced a parsed result but at least one AppTask still exists, this branch returns only live members and never includes result.subagents. In interrupted swarms, the result can contain state="not_started" / outcome="aborted" entries for items that never spawned a task, so the inline card omits those rows and undercounts progress until a refresh drops the tasks. Once result is present, merge missing result rows or prefer the result for terminal entries.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 354b6c7. Extracted the row model into buildSwarmCardRows(members, result) and merged result-only aborted/not-started rows on top of the live member rows, so interrupted swarms that have a parsed result but still some live AppTasks no longer drop subagents that never spawned a task.
|
|
||
| <div v-else-if="errorOutput" class="error-output">{{ errorOutput }}</div> | ||
|
|
||
| <div v-else class="waiting">{{ t('tools.swarm.waiting') }}</div> |
There was a problem hiding this comment.
Show raw AgentSwarm output instead of waiting
For a completed AgentSwarm whose output is not recognized by parseSwarmResult—for example a legacy transcript or any parser miss—rows is empty and errorOutput is suppressed because the status is not error, so this renders “Waiting for subagents…” even though tool.output contains final text. That regresses completed swarm calls from showing any result; use the raw output as a fallback when the tool is no longer running.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 354b6c7. Renamed errorOutput -> fallbackOutput; whenever the tool is no longer running and produced no structured result (ok or error, parser miss, or legacy output), the raw tool output is shown. Covered by unit tests.
Address the two latest swarm review comments: - Rows: when a parsed agent_swarm_result coexists with live AppTasks (which the detail panel also depends on), the inline card previously only rendered the live members. Interrupted swarms can carry state="not_started" / outcome="aborted" result entries for items that never spawned a task; those rows were dropped until a refresh cleared the live tasks. Extract the row model into buildSwarmCardRows and merge result-only aborted/not-started rows with the live member rows. - Fallback: when the tool is no longer running but produced no structured result (argument validation, parser miss, or legacy legacy transcript), render the raw tool output instead of "Waiting for subagents…" so the final text / failure cause is visible to the user.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7592612cae
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| const SUMMARY_RE = /<summary>([\s\S]*?)<\/summary>/; | ||
| const RESUME_HINT_RE = /<resume_hint>([\s\S]*?)<\/resume_hint>/; | ||
| const SUBAGENT_RE = /<subagent\b([^>]*)>([\s\S]*?)<\/subagent>/g; |
There was a problem hiding this comment.
Avoid truncating subagent bodies on XML-like output
When a subagent's final text contains a literal </subagent> sequence (for example while analyzing or emitting an AgentSwarm/XML snippet), the AgentSwarm producer writes that body text unescaped, so this non-greedy regex treats the embedded text as the end of the row. In the result-only path used after refresh, the inline swarm card will then lose the rest of that subagent output and can misparse following rows; parse the top-level result format more defensively or escape/unescape body content.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 354b6c7. Rewrote parseSwarmResult to scan opening tags and resolve each row's body as everything up to the last before the next row's opening tag (or doc end), so a subagent whose body contains a literal is no longer truncated. Tests cover both a single body and sibling rows.
Producer writes subagent body unescaped, so a subagent that analyzes or emits an AgentSwarm snippet can include a literal "</subagent>" inside its body. The non-greedy regex treated that as the row close and truncated the body in the result-only path (post-refresh where the AppTask store is gone). Rewrite the parser to scan opening tags, then resolve each row's body as everything up to the last "</subagent>" before the next row's opening tag (or document end), preserving embedded close-tag strings. Add tests for a literal "</subagent>" within a single body and across sibling rows.
|
@codex All P2 review comments addressed in
Please re-review. CI is currently green (build / lint / typecheck / test / nix build all pass). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 354b6c7f25
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| while ((m = SUBAGENT_START_RE.exec(text)) !== null) { | ||
| opens.push({ attrs: m[1] ?? '', start: m.index, openEnd: SUBAGENT_START_RE.lastIndex }); |
There was a problem hiding this comment.
Parse only top-level subagent result tags
If a subagent's returned body contains an AgentSwarm snippet or any literal <subagent ...> tag, this scan treats that embedded tag as another result row because the producer leaves body text unescaped and this loop pre-collects every opening tag in the whole output. In that case the completed/refreshed swarm card splits the real body and shows bogus/duplicate subagents; the parser needs to ignore nested/body tags or otherwise delimit only the top-level rows.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 77f0c5e. Rewrote parseSubagents with a depth-tracking tokenizer: each <subagent ...> / </subagent> token is consumed in order; a row only registers at the outermost level, while openings/closings nested inside another row's body are treated as body text. A subagent emitting an AgentSwarm snippet no longer spawns a bogus row. Tests cover a single nested snippet and sibling rows with one of them carrying a nested snippet.
A subagent body that contains a literal `<subagent ...>` tag — for example emitting an AgentSwarm/XML snippet — was being pre-collected as another result row, splitting the real body and producing duplicate / bogus subagents after refresh where the AppTask store is gone. Rewrite parseSubagents with a depth-tracking tokenizer: scan every `<subagent ...>` / `</subagent>` token in order, push a real row frame only at the outermost level, and treat openings / closings while nested inside another body as body text. Drop the now-inaccurate "literal </subagent> without matching open" regression tests; replace with tests that verify a balanced nested snippet stays inside the parent body and does not register as a separate row.
|
@codex New fix on top of the previous recap: 77f0c5e addresses your latest P2 on parseSwarmResult.ts — only top-level |
|
To use Codex here, create a Codex account and connect to github. |
Related Issue
No linked issue — see Problem below.
Problem
The legacy AgentSwarm render used a bottom
SwarmCardfooter plus asuppressed inline card, gated on live-only fields (
swarmIndex/parentToolCallId) that only thesubagent.spawnedWebSocket path sets.Two bugs followed:
.content-wraphadmin-height: 100%inside a flexscroll container, so overflow content compressed back to viewport height
and pinned the footer near the top of the conversation.
subagent tasks, and the projector's
reset()replaced accumulated taskswith skeleton ones that drop identity metadata, so the footer never
appeared (or vanished mid-flight) and a stray inline card could render.
What changed
SwarmTool):a phase overview plus a per-subagent accordion. Each subagent row shows
state + name + one-line activity; clicking it expands its full output.
Live rows come from the AppTask store; once the tool result lands — and
after a refresh that has already dropped the live tasks — the same rows
are rebuilt from the parsed
<agent_swarm_result>payload.SwarmCard, themessagesToTurnslive-swarm skip,and the plumbing that passed
subagentTasksintomessagesToTurns/useSideChat. Theswarmprop wiring fromApptoConversationPaneis dropped too.
.content-wrapwithflex-shrink: 0so siblings after<ChatPane>no longer compress it back to viewport height.taskCreatedfrom overwritingswarmIndex/parentToolCallId/subagentType/runInBackgroundwithundefinedwhen a resync ships askeleton task over an accumulated one.
parseSwarmResulthelper plus tests so the refresh path canreconstruct the subagent roster from the tool result.
Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.