feat(cli): replace inline AgentExecutionDisplay with always-on LiveAgentPanel#3909
Conversation
…entPanel Surface running subagents in a borderless, always-on roster anchored beneath the input footer (mirrors Claude Code's CoordinatorTaskPanel) and retire the verbose inline `AgentExecutionDisplay` frame whose per-tool-call mutations caused scrollback flicker. Detail / cancel / resume keep flowing through the existing BackgroundTasksDialog. LiveAgentPanel: - Two-column row: status icon + (optional) type + description + activity on the left (truncate-end), elapsed + tokens on the right in a flex-shrink:0 column so the cost/time fields are never hidden by long descriptions. - Re-pulls each agent from BackgroundTaskRegistry on every wall-clock tick so `recentActivities` stays fresh — the snapshot from `useBackgroundTaskView` only refreshes on `statusChange` to keep the footer pill / AppContainer quiet under heavy tool traffic. - Reaches for Config via raw ConfigContext (not useConfig) so the panel degrades to snapshot-only when no provider is mounted (test isolation). - Hides when any dialog is visible (auth / permission / bg tasks) and self-hides when no agent entries are live. - Drops `subagentType` from the row when it is the default `general-purpose` builtin to keep the line uncluttered; specialized types still bold-anchor the row. - Keeps terminal entries on screen for 8s so the user gets feedback when an agent finishes, then they fall off (BackgroundTasksDialog retains them long-term). Inline frame retirement: - ToolMessage's SubagentExecutionRenderer collapses to the focus- routed approval surfaces only (focus-holder banner + queued marker). All other agent state is owned by the panel + dialog. - AgentExecutionDisplay.tsx + test removed (-918 lines); the subagents/index export is dropped with a pointer to the new surfaces. Net diff: +97 / -1069.
The two-column layout used `flex-grow:1` on the left description column, which puffed it out to fill the row even when content was short — leaving a visible gap between the description tail and the right-pinned elapsed/tokens whenever the terminal was wider than the content. Worse, the gap made it look like display space was being wasted while the description still got truncated. Move elapsed + tokens to the front of the row (right after the status icon) so: - Time and cost are pinned at a stable left position and are NEVER at risk of being truncated, regardless of description / activity length. - The row reads as one tight left-to-right line — no flex-grow, no internal gap. On wide terminals the unused width sits at the row tail (invisible), where it belongs. - Description + activity become the truncatable tail; `truncate-end` cuts only when the line genuinely overflows the panel width. Also wrap the icon-plus-spaces span in a template literal so the two spaces of breathing room after the glyph survive a prettier pass. Verified at 60 / 100 / 200 cols: at 200 cols the row renders flush with no trailing ellipsis and no internal gap; at 60 cols the time + tokens stay at the front and the description tail truncates with `…`.
…x-grow) Iterating on the row layout based on visual review: - Layout A (time first, single Text) put numbers ahead of identity, which broke the natural left-to-right reading order. - Layout B (right-pinned with flex-grow:1 on left) puffed the left column out to fill the row, leaving a visible gap between the description tail and the right-pinned elapsed when the terminal was wider than the content. Layout C keeps the right column flex-shrink:0 so elapsed + tokens are never clipped, but DROPS flex-grow on the left so the two columns sit side-by-side: empty slack falls off the row tail (invisible) instead of opening a gap inside the row. Identity (type) and intent (description / activity) read first, cost reads last — matching the natural visual hierarchy. When the row overflows the panel width the left column truncates with `…` mid-row, while elapsed + tokens stay intact. Verified at 60 / 100 / 200 cols — at 200 cols there is no internal gap and no trailing ellipsis; at 60 cols time + tokens stay visible on the right and the description / activity tail truncates with `…`.
…rows Adopt the leaked CoordinatorTaskPanel visual conventions: - `○` replaces `⊷` for live (running) slots — matches Claude Code's use of `figures.circle` for the active-agent bullet, gives a uniform list look across the running roster. Terminal states keep distinct check / cross marks (✔ / ✖) so they're easy to scan at a glance. - `▶` separates the description from elapsed / tokens, mirroring Claude's `PLAY_ICON` suffix marker. - Activity is wrapped in `( ... )` so it reads as an annotation on the description rather than a sibling field, and the type prefix switches from ` · ` to `: ` (e.g. `editor: tighten import order`) to match Claude's `name: description` pattern. The two-column flex layout from layout C is preserved — left column flex-shrink:1 with truncate-end, right column (` ▶ Ns · Nk tokens`) flex-shrink:0 so elapsed + tokens are never clipped, regardless of how long the description / activity grows. This is the one intentional divergence from Claude's literal pattern, which puts elapsed at the row tail without pinning and lets it disappear off narrow terminals. Verified at 60 / 100 / 200 cols: at 200 cols the row is flush with no internal gap; at 60 cols the description / activity tail truncates with `…` while elapsed + tokens stay visible on the right.
There was a problem hiding this comment.
Pull request overview
Replaces the CLI’s verbose inline subagent execution frame (AgentExecutionDisplay) with an always-on LiveAgentPanel roster rendered beneath the main controls, while keeping inline subagent rendering limited to approval prompts/queued markers (with details/cancel/resume remaining in BackgroundTasksDialog).
Changes:
- Mount
LiveAgentPanelin the default UI layout and retire the inlineAgentExecutionDisplaycomponent + tests. - Simplify
ToolMessagesubagent rendering to approval-only inline surfaces (no committed or live inline frames). - Add unit tests covering
LiveAgentPanelbehavior and updateToolMessagetests to match the new rendering contract.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/cli/src/ui/layouts/DefaultAppLayout.tsx | Mounts LiveAgentPanel in the main layout when appropriate. |
| packages/cli/src/ui/components/background-view/LiveAgentPanel.tsx | Introduces the always-on roster UI and its live registry “re-pull” behavior. |
| packages/cli/src/ui/components/background-view/LiveAgentPanel.test.tsx | Adds coverage for rendering, windowing, live re-pull, and terminal TTL behavior. |
| packages/cli/src/ui/components/messages/ToolMessage.tsx | Removes inline subagent frame rendering; keeps approval prompt + queued marker inline only. |
| packages/cli/src/ui/components/messages/ToolMessage.test.tsx | Updates assertions to reflect approval-only inline rendering for subagents. |
| packages/cli/src/ui/components/subagents/index.ts | Removes the AgentExecutionDisplay export and documents the new surfaces. |
| packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.tsx | Deletes the retired inline subagent execution frame implementation. |
| packages/cli/src/ui/components/subagents/runtime/AgentExecutionDisplay.test.tsx | Deletes tests for the retired inline execution frame. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… at dialog Three usability fixes from review: 1. Use `terminalWidth` instead of `mainAreaWidth`. The latter is capped at 100 cols (intended for markdown / code where soft-wrap matters), which on a 200-col terminal left half the screen empty to the right of an already-truncating row. Live progress lines have nothing to soft-wrap, so the panel wants the full width. 2. Drop the `[in turn]` foreground marker. The flavor distinction matters in BackgroundTasksDialog (cancel semantics differ for foreground vs background entries) but in the glance panel the marker reads as cryptic noise — users asked what it meant. Keep the dialog as the surface that surfaces it. 3. Annotate the overflow callout with `(↓ to view all)`. The panel is intentionally read-only (it has no keyboard focus so it can't steal input from the composer), so when the roster outgrows the row budget we point users at the existing dialog — same keystroke the footer pill uses, kept in sync so users only learn one gesture.
The focus chain Composer → AgentTabBar → BackgroundTasksPill is walked with the Down arrow, but Down dead-ended at the pill — the pill only opened the dialog on Enter. Users who followed the LiveAgentPanel's "(↓ to view all)" overflow callout reached the highlighted pill and got stuck there, defeating the hint. Route Down on the focused pill into openDialog so the chain completes naturally: Composer ↓ → AgentTabBar ↓ → Pill ↓ → Dialog. Enter still works, so existing muscle memory keeps functioning.
…host rows, dead doc Three findings from Copilot's PR review: 1. **1s interval kept ticking forever after expiry.** The gate was `entries.some(isAgentEntry)`, but `BackgroundTaskRegistry.getAll()` retains terminal entries indefinitely — once the last visible row passed its 8s window the panel returned null but the interval kept firing setNow each second, churning re-renders for nothing on screen. The gate now considers visibility (running / paused OR terminal-within-window) and the interval clears itself once the condition flips false. New entries restart the interval via the `entries` dep. 2. **Ghost rows when registry forgets an entry.** The live re-pull fell back to the snapshot when `registry.get()` returned undefined. The canonical case is a foreground subagent that unregisters silently after its statusChange fires (`unregisterForeground` deletes without emitting a follow-up transition) — the snapshot still says `running`, so the row would never clear. Trust the registry: when it says the entry is gone, drop the row. The snapshot-only fallback is preserved for the no-Config case (test fixtures). 3. **Dead doc reference.** The trailing comment in `subagents/index.ts` pointed at `docs/comparison/subagent-display-deep-dive.md`, which doesn't exist in this repo (it lives in the codeagents knowledge base, not qwen-code). Dropped the dead pointer; the in-tree pointers to `LiveAgentPanel` and `BackgroundTasksDialog` already tell readers where to look. Coverage delta: +2 cases on `LiveAgentPanel.test.tsx` — `drops snapshot rows the live registry no longer knows about` (issue #2) and `still shows the snapshot when no Config is mounted` (locks in the test-fixture fallback that issue #2's stricter rule would otherwise have broken).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
packages/cli/src/ui/components/subagents/index.ts:16
- This comment points to
docs/comparison/subagent-display-deep-dive.md, but there is nodocs/comparisondirectory or matching file in the repo. Either update the reference to an existing doc location or add the referenced doc so future readers can actually find the rationale.
// Execution Display: the verbose inline frame was retired. Live progress
// is now rendered by `LiveAgentPanel` (always-on roster, beneath the
// composer) and `BackgroundTasksDialog` (Down-arrow detail view).
wenshao
left a comment
There was a problem hiding this comment.
Design-level findings (no single line reference):
-
Failed agents don't show
terminateReasonon the panel.AgentRowrenders only a✖icon for failed agents — the error reason is hidden behindBackgroundTasksDialog. The oldAgentExecutionDisplayshowedFailed: {terminateReason}inline. This is a UX regression for at-a-glance debugging. -
Completed agents leave no trace in scrollback after 8s.
SubagentExecutionRenderernow returnsnullfor non-pending-confirmation states. OnceTERMINAL_VISIBLE_MSexpires, there's no record of the agent's work in the<Static>scrollback — users must open the dialog to find past results. -
Test coverage gaps in
LiveAgentPanel.test.tsx:paused/failed/cancelledstatus icons,general-purposetype elision, andpausedagent expiry exclusion are not tested. These are high-value regression guards.
…isFocused doc Two further findings from Copilot: 1. **Interval kept ticking while bg-tasks dialog was open.** The first-pass fix already torn the tick down once all rows expired from the visibility window, but `dialogOpen` was a separate reason the panel returned null and was missed by the gate. Add `dialogOpen` to the useEffect deps and short-circuit when true, so the dialog's tenure is interval-free. 2. **Stale `isFocused` doc comment in `ToolMessageProps`.** The comment claimed the prop controlled the now-retired `Ctrl+E / Ctrl+F` display shortcuts (those died with the inline `AgentExecutionDisplay` frame). Rewrite the comment to describe the only remaining behavior — the focus-routed approval surface (focus-holder banner vs. queued-sibling marker). Coverage delta: +1 case on `LiveAgentPanel.test.tsx` — `tears the 1s tick down when the bg-tasks dialog opens` advances 60s of fake time with `dialogOpen=true` and asserts no panel state drift.
wenshao
left a comment
There was a problem hiding this comment.
Three test-coverage gaps surfaced on a re-pass — non-blocking, but each is a 5-line addition that would lock in behavior the panel already implements.
…ress review nits
Hot-fix: the previous round's "drop the row when registry.get()
returns undefined" was too aggressive. `unregisterForeground` calls
`emitStatusChange(entry)` BEFORE it deletes the entry, so the
snapshot useBackgroundTaskView captures still says "running" while
the very next render's registry.get sees nothing. Dropping the row
outright made foreground subagents disappear from the panel the
instant they finished — users saw "SubAgents 不显示了" on tasks that
ran-and-immediately-completed.
Reconciliation now has three branches:
1. live found → use live (newest recentActivities).
2. snap says still-live but registry forgot → synthesize a
terminal version with endTime pinned to the FIRST observation
so the 8s visibility window gives the user a "the agent
finished" beat then evicts cleanly. The first-seen-missing
timestamp is held in a useRef map (without it, each tick
resets endTime to `now` and the row never expires).
3. snap is already terminal but registry forgot → drop (no
useful state to keep showing).
Also addresses three smaller review notes (deepseek-v4-pro via
/review):
- DEFAULT_SUBAGENT_TYPE is now imported from
@qwen-code/qwen-code-core (a new DEFAULT_BUILTIN_SUBAGENT_TYPE
export referenced by both BuiltinAgentRegistry's seed entry and
the panel's default-type elision). A backend rename now propagates
instead of silently re-introducing the redundant
`general-purpose:` prefix on every row.
- The useMemo body now reads `now` (as `reconcileAt`) so the
dependency is semantically honest — a future "remove dead dep"
cleanup can no longer silently freeze the panel on the first
tool-call after a snapshot refresh.
Coverage delta: +5 cases on LiveAgentPanel.test.tsx — token rendering
on completed entries, status-icon routing for paused / failed /
cancelled (parametrized), case-insensitive prefix stripping in
descriptionWithoutPrefix, plus the rewritten ghost-row case
(synthesized terminal lingers 8s then evicts) and a sibling case
asserting already-terminal snapshots with empty registry still drop.
17 LiveAgentPanel tests pass.
tanzhenxin
left a comment
There was a problem hiding this comment.
Review
The refactor is well-scoped and the implementation is careful — hook ordering, interval lifecycle, and the registry/snapshot reconciliation all hold up to scrutiny. Two rounds of fix-up commits already addressed the obvious leak risks (interval not clearing after expiry, ghost rows when the foreground unregister silently deletes, interval still firing while the bg-tasks dialog is open).
Committed-phase artifact is leaner than the description suggests
The rationale leans on BackgroundTasksDialog retaining the long-term view, but the dialog only surfaces totals — total tokens and a tool count. The old inline frame also surfaced success rate, success/fail breakdowns, and the per-tool usage table; none of those are visible in the live UI anymore. The data still flows through to session export and history replay, so nothing is lost on disk, but a user reading committed scrollback (or opening the dialog mid-session) can't see the breakdown anywhere. This reads as a deliberate trade-off given the "committed phase is heavy" framing — worth flagging in the description so future readers don't assume the dialog is a full replacement.
A few test gaps worth picking up
The new test file is thoughtful, but a couple of behaviors aren't asserted: general-purpose label elision, narrow-width truncation (the helper accepts a width param but no case actually varies it), and interval cleanup on a real unmount — the closest case starts with the dialog already open, which short-circuits before the interval is ever created. Likewise, the rewritten ToolMessage cases only assert that old strings are absent; no positive sentinel proves the deleted component can't render.
Verdict
APPROVE — solid refactor, clean cleanup of the inline frame, and the residual concerns are minor or deliberate.
…ngForOtherApproval props Two structural reviews from deepseek-v4-pro via /review: 1. **Hook-order footgun.** The `if (dialogOpen) return null` guard sat between the hook block and a substantial block of pure-render code; an extension that added `useMemo`/`useRef`/`useCallback` below the guard would crash with "Rendered fewer hooks than expected" the next time `dialogOpen` toggled. Extract the pure- render logic into `LiveAgentPanelBody` so the guard becomes the parent's last statement, and any future "add a hook to the body" refactor naturally lands in the inner component (hook-free today, hook-free for as long as it stays a presentational FC). 2. **Dead pass-through removed.** `isPending` and `isWaitingForOtherApproval` were dead in the subagent renderer (LiveAgentPanel + BackgroundTasksDialog own that surface) but ToolGroupMessage still computed `isWaitingForOtherApproval` and forwarded both into ToolMessage. Drop them from `ToolMessageProps`, drop the computation + forwarding in ToolGroupMessage, drop the test factory references. ToolGroupMessage keeps `isPending` on its own props for upstream caller compatibility (HistoryItemDisplay et al. forward it) but stops destructuring it; the doc comment now points at the surfaces that own that gating today. Coverage: existing 115 cases across LiveAgentPanel / BackgroundTasksDialog / BackgroundTasksPill / ToolMessage / ToolGroupMessage all green; the panel split is purely structural and the dead-prop removal was verified TS-clean before commit.
…eAgentPanel rows `recentActivities[].name` carries the internal tool name from AgentToolCallEvent (e.g. `run_shell_command`, `glob`). Rendering it verbatim surfaced raw identifiers in the panel (`run_shell_command rg TODO`) while BackgroundTasksDialog already mapped through ToolDisplayNames to show user-facing names (`Shell rg TODO`) — the two surfaces' vocabularies drifted on the same data. Mirror the dialog's `TOOL_DISPLAY_BY_NAME` lookup so the panel and dialog speak the same vocabulary. Added a test asserting `run_shell_command` renders as `Shell` and the raw name is not surfaced. Coverage delta: +1 case on LiveAgentPanel.test.tsx (18 total).
… test gaps
1. **Terminal snap + missing registry now follows the visibility
window.** Cancelled / failed foreground subagents go through
`cancel`/`fail` (which stamp `endTime` and emit statusChange)
followed by `unregisterForeground` (which deletes silently). The
snap captures the real `endTime`, so the previous "drop on
missing registry" branch made cancelled / failed foreground
tasks disappear instantly — contradicting the panel's "brief
terminal visibility" contract that the synthesized-completion
path also relies on. Keep the snap as-is when `endTime` is set;
the visibleAgents filter evicts it after `TERMINAL_VISIBLE_MS`
like any other terminal entry. Defensive fallback drops the
pathological "terminal status with no endTime" shape.
(Copilot finding on PRRT_kwDOPB-92c6AS4z9.)
2. **Close 4 test gaps flagged by tanzhenxin's review:**
- `elides the default 'general-purpose' subagent type from the row`
locks in DEFAULT_BUILTIN_SUBAGENT_TYPE comparison.
- `truncates the description tail when the panel width is too
narrow` exercises the `width` prop (existing cases ignored it)
and anchors on the right-pinned `▶ 3s` tail staying intact.
- `clears the 1s tick interval when unmounted with live work in
flight` spies on setInterval / clearInterval so a discarded
fiber can't keep firing setNow.
- `keeps terminal snapshots visible until the TTL even when the
registry forgot them` covers the cancelled / failed foreground
reconciliation path with a `✖` glyph + 9s eviction assertion.
Coverage: 22 LiveAgentPanel tests pass (was 18). All TS clean.
|
Thanks for the careful read, @tanzhenxin. Picked up all three test gaps you flagged in c3d80fc:
Plus the related Copilot finding (PRRT_kwDOPB-92c6AS4z9) about cancelled / failed foreground tasks disappearing instantly instead of lingering for the 8s TTL — same commit reconciles them as terminal snaps. Also added a "Deliberate trade-off — committed-phase summary is leaner" section to the PR description spelling out exactly which fields the dialog does NOT replicate from the old inline frame (success rate, success / fail breakdown, per-tool usage table) so future readers don't assume the dialog is a 1:1 replacement. |
…cking]` User feedback: `[in turn]` reads as "queued / sequential" — the opposite of what it actually means (the row is blocking the user's current turn). Even maintainers had to chase the source comment to confirm the semantics, which is a strong signal that end users are not getting the warning the prefix is supposed to deliver. `[blocking]` reads more directly: "this row is what's holding up your input", which is what cancelling it actually unblocks. Update the in-row prefix and the cancel-confirmation hint in lockstep (`x again to confirm stop · ends the blocking turn`) so the two surfaces share vocabulary. LiveAgentPanel still suppresses the marker in its row (the panel has no cancel surface, so the warning has nothing actionable to pair with); update the historical comment + reinforce the test guard so neither the legacy `[in turn]` nor the new `[blocking]` bleeds into the glance roster.
…s glyph, stale comments 1. **`availableTerminalHeight` regression** (claude-opus-4-7 via /qreview, DefaultAppLayout.tsx:127). LiveAgentPanel rendered OUTSIDE the `mainControlsRef` Box, so its 2-7 rows were not measured by `controlsHeight` and not subtracted from `availableTerminalHeight = terminalHeight - controlsHeight - staticExtraHeight - 2 - tabBarHeight` in AppContainer. Pending tool results in MainContent could render past the visible area and push the composer / panel off-screen — a regression vs PR QwenLM#3768 which suppressed the inline frame in the live phase. Move the panel INSIDE the `mainControlsRef` Box so `measureElement` picks up its rows automatically; no new infrastructure needed. 2. **Neutral glyph for synthesized terminal rows** (claude-opus-4-7, LiveAgentPanel.tsx:278). The synthesis branch hardcoded `status: 'completed'` regardless of the actual outcome. Foreground subagents that errored or were cancelled were rendered with the green ✔ for 8s — directly contradicting the inline tool result the user just saw (`Subagent execution failed.` / `Agent was cancelled by the user.`). Add a `synthesized` flag on the synthesized rows; `statusIcon` checks it first and returns a neutral `·` + secondary color, so the panel never lies about an outcome it cannot determine. Status stays `'completed'` purely so the visibility-window filter treats the row as terminal. 3. **PR description claim retired** (Copilot, ToolMessage.tsx:411). The body's Reviewer Notes section claimed `isPending` / `isWaitingForOtherApproval` were kept on `ToolMessageProps` as vestigial pass-through; the props were actually removed in commit 93036b1. Will update the PR description in a follow-up non-code edit (the comment thread itself is on now-outdated line 411). 4. **Stale comment in ToolGroupMessage** (Copilot, ToolGroupMessage.tsx:303). The comment above the focus-routing logic referenced the retired Ctrl+E / Ctrl+F display shortcuts. Rewrite it to describe the current behavior — focus routing for inline approval prompts only — and call out where the live progress / drill-down moved (LiveAgentPanel + BackgroundTasksDialog). Coverage delta: existing `reconciles snapshots…` test rewritten to assert `·` instead of `✔` (and explicitly assert `not.toContain('✔')`); new sibling `keeps the success glyph for entries the registry still tracks (non-synthesized)` locks the non-synthesis path against a future regression that always returns the neutral glyph. 68 background-view tests + 128 messages/layouts tests pass.
…llback summary
Two findings from deepseek-v4-pro via /review:
1. **ANSI sanitization on the panel** (LiveAgentPanel.tsx:368). The
row was rendering `entry.subagentType`, `descriptionWithoutPrefix`
output, and `recentActivities[].description` straight through Ink's
`<Text>` — both subagent config (user-authored) and tool-call
description (LLM-generated) can contain terminal control sequences
that bleed through and corrupt the panel chrome. Apply
`escapeAnsiCtrlCodes` to all three (HistoryItemDisplay does the
same on its user-facing content for the same reason).
2. **One-line scrollback summary for terminal subagents**
(ToolMessage.tsx:295). The previous round retired the verbose
inline frame entirely and SubagentExecutionRenderer returned null
for all non-approval states. The result: completed subagent
results disappeared from scrollback the moment LiveAgentPanel's
8s window expired. Reopening a session or dismissing the dialog
left no record. Add a single-line `SubagentScrollbackSummary` for
completed / failed / cancelled states — `<icon> <name>:
<description> · N tools · Xs · Yk tokens` (plus terminateReason
on non-success). One row per agent, no flicker risk; the verbose
frame stays retired.
Coverage: +3 cases — `escapes ANSI control codes in user-controlled
strings` (LiveAgentPanel), `completed subagent → renders a one-line
scrollback summary` and `failed subagent → renders summary with
terminate reason` (ToolMessage). 24 + 34 panel/ToolMessage tests
pass; broader 2894-test cli/ui sweep clean.
Note: this restores some inline rendering that the original feature
choice ("inline AgentExecutionDisplay 完全移除") removed entirely.
The compromise — one line per terminal agent vs the old 15-row
frame — preserves history while keeping the flicker fix that
motivated the retirement.
…n comment
Two findings from Copilot:
1. **Header tally now matches what the user sees.** The numerator
was `runningCount` (status === 'running'), but the panel also
renders paused entries as active rows (warning color, ⏸ glyph).
With only paused agents present the header read "(0/1)" while
the row was clearly visible — a confusing mismatch. Rename to
`activeCount` and include paused in the count. New test
`counts paused agents as active in the header tally` locks the
tally + glyph in.
2. **Reconciliation block comment now describes the four real
paths**, not the older three:
1. live found → use live
2. snap still-live + registry forgot → synthesize neutral terminal
3. snap terminal + endTime present → keep snap, let TTL filter
evict it (the cancelled / failed foreground path the previous
round added)
4. snap terminal + no endTime → drop (upstream invariant violation)
Comment is now in lockstep with the implementation; the prior
"drop terminal-with-empty-registry outright" wording was stale
after the TTL-keep change.
Coverage: 25 LiveAgentPanel tests pass (was 24).
wenshao
left a comment
There was a problem hiding this comment.
Typecheck Errors (Critical)
- packages/channels/weixin/src/send.test.ts:195,245 — TS2345: function type not assignable to NormalizedProcedure
- packages/cli/src/ui/components/InputPrompt.test.tsx:184 — TS2741: missing midInputGhostText in UseCommandCompletionReturn mock
- packages/cli/src/ui/components/InputPrompt.test.tsx:2012,2028 — TS2339: vimModeEnabled does not exist on InputPromptProps
- packages/cli/src/ui/components/InputPrompt.test.tsx:3466 — TS2352: unsafe type conversion to UIState (use )
- packages/cli/src/ui/hooks/slashCommandProcessor.test.ts:562 — TS2339: stripThoughtsFromHistory does not exist on GeminiClient
- packages/core/src/services/shellExecutionService.ts:13 — TS7016: no declaration file for @lydell/node-pty
Critical Review Findings
WeixinAdapter.sendMessage (WeixinAdapter.ts:193-258): ~65 lines of image marker parsing and sendText/sendImage orchestration with zero test coverage. No WeixinAdapter.test.ts exists. Core message pipeline lacks integration tests.
weixin/api.ts:247-407: Four new exported functions (isRetryableError, retryWithBackoff, getUploadUrl, uploadToCdn) have zero direct test coverage. Critical network calls for image sending.
subagent-manager.ts:721-737: When upstreamRebuilt=true, the inherit-selector path skips rebuildToolRegistryOnOverride. Subagent tools remain bound to upstream FileReadCache, not the subagent's own. ReadFileTool's file_unchanged optimization may fire incorrectly.
geminiChat.ts:471-485: tryCompress only sets hasFailedCompressionAttempt for explicit COMPRESSION_FAILED_* status codes. If the compression API throws an exception (network error), the flag is never set. Every subsequent sendMessageStream retries compression and fails, making the chat unusable.
Suggestions
shellExecutionService.ts:76-90: getShellAbortReasonKind value-equality whitelist has no compile-time link to the discriminated union. New kind variants silently degrade to 'cancel'.
shellExecutionService.ts:249/589: performCancelKill and abortHandler switch duplicated between child_process and PTY paths. Extract shared helper.
— deepseek-v4-pro via Qwen Code /review
…entPanel (QwenLM#3909) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…Change emit (#3919) * fix(cli,core): isPending gate on subagent scrollback summary + post-delete statusChange emit Two follow-ups from PR #3909 review. 1. **Re-introduce `isPending` gate on `SubagentExecutionRenderer`'s scrollback summary** (Copilot finding on PRRT_kwDOPB-92c6AUQHn). The verbose inline frame retirement collapsed `SubagentExecutionRenderer` to "render the summary whenever a subagent reaches a terminal status" — but with `isPending` removed in #3909, that fired in BOTH live (pendingHistoryItems) AND committed (Static) phases. Live-phase rendering duplicated the row LiveAgentPanel already paints below the composer until the parent turn committed. Add `isPending` back to `ToolMessageProps` purely as a gate for this one render path: the summary fires only when `!isPending` (committed). `ToolGroupMessage` forwards the flag (it kept the prop on its own interface for upstream compat the whole time). Test gap closed by the new `live (isPending) terminal subagent → no scrollback summary (panel owns the row)` case. 2. **Emit `statusChange` AFTER delete in `unregisterForeground`** (Copilot finding on PRRT_kwDOPB-92c6AUQGc + the panel-only reconciliation it spawned). The shared snapshot in `useBackgroundTaskView` only refreshes on `statusChange`, and `unregisterForeground` previously fired exactly once — BEFORE delete — so the snapshot froze with the agent as "running" while `registry.get()` returned undefined. Result: `BackgroundTasksDialog` list mode showed a ghost "running" row with cancel hints whose `x` was a no-op, contradicting what the panel already showed (synthesized neutral terminal). Fire `statusChange` a second time AFTER `agents.delete()` so snapshot consumers see the registry-less state and stop surfacing the agent. The first emit still mirrors complete/fail/cancel/finalize ordering (callbacks that re-read `registry.get` see the entry); the second emit is the new contract for snapshot-based views. React batches the two resulting setState calls into one re-render so consumers re-render exactly once. Updated the existing "emits status change before removing the entry" test to capture both emits and explicitly assert that the second observes the registry-less state. Added a sibling test covering the post-delete `getAll()` count. Coverage: 190 passing tests across core + cli (background-view + ToolMessage + ToolGroupMessage + useBackgroundTaskView). * fix(cli,core): compact-mode terminal subagent expansion + statusChange context flag Five review findings on PR #3919: 1. **Compact mode bypassed the scrollback summary** (gpt-5.5 via /qreview, ToolGroupMessage:324). `ToolGroupMessage` returns `CompactToolGroupDisplay` before the ToolMessage path when `compactMode === true`, so the new `isPending` gate on `SubagentExecutionRenderer` only protected the expanded path — committed terminal subagents in compact mode never reached `SubagentScrollbackSummary` and the LiveAgentPanel → committed- summary handoff broke for users who turned compact mode on. Force-expand the group when `!isPending` AND any tool call has a terminal `task_execution` resultDisplay. Stay compact while the parent turn is still live (`isPending`) — the panel below the composer owns that surface and an inline summary would duplicate it. Coverage: 4 new ToolGroupMessage cases (compact + completed-committed expands; compact + running-live stays compact; compact + completed-live stays compact; compact + failed-committed expands). 2. **Snapshot-coupled comment in `packages/core`** (Copilot, background-tasks.ts:292). The comment named CLI/UI consumers (`useBackgroundTaskView`, `BackgroundTasksDialog`) and asserted React batching guarantees from a core file. Reword to "snapshot-style consumers that re-pull `getAll()` from inside the callback" and drop the framework-specific batching claim. 3. **Two-phase emit needed an explicit signal** (Copilot, background-tasks.ts:283). Emitting `statusChange` twice without distinguishing the phases forced consumers to either do duplicate work or risk persisting a stale `entry` from the second callback. Add an optional second arg `context?: { removed?: boolean }` to `BackgroundStatusChangeCallback`; the post-delete emit passes `{ removed: true }` so consumers can disambiguate without re-querying the registry. Backwards compatible — existing callbacks ignore the new arg. Tests updated to assert both `mock.calls[0][1] === undefined` and `mock.calls[1][1] === { removed: true }`. 4. **`isPending` doc clarified** (Copilot, ToolMessage.tsx:507). Made the default semantics explicit: omitted/undefined is treated as committed (not pending); live-area renderers MUST pass `true` explicitly to suppress the scrollback summary. 5. (4 of the threads were duplicate Copilot fires of #2 + #3.) Coverage: 219 test files / 3369 passing across cli/ui + core/agents. * docs(cli): update ToolGroupMessageProps.isPending JSDoc The previous prop comment claimed `isPending` was "not consumed by the group body" — true at the time, but the body now reads it for two real purposes (compact-mode gating + forwarding to ToolMessage). Update the doc so future callers / tests don't treat it as legacy. Addresses Copilot finding on PRRT_kwDOPB-92c6AYE0V. * fix(cli): hide live-phase subagent tool entries — LiveAgentPanel owns the row User report: with compact mode OFF, a running subagent shows up twice — once as the parent tool group's `task` row (status icon + name + description), once as the LiveAgentPanel row beneath the composer. Same agent, two surfaces, redundant. Filter `task_execution` tool entries out of the expanded `ToolGroupMessage` while `isPending=true` so the panel is the single source of truth for in-flight subagents. The entry returns once the parent turn commits (`isPending=false`), letting `SubagentScrollbackSummary` land inside the parent's tool group as a persistent audit trail. Exception: subagents with a pending approval still render, because the focus-routed banner / queued marker is the only inline surface that lets users answer the prompt without opening the dialog. If a group is purely panel-owned (e.g. a single Task call with no sibling tools), the entire `ToolGroupMessage` returns `null` so an empty bordered container doesn't float above the panel. Coverage: +4 ToolGroupMessage cases — running entry hidden in live phase / mixed group keeps siblings / pending-approval entry still renders / committed entry comes back for the audit trail. * refactor(cli): tighten subagent-tool helper naming + ANSI-safe scrollback summary Self-audit + independent review found 5 cleanup items on the live-phase hide path; all addressed in one commit since none are behavioral changes: 1. **Move `allEntriesPanelOwned` short-circuit BEFORE `showCompact`** so a pure-subagent group in compact mode is also hidden during the live phase (previously CompactToolGroupDisplay rendered a single summary line above the panel — a mild duplicate on top of what the non-compact path already fixed). 2. **Rename `isLiveSubagentTool` → `isSubagentToolEntry`.** The helper identifies a tool's resultDisplay shape; it doesn't check live-state. The previous name conflated "predicate" with "use case" and read as if it returned true only during the live phase. 3. **DRY up `hasCommittedTerminalSubagent`** to use `isSubagentToolEntry` instead of inlining its own type-narrowing. 4. **ANSI-escape `subagentName` / `taskDescription` / `terminateReason`** in `SubagentScrollbackSummary`. Same threat model as the panel rows and HistoryItemDisplay — these strings come from subagent config (user-authored) and LLM output and could carry terminal control sequences. The stats fields (tool count / duration / tokens) flow through trusted formatters and don't need escaping. 5. **Doc comments updated** to reflect the four real responsibilities of `isPending` on `ToolGroupMessageProps` (hide pure groups, force-expand committed compact, per-tool filter, forward to ToolMessage), to clarify that the keyboard-focused subagent id can point at a hidden tool harmlessly (the iterator returns `null` before the focus prop is computed), and to drop the redundant "EXCEPT" clause on the per-tool filter in favor of a single sentence. Coverage unchanged: 251 passing tests across messages / background-view / core/agents; broader 3374-test sweep clean; TS clean on both cli and core packages. * fix(cli,core): address 3 critical review findings + ANSI/doc cleanups Three real bugs flagged by gpt-5.5 via /qreview, plus 4 doc / sanitization nits from Copilot. All 7 threads close together since they share the same surfaces. ## Critical fixes 1. **Foreground subagents disappeared mid-parent-turn** (PRRT_kwDOPB-92c6AYvL9). Post-#3921 swap-order, `unregisterForeground` drops the entry from the panel snapshot the moment the subagent finishes. The previous round's `!isPending` gate on `SubagentScrollbackSummary` then suppressed the inline summary too, leaving the user with nothing on screen for the run until the parent committed. - Drop the `!isPending` gate — `unregisterForeground` already removes the row from the panel, so the inline summary can fire in BOTH live and committed phases without duplicating it. - Tighten the `ToolGroupMessage` live-phase hide so it only filters `running` / `paused` / `background` task entries (`isPanelOwnedSubagentTool`), not terminal ones. Terminal entries pass through immediately so the summary lands. - The "panel-owned" predicate is now distinct from the broader "subagent tool entry" predicate (`isSubagentToolEntry`) and the "terminal subagent" predicate (`isTerminalSubagentTool`); each usage site picks the one it actually means. 2. **Compact mode dropped the scrollback summary** (PRRT_kwDOPB-92c6AYvLw). Force-expanding the group made the container go through the expanded path, but `ToolMessage`'s own compact-mode gate (`!compactMode || forceShowResult ? renderer : 'none'`) still suppressed the result block, so `SubagentScrollbackSummary` never rendered for compact-mode users. Pass `forceShowResult={true}` for terminal subagent tool entries so the result block is always rendered. 3. **`mergeCompactToolGroups.isForceExpandGroup` didn't know about terminal subagents** (PRRT_kwDOPB-92c6AYvMC). The committed- history preprocessor merged adjacent tool_groups before render, so a terminal `task_execution` group could be absorbed into a compact batch (its `tool_use_summary` label dropped), and the render-time force-expand check never got a chance to override. Mirror the `hasCommittedTerminalSubagent` predicate inside `isForceExpandGroup` so preprocessing and rendering agree. ## Doc / sanitization nits - `BackgroundStatusChangeCallback` doc now lists every emitter (register / complete / fail / cancel / finalizeCancelled / finalizeCancellationIfPending / abandon / unregisterForeground / reset) and groups them by ordering camp (keeps-the-entry vs removes-the-entry — `reset` joins `unregisterForeground` in the delete-then-emit camp). - ANSI-escape `data.subagentName` in the focus-holder banner and the queued marker (`SubagentExecutionRenderer`) — same threat model as the panel rows and `SubagentScrollbackSummary`. ## Coverage delta - New ToolMessage case: live-phase terminal subagent now renders inline (replaces the prior "no scrollback summary" assertion that was the symptom of the AYvL9 bug). - New ToolGroupMessage cases: terminal subagent in live phase renders inline; `forceShowResult=true` propagates for terminal subagent tools (mock now exposes the prop). - New mergeCompactToolGroups parametrized cases: terminal subagent in any of completed / failed / cancelled stays its own batch. 280 tests pass across cli messages + utils + background-view + core/agents. TS clean. * fix(cli): drop `'paused'` arm from isPanelOwnedSubagentTool — not in AgentResultDisplay union CI Lint failed with TS2367: the previous round's `isPanelOwnedSubagentTool` checked for `status === 'paused'` but `AgentResultDisplay.status` (the tool-result-side type) only carries `'running' | 'completed' | 'failed' | 'cancelled' | 'background'`. The `'paused'` status lives on the registry-side `BackgroundTaskStatus` union and is only ever surfaced through `LiveAgentPanel` directly, never through a `task_execution` payload. Drop the dead arm and add a comment so a future "let's also check paused here" doesn't get re-introduced. * fix(cli): apply panel-ownership filter once before compact-mode decision Mixed live groups (running subagent + sibling tool) leaked the panel-owned subagent into `CompactToolGroupDisplay`'s count and `getActiveTool` selection, because `showCompact` returned BEFORE the inline `.map()` filter ran. Compact-mode users would see e.g. `task × 2 Delegate task to subagent` even though LiveAgentPanel already owned the subagent row below the composer. Derive `inlineToolCalls` once via `useMemo` immediately after the existing hook block and use it consistently for the compact summary, sizing math, and the render map. The early-return for "all-entries-panel-owned" collapses into `inlineToolCalls.length === 0` (gated on `isPending` so the legacy empty-input committed-phase snapshot is preserved). Remove the inner `.map()` filter — the upstream derivation already excluded the same entries. JSDoc updates: - `ToolGroupMessageProps.isPending` now describes the real flow (build inlineToolCalls / force-expand / forward to ToolMessage for parity). - `ToolMessageProps.isPending` is documented as forwarded-but-inert (`SubagentExecutionRenderer` doesn't gate on it; the live-phase filter and the unconditional terminal summary do the actual work). Regression test: live mixed group in compact mode → sibling wins active-tool, count collapses to 1, no `× 2` suffix, no subagent description in the header. Addresses Copilot review comments 3205262972 / 3205263020 (doc/code mismatch) and gpt-5.5 critical 3205288299 (compact-mode leak). * fix(cli): force-expand compact groups on terminal subagent in live phase too Resolved comment 3203286936 codified the design intent that `SubagentScrollbackSummary` "fires in BOTH live and committed phases" to bridge `unregisterForeground`'s post-delete panel-snapshot drop and the parent turn committing. Non-compact mode honored that contract (terminal subagents render the summary inline whenever they appear in `inlineToolCalls`), but compact mode still gated `hasCommittedTerminalSubagent` on `!isPending`, so a foreground subagent finishing mid-turn under compact mode produced NOTHING inline until the parent committed — exactly the gap the bridge was meant to close. Drop the `!isPending` arm and rename `hasCommittedTerminalSubagent` → `hasTerminalSubagent`. The force-expand now applies to terminal subagents in either phase; compact-mode users see the same outcome line non-compact users already get. Mirrors `SubagentExecutionRenderer`'s ungated terminal-summary path and `mergeCompactToolGroups.isForceExpandGroup`'s no-isPending-gate preprocessing rule. Tests: - Flip "compact mode: live group with completed subagent stays compact" → "force-expands so the summary bridges the panel-snapshot drop". Update rationale to reflect post-#3921 reality (panel evicts terminal foreground rows immediately). - Add "compact mode: live mixed group with terminal subagent + sibling force-expands and renders both" — covers the bridge in mixed groups. - Update two stale `hasCommittedTerminalSubagent` cross-references in `mergeCompactToolGroups.{ts,test.ts}` comments.
…entPanel (#3909) * feat(cli): replace inline AgentExecutionDisplay with always-on LiveAgentPanel Surface running subagents in a borderless, always-on roster anchored beneath the input footer (mirrors Claude Code's CoordinatorTaskPanel) and retire the verbose inline `AgentExecutionDisplay` frame whose per-tool-call mutations caused scrollback flicker. Detail / cancel / resume keep flowing through the existing BackgroundTasksDialog. LiveAgentPanel: - Two-column row: status icon + (optional) type + description + activity on the left (truncate-end), elapsed + tokens on the right in a flex-shrink:0 column so the cost/time fields are never hidden by long descriptions. - Re-pulls each agent from BackgroundTaskRegistry on every wall-clock tick so `recentActivities` stays fresh — the snapshot from `useBackgroundTaskView` only refreshes on `statusChange` to keep the footer pill / AppContainer quiet under heavy tool traffic. - Reaches for Config via raw ConfigContext (not useConfig) so the panel degrades to snapshot-only when no provider is mounted (test isolation). - Hides when any dialog is visible (auth / permission / bg tasks) and self-hides when no agent entries are live. - Drops `subagentType` from the row when it is the default `general-purpose` builtin to keep the line uncluttered; specialized types still bold-anchor the row. - Keeps terminal entries on screen for 8s so the user gets feedback when an agent finishes, then they fall off (BackgroundTasksDialog retains them long-term). Inline frame retirement: - ToolMessage's SubagentExecutionRenderer collapses to the focus- routed approval surfaces only (focus-holder banner + queued marker). All other agent state is owned by the panel + dialog. - AgentExecutionDisplay.tsx + test removed (-918 lines); the subagents/index export is dropped with a pointer to the new surfaces. Net diff: +97 / -1069. * fix(cli): move elapsed + tokens to the front of LiveAgentPanel rows The two-column layout used `flex-grow:1` on the left description column, which puffed it out to fill the row even when content was short — leaving a visible gap between the description tail and the right-pinned elapsed/tokens whenever the terminal was wider than the content. Worse, the gap made it look like display space was being wasted while the description still got truncated. Move elapsed + tokens to the front of the row (right after the status icon) so: - Time and cost are pinned at a stable left position and are NEVER at risk of being truncated, regardless of description / activity length. - The row reads as one tight left-to-right line — no flex-grow, no internal gap. On wide terminals the unused width sits at the row tail (invisible), where it belongs. - Description + activity become the truncatable tail; `truncate-end` cuts only when the line genuinely overflows the panel width. Also wrap the icon-plus-spaces span in a template literal so the two spaces of breathing room after the glyph survive a prettier pass. Verified at 60 / 100 / 200 cols: at 200 cols the row renders flush with no trailing ellipsis and no internal gap; at 60 cols the time + tokens stay at the front and the description tail truncates with `…`. * fix(cli): switch LiveAgentPanel row to layout C (right-pinned, no flex-grow) Iterating on the row layout based on visual review: - Layout A (time first, single Text) put numbers ahead of identity, which broke the natural left-to-right reading order. - Layout B (right-pinned with flex-grow:1 on left) puffed the left column out to fill the row, leaving a visible gap between the description tail and the right-pinned elapsed when the terminal was wider than the content. Layout C keeps the right column flex-shrink:0 so elapsed + tokens are never clipped, but DROPS flex-grow on the left so the two columns sit side-by-side: empty slack falls off the row tail (invisible) instead of opening a gap inside the row. Identity (type) and intent (description / activity) read first, cost reads last — matching the natural visual hierarchy. When the row overflows the panel width the left column truncates with `…` mid-row, while elapsed + tokens stay intact. Verified at 60 / 100 / 200 cols — at 200 cols there is no internal gap and no trailing ellipsis; at 60 cols time + tokens stay visible on the right and the description / activity tail truncates with `…`. * fix(cli): port Claude Code's bullet + arrow visual to LiveAgentPanel rows Adopt the leaked CoordinatorTaskPanel visual conventions: - `○` replaces `⊷` for live (running) slots — matches Claude Code's use of `figures.circle` for the active-agent bullet, gives a uniform list look across the running roster. Terminal states keep distinct check / cross marks (✔ / ✖) so they're easy to scan at a glance. - `▶` separates the description from elapsed / tokens, mirroring Claude's `PLAY_ICON` suffix marker. - Activity is wrapped in `( ... )` so it reads as an annotation on the description rather than a sibling field, and the type prefix switches from ` · ` to `: ` (e.g. `editor: tighten import order`) to match Claude's `name: description` pattern. The two-column flex layout from layout C is preserved — left column flex-shrink:1 with truncate-end, right column (` ▶ Ns · Nk tokens`) flex-shrink:0 so elapsed + tokens are never clipped, regardless of how long the description / activity grows. This is the one intentional divergence from Claude's literal pattern, which puts elapsed at the row tail without pinning and lets it disappear off narrow terminals. Verified at 60 / 100 / 200 cols: at 200 cols the row is flush with no internal gap; at 60 cols the description / activity tail truncates with `…` while elapsed + tokens stay visible on the right. * fix(cli): widen LiveAgentPanel, drop [in turn] marker, point overflow at dialog Three usability fixes from review: 1. Use `terminalWidth` instead of `mainAreaWidth`. The latter is capped at 100 cols (intended for markdown / code where soft-wrap matters), which on a 200-col terminal left half the screen empty to the right of an already-truncating row. Live progress lines have nothing to soft-wrap, so the panel wants the full width. 2. Drop the `[in turn]` foreground marker. The flavor distinction matters in BackgroundTasksDialog (cancel semantics differ for foreground vs background entries) but in the glance panel the marker reads as cryptic noise — users asked what it meant. Keep the dialog as the surface that surfaces it. 3. Annotate the overflow callout with `(↓ to view all)`. The panel is intentionally read-only (it has no keyboard focus so it can't steal input from the composer), so when the roster outgrows the row budget we point users at the existing dialog — same keystroke the footer pill uses, kept in sync so users only learn one gesture. * fix(cli): make Down on focused BackgroundTasksPill open the dialog The focus chain Composer → AgentTabBar → BackgroundTasksPill is walked with the Down arrow, but Down dead-ended at the pill — the pill only opened the dialog on Enter. Users who followed the LiveAgentPanel's "(↓ to view all)" overflow callout reached the highlighted pill and got stuck there, defeating the hint. Route Down on the focused pill into openDialog so the chain completes naturally: Composer ↓ → AgentTabBar ↓ → Pill ↓ → Dialog. Enter still works, so existing muscle memory keeps functioning. * fix(cli): address Copilot review on LiveAgentPanel — interval gate, ghost rows, dead doc Three findings from Copilot's PR review: 1. **1s interval kept ticking forever after expiry.** The gate was `entries.some(isAgentEntry)`, but `BackgroundTaskRegistry.getAll()` retains terminal entries indefinitely — once the last visible row passed its 8s window the panel returned null but the interval kept firing setNow each second, churning re-renders for nothing on screen. The gate now considers visibility (running / paused OR terminal-within-window) and the interval clears itself once the condition flips false. New entries restart the interval via the `entries` dep. 2. **Ghost rows when registry forgets an entry.** The live re-pull fell back to the snapshot when `registry.get()` returned undefined. The canonical case is a foreground subagent that unregisters silently after its statusChange fires (`unregisterForeground` deletes without emitting a follow-up transition) — the snapshot still says `running`, so the row would never clear. Trust the registry: when it says the entry is gone, drop the row. The snapshot-only fallback is preserved for the no-Config case (test fixtures). 3. **Dead doc reference.** The trailing comment in `subagents/index.ts` pointed at `docs/comparison/subagent-display-deep-dive.md`, which doesn't exist in this repo (it lives in the codeagents knowledge base, not qwen-code). Dropped the dead pointer; the in-tree pointers to `LiveAgentPanel` and `BackgroundTasksDialog` already tell readers where to look. Coverage delta: +2 cases on `LiveAgentPanel.test.tsx` — `drops snapshot rows the live registry no longer knows about` (issue #2) and `still shows the snapshot when no Config is mounted` (locks in the test-fixture fallback that issue #2's stricter rule would otherwise have broken). * fix(cli): address Copilot second-pass review — dialogOpen tick gate, isFocused doc Two further findings from Copilot: 1. **Interval kept ticking while bg-tasks dialog was open.** The first-pass fix already torn the tick down once all rows expired from the visibility window, but `dialogOpen` was a separate reason the panel returned null and was missed by the gate. Add `dialogOpen` to the useEffect deps and short-circuit when true, so the dialog's tenure is interval-free. 2. **Stale `isFocused` doc comment in `ToolMessageProps`.** The comment claimed the prop controlled the now-retired `Ctrl+E / Ctrl+F` display shortcuts (those died with the inline `AgentExecutionDisplay` frame). Rewrite the comment to describe the only remaining behavior — the focus-routed approval surface (focus-holder banner vs. queued-sibling marker). Coverage delta: +1 case on `LiveAgentPanel.test.tsx` — `tears the 1s tick down when the bg-tasks dialog opens` advances 60s of fake time with `dialogOpen=true` and asserts no panel state drift. * fix(cli): reconcile registry-missing snapshots as just-finished + address review nits Hot-fix: the previous round's "drop the row when registry.get() returns undefined" was too aggressive. `unregisterForeground` calls `emitStatusChange(entry)` BEFORE it deletes the entry, so the snapshot useBackgroundTaskView captures still says "running" while the very next render's registry.get sees nothing. Dropping the row outright made foreground subagents disappear from the panel the instant they finished — users saw "SubAgents 不显示了" on tasks that ran-and-immediately-completed. Reconciliation now has three branches: 1. live found → use live (newest recentActivities). 2. snap says still-live but registry forgot → synthesize a terminal version with endTime pinned to the FIRST observation so the 8s visibility window gives the user a "the agent finished" beat then evicts cleanly. The first-seen-missing timestamp is held in a useRef map (without it, each tick resets endTime to `now` and the row never expires). 3. snap is already terminal but registry forgot → drop (no useful state to keep showing). Also addresses three smaller review notes (deepseek-v4-pro via /review): - DEFAULT_SUBAGENT_TYPE is now imported from @qwen-code/qwen-code-core (a new DEFAULT_BUILTIN_SUBAGENT_TYPE export referenced by both BuiltinAgentRegistry's seed entry and the panel's default-type elision). A backend rename now propagates instead of silently re-introducing the redundant `general-purpose:` prefix on every row. - The useMemo body now reads `now` (as `reconcileAt`) so the dependency is semantically honest — a future "remove dead dep" cleanup can no longer silently freeze the panel on the first tool-call after a snapshot refresh. Coverage delta: +5 cases on LiveAgentPanel.test.tsx — token rendering on completed entries, status-icon routing for paused / failed / cancelled (parametrized), case-insensitive prefix stripping in descriptionWithoutPrefix, plus the rewritten ghost-row case (synthesized terminal lingers 8s then evicts) and a sibling case asserting already-terminal snapshots with empty registry still drop. 17 LiveAgentPanel tests pass. * refactor(cli): split LiveAgentPanelBody + drop dead isPending/isWaitingForOtherApproval props Two structural reviews from deepseek-v4-pro via /review: 1. **Hook-order footgun.** The `if (dialogOpen) return null` guard sat between the hook block and a substantial block of pure-render code; an extension that added `useMemo`/`useRef`/`useCallback` below the guard would crash with "Rendered fewer hooks than expected" the next time `dialogOpen` toggled. Extract the pure- render logic into `LiveAgentPanelBody` so the guard becomes the parent's last statement, and any future "add a hook to the body" refactor naturally lands in the inner component (hook-free today, hook-free for as long as it stays a presentational FC). 2. **Dead pass-through removed.** `isPending` and `isWaitingForOtherApproval` were dead in the subagent renderer (LiveAgentPanel + BackgroundTasksDialog own that surface) but ToolGroupMessage still computed `isWaitingForOtherApproval` and forwarded both into ToolMessage. Drop them from `ToolMessageProps`, drop the computation + forwarding in ToolGroupMessage, drop the test factory references. ToolGroupMessage keeps `isPending` on its own props for upstream caller compatibility (HistoryItemDisplay et al. forward it) but stops destructuring it; the doc comment now points at the surfaces that own that gating today. Coverage: existing 115 cases across LiveAgentPanel / BackgroundTasksDialog / BackgroundTasksPill / ToolMessage / ToolGroupMessage all green; the panel split is purely structural and the dead-prop removal was verified TS-clean before commit. * fix(cli): map internal tool names to user-facing display names in LiveAgentPanel rows `recentActivities[].name` carries the internal tool name from AgentToolCallEvent (e.g. `run_shell_command`, `glob`). Rendering it verbatim surfaced raw identifiers in the panel (`run_shell_command rg TODO`) while BackgroundTasksDialog already mapped through ToolDisplayNames to show user-facing names (`Shell rg TODO`) — the two surfaces' vocabularies drifted on the same data. Mirror the dialog's `TOOL_DISPLAY_BY_NAME` lookup so the panel and dialog speak the same vocabulary. Added a test asserting `run_shell_command` renders as `Shell` and the raw name is not surfaced. Coverage delta: +1 case on LiveAgentPanel.test.tsx (18 total). * fix(cli): keep already-terminal snapshots visible until TTL + close 4 test gaps 1. **Terminal snap + missing registry now follows the visibility window.** Cancelled / failed foreground subagents go through `cancel`/`fail` (which stamp `endTime` and emit statusChange) followed by `unregisterForeground` (which deletes silently). The snap captures the real `endTime`, so the previous "drop on missing registry" branch made cancelled / failed foreground tasks disappear instantly — contradicting the panel's "brief terminal visibility" contract that the synthesized-completion path also relies on. Keep the snap as-is when `endTime` is set; the visibleAgents filter evicts it after `TERMINAL_VISIBLE_MS` like any other terminal entry. Defensive fallback drops the pathological "terminal status with no endTime" shape. (Copilot finding on PRRT_kwDOPB-92c6AS4z9.) 2. **Close 4 test gaps flagged by tanzhenxin's review:** - `elides the default 'general-purpose' subagent type from the row` locks in DEFAULT_BUILTIN_SUBAGENT_TYPE comparison. - `truncates the description tail when the panel width is too narrow` exercises the `width` prop (existing cases ignored it) and anchors on the right-pinned `▶ 3s` tail staying intact. - `clears the 1s tick interval when unmounted with live work in flight` spies on setInterval / clearInterval so a discarded fiber can't keep firing setNow. - `keeps terminal snapshots visible until the TTL even when the registry forgot them` covers the cancelled / failed foreground reconciliation path with a `✖` glyph + 9s eviction assertion. Coverage: 22 LiveAgentPanel tests pass (was 18). All TS clean. * refactor(cli): rename FOREGROUND_ROW_PREFIX from `[in turn]` to `[blocking]` User feedback: `[in turn]` reads as "queued / sequential" — the opposite of what it actually means (the row is blocking the user's current turn). Even maintainers had to chase the source comment to confirm the semantics, which is a strong signal that end users are not getting the warning the prefix is supposed to deliver. `[blocking]` reads more directly: "this row is what's holding up your input", which is what cancelling it actually unblocks. Update the in-row prefix and the cancel-confirmation hint in lockstep (`x again to confirm stop · ends the blocking turn`) so the two surfaces share vocabulary. LiveAgentPanel still suppresses the marker in its row (the panel has no cancel surface, so the warning has nothing actionable to pair with); update the historical comment + reinforce the test guard so neither the legacy `[in turn]` nor the new `[blocking]` bleeds into the glance roster. * fix(cli): address 4 review findings — height budget, neutral synthesis glyph, stale comments 1. **`availableTerminalHeight` regression** (claude-opus-4-7 via /qreview, DefaultAppLayout.tsx:127). LiveAgentPanel rendered OUTSIDE the `mainControlsRef` Box, so its 2-7 rows were not measured by `controlsHeight` and not subtracted from `availableTerminalHeight = terminalHeight - controlsHeight - staticExtraHeight - 2 - tabBarHeight` in AppContainer. Pending tool results in MainContent could render past the visible area and push the composer / panel off-screen — a regression vs PR #3768 which suppressed the inline frame in the live phase. Move the panel INSIDE the `mainControlsRef` Box so `measureElement` picks up its rows automatically; no new infrastructure needed. 2. **Neutral glyph for synthesized terminal rows** (claude-opus-4-7, LiveAgentPanel.tsx:278). The synthesis branch hardcoded `status: 'completed'` regardless of the actual outcome. Foreground subagents that errored or were cancelled were rendered with the green ✔ for 8s — directly contradicting the inline tool result the user just saw (`Subagent execution failed.` / `Agent was cancelled by the user.`). Add a `synthesized` flag on the synthesized rows; `statusIcon` checks it first and returns a neutral `·` + secondary color, so the panel never lies about an outcome it cannot determine. Status stays `'completed'` purely so the visibility-window filter treats the row as terminal. 3. **PR description claim retired** (Copilot, ToolMessage.tsx:411). The body's Reviewer Notes section claimed `isPending` / `isWaitingForOtherApproval` were kept on `ToolMessageProps` as vestigial pass-through; the props were actually removed in commit 93036b1. Will update the PR description in a follow-up non-code edit (the comment thread itself is on now-outdated line 411). 4. **Stale comment in ToolGroupMessage** (Copilot, ToolGroupMessage.tsx:303). The comment above the focus-routing logic referenced the retired Ctrl+E / Ctrl+F display shortcuts. Rewrite it to describe the current behavior — focus routing for inline approval prompts only — and call out where the live progress / drill-down moved (LiveAgentPanel + BackgroundTasksDialog). Coverage delta: existing `reconciles snapshots…` test rewritten to assert `·` instead of `✔` (and explicitly assert `not.toContain('✔')`); new sibling `keeps the success glyph for entries the registry still tracks (non-synthesized)` locks the non-synthesis path against a future regression that always returns the neutral glyph. 68 background-view tests + 128 messages/layouts tests pass. * fix(cli): ANSI-escape user-controlled strings + restore one-line scrollback summary Two findings from deepseek-v4-pro via /review: 1. **ANSI sanitization on the panel** (LiveAgentPanel.tsx:368). The row was rendering `entry.subagentType`, `descriptionWithoutPrefix` output, and `recentActivities[].description` straight through Ink's `<Text>` — both subagent config (user-authored) and tool-call description (LLM-generated) can contain terminal control sequences that bleed through and corrupt the panel chrome. Apply `escapeAnsiCtrlCodes` to all three (HistoryItemDisplay does the same on its user-facing content for the same reason). 2. **One-line scrollback summary for terminal subagents** (ToolMessage.tsx:295). The previous round retired the verbose inline frame entirely and SubagentExecutionRenderer returned null for all non-approval states. The result: completed subagent results disappeared from scrollback the moment LiveAgentPanel's 8s window expired. Reopening a session or dismissing the dialog left no record. Add a single-line `SubagentScrollbackSummary` for completed / failed / cancelled states — `<icon> <name>: <description> · N tools · Xs · Yk tokens` (plus terminateReason on non-success). One row per agent, no flicker risk; the verbose frame stays retired. Coverage: +3 cases — `escapes ANSI control codes in user-controlled strings` (LiveAgentPanel), `completed subagent → renders a one-line scrollback summary` and `failed subagent → renders summary with terminate reason` (ToolMessage). 24 + 34 panel/ToolMessage tests pass; broader 2894-test cli/ui sweep clean. Note: this restores some inline rendering that the original feature choice ("inline AgentExecutionDisplay 完全移除") removed entirely. The compromise — one line per terminal agent vs the old 15-row frame — preserves history while keeping the flicker fix that motivated the retirement. * fix(cli): paused agents count as active in tally + sync reconciliation comment Two findings from Copilot: 1. **Header tally now matches what the user sees.** The numerator was `runningCount` (status === 'running'), but the panel also renders paused entries as active rows (warning color, ⏸ glyph). With only paused agents present the header read "(0/1)" while the row was clearly visible — a confusing mismatch. Rename to `activeCount` and include paused in the count. New test `counts paused agents as active in the header tally` locks the tally + glyph in. 2. **Reconciliation block comment now describes the four real paths**, not the older three: 1. live found → use live 2. snap still-live + registry forgot → synthesize neutral terminal 3. snap terminal + endTime present → keep snap, let TTL filter evict it (the cancelled / failed foreground path the previous round added) 4. snap terminal + no endTime → drop (upstream invariant violation) Comment is now in lockstep with the implementation; the prior "drop terminal-with-empty-registry outright" wording was stale after the TTL-keep change. Coverage: 25 LiveAgentPanel tests pass (was 24).
…Change emit (#3919) * fix(cli,core): isPending gate on subagent scrollback summary + post-delete statusChange emit Two follow-ups from PR #3909 review. 1. **Re-introduce `isPending` gate on `SubagentExecutionRenderer`'s scrollback summary** (Copilot finding on PRRT_kwDOPB-92c6AUQHn). The verbose inline frame retirement collapsed `SubagentExecutionRenderer` to "render the summary whenever a subagent reaches a terminal status" — but with `isPending` removed in #3909, that fired in BOTH live (pendingHistoryItems) AND committed (Static) phases. Live-phase rendering duplicated the row LiveAgentPanel already paints below the composer until the parent turn committed. Add `isPending` back to `ToolMessageProps` purely as a gate for this one render path: the summary fires only when `!isPending` (committed). `ToolGroupMessage` forwards the flag (it kept the prop on its own interface for upstream compat the whole time). Test gap closed by the new `live (isPending) terminal subagent → no scrollback summary (panel owns the row)` case. 2. **Emit `statusChange` AFTER delete in `unregisterForeground`** (Copilot finding on PRRT_kwDOPB-92c6AUQGc + the panel-only reconciliation it spawned). The shared snapshot in `useBackgroundTaskView` only refreshes on `statusChange`, and `unregisterForeground` previously fired exactly once — BEFORE delete — so the snapshot froze with the agent as "running" while `registry.get()` returned undefined. Result: `BackgroundTasksDialog` list mode showed a ghost "running" row with cancel hints whose `x` was a no-op, contradicting what the panel already showed (synthesized neutral terminal). Fire `statusChange` a second time AFTER `agents.delete()` so snapshot consumers see the registry-less state and stop surfacing the agent. The first emit still mirrors complete/fail/cancel/finalize ordering (callbacks that re-read `registry.get` see the entry); the second emit is the new contract for snapshot-based views. React batches the two resulting setState calls into one re-render so consumers re-render exactly once. Updated the existing "emits status change before removing the entry" test to capture both emits and explicitly assert that the second observes the registry-less state. Added a sibling test covering the post-delete `getAll()` count. Coverage: 190 passing tests across core + cli (background-view + ToolMessage + ToolGroupMessage + useBackgroundTaskView). * fix(cli,core): compact-mode terminal subagent expansion + statusChange context flag Five review findings on PR #3919: 1. **Compact mode bypassed the scrollback summary** (gpt-5.5 via /qreview, ToolGroupMessage:324). `ToolGroupMessage` returns `CompactToolGroupDisplay` before the ToolMessage path when `compactMode === true`, so the new `isPending` gate on `SubagentExecutionRenderer` only protected the expanded path — committed terminal subagents in compact mode never reached `SubagentScrollbackSummary` and the LiveAgentPanel → committed- summary handoff broke for users who turned compact mode on. Force-expand the group when `!isPending` AND any tool call has a terminal `task_execution` resultDisplay. Stay compact while the parent turn is still live (`isPending`) — the panel below the composer owns that surface and an inline summary would duplicate it. Coverage: 4 new ToolGroupMessage cases (compact + completed-committed expands; compact + running-live stays compact; compact + completed-live stays compact; compact + failed-committed expands). 2. **Snapshot-coupled comment in `packages/core`** (Copilot, background-tasks.ts:292). The comment named CLI/UI consumers (`useBackgroundTaskView`, `BackgroundTasksDialog`) and asserted React batching guarantees from a core file. Reword to "snapshot-style consumers that re-pull `getAll()` from inside the callback" and drop the framework-specific batching claim. 3. **Two-phase emit needed an explicit signal** (Copilot, background-tasks.ts:283). Emitting `statusChange` twice without distinguishing the phases forced consumers to either do duplicate work or risk persisting a stale `entry` from the second callback. Add an optional second arg `context?: { removed?: boolean }` to `BackgroundStatusChangeCallback`; the post-delete emit passes `{ removed: true }` so consumers can disambiguate without re-querying the registry. Backwards compatible — existing callbacks ignore the new arg. Tests updated to assert both `mock.calls[0][1] === undefined` and `mock.calls[1][1] === { removed: true }`. 4. **`isPending` doc clarified** (Copilot, ToolMessage.tsx:507). Made the default semantics explicit: omitted/undefined is treated as committed (not pending); live-area renderers MUST pass `true` explicitly to suppress the scrollback summary. 5. (4 of the threads were duplicate Copilot fires of #2 + #3.) Coverage: 219 test files / 3369 passing across cli/ui + core/agents. * docs(cli): update ToolGroupMessageProps.isPending JSDoc The previous prop comment claimed `isPending` was "not consumed by the group body" — true at the time, but the body now reads it for two real purposes (compact-mode gating + forwarding to ToolMessage). Update the doc so future callers / tests don't treat it as legacy. Addresses Copilot finding on PRRT_kwDOPB-92c6AYE0V. * fix(cli): hide live-phase subagent tool entries — LiveAgentPanel owns the row User report: with compact mode OFF, a running subagent shows up twice — once as the parent tool group's `task` row (status icon + name + description), once as the LiveAgentPanel row beneath the composer. Same agent, two surfaces, redundant. Filter `task_execution` tool entries out of the expanded `ToolGroupMessage` while `isPending=true` so the panel is the single source of truth for in-flight subagents. The entry returns once the parent turn commits (`isPending=false`), letting `SubagentScrollbackSummary` land inside the parent's tool group as a persistent audit trail. Exception: subagents with a pending approval still render, because the focus-routed banner / queued marker is the only inline surface that lets users answer the prompt without opening the dialog. If a group is purely panel-owned (e.g. a single Task call with no sibling tools), the entire `ToolGroupMessage` returns `null` so an empty bordered container doesn't float above the panel. Coverage: +4 ToolGroupMessage cases — running entry hidden in live phase / mixed group keeps siblings / pending-approval entry still renders / committed entry comes back for the audit trail. * refactor(cli): tighten subagent-tool helper naming + ANSI-safe scrollback summary Self-audit + independent review found 5 cleanup items on the live-phase hide path; all addressed in one commit since none are behavioral changes: 1. **Move `allEntriesPanelOwned` short-circuit BEFORE `showCompact`** so a pure-subagent group in compact mode is also hidden during the live phase (previously CompactToolGroupDisplay rendered a single summary line above the panel — a mild duplicate on top of what the non-compact path already fixed). 2. **Rename `isLiveSubagentTool` → `isSubagentToolEntry`.** The helper identifies a tool's resultDisplay shape; it doesn't check live-state. The previous name conflated "predicate" with "use case" and read as if it returned true only during the live phase. 3. **DRY up `hasCommittedTerminalSubagent`** to use `isSubagentToolEntry` instead of inlining its own type-narrowing. 4. **ANSI-escape `subagentName` / `taskDescription` / `terminateReason`** in `SubagentScrollbackSummary`. Same threat model as the panel rows and HistoryItemDisplay — these strings come from subagent config (user-authored) and LLM output and could carry terminal control sequences. The stats fields (tool count / duration / tokens) flow through trusted formatters and don't need escaping. 5. **Doc comments updated** to reflect the four real responsibilities of `isPending` on `ToolGroupMessageProps` (hide pure groups, force-expand committed compact, per-tool filter, forward to ToolMessage), to clarify that the keyboard-focused subagent id can point at a hidden tool harmlessly (the iterator returns `null` before the focus prop is computed), and to drop the redundant "EXCEPT" clause on the per-tool filter in favor of a single sentence. Coverage unchanged: 251 passing tests across messages / background-view / core/agents; broader 3374-test sweep clean; TS clean on both cli and core packages. * fix(cli,core): address 3 critical review findings + ANSI/doc cleanups Three real bugs flagged by gpt-5.5 via /qreview, plus 4 doc / sanitization nits from Copilot. All 7 threads close together since they share the same surfaces. ## Critical fixes 1. **Foreground subagents disappeared mid-parent-turn** (PRRT_kwDOPB-92c6AYvL9). Post-#3921 swap-order, `unregisterForeground` drops the entry from the panel snapshot the moment the subagent finishes. The previous round's `!isPending` gate on `SubagentScrollbackSummary` then suppressed the inline summary too, leaving the user with nothing on screen for the run until the parent committed. - Drop the `!isPending` gate — `unregisterForeground` already removes the row from the panel, so the inline summary can fire in BOTH live and committed phases without duplicating it. - Tighten the `ToolGroupMessage` live-phase hide so it only filters `running` / `paused` / `background` task entries (`isPanelOwnedSubagentTool`), not terminal ones. Terminal entries pass through immediately so the summary lands. - The "panel-owned" predicate is now distinct from the broader "subagent tool entry" predicate (`isSubagentToolEntry`) and the "terminal subagent" predicate (`isTerminalSubagentTool`); each usage site picks the one it actually means. 2. **Compact mode dropped the scrollback summary** (PRRT_kwDOPB-92c6AYvLw). Force-expanding the group made the container go through the expanded path, but `ToolMessage`'s own compact-mode gate (`!compactMode || forceShowResult ? renderer : 'none'`) still suppressed the result block, so `SubagentScrollbackSummary` never rendered for compact-mode users. Pass `forceShowResult={true}` for terminal subagent tool entries so the result block is always rendered. 3. **`mergeCompactToolGroups.isForceExpandGroup` didn't know about terminal subagents** (PRRT_kwDOPB-92c6AYvMC). The committed- history preprocessor merged adjacent tool_groups before render, so a terminal `task_execution` group could be absorbed into a compact batch (its `tool_use_summary` label dropped), and the render-time force-expand check never got a chance to override. Mirror the `hasCommittedTerminalSubagent` predicate inside `isForceExpandGroup` so preprocessing and rendering agree. ## Doc / sanitization nits - `BackgroundStatusChangeCallback` doc now lists every emitter (register / complete / fail / cancel / finalizeCancelled / finalizeCancellationIfPending / abandon / unregisterForeground / reset) and groups them by ordering camp (keeps-the-entry vs removes-the-entry — `reset` joins `unregisterForeground` in the delete-then-emit camp). - ANSI-escape `data.subagentName` in the focus-holder banner and the queued marker (`SubagentExecutionRenderer`) — same threat model as the panel rows and `SubagentScrollbackSummary`. ## Coverage delta - New ToolMessage case: live-phase terminal subagent now renders inline (replaces the prior "no scrollback summary" assertion that was the symptom of the AYvL9 bug). - New ToolGroupMessage cases: terminal subagent in live phase renders inline; `forceShowResult=true` propagates for terminal subagent tools (mock now exposes the prop). - New mergeCompactToolGroups parametrized cases: terminal subagent in any of completed / failed / cancelled stays its own batch. 280 tests pass across cli messages + utils + background-view + core/agents. TS clean. * fix(cli): drop `'paused'` arm from isPanelOwnedSubagentTool — not in AgentResultDisplay union CI Lint failed with TS2367: the previous round's `isPanelOwnedSubagentTool` checked for `status === 'paused'` but `AgentResultDisplay.status` (the tool-result-side type) only carries `'running' | 'completed' | 'failed' | 'cancelled' | 'background'`. The `'paused'` status lives on the registry-side `BackgroundTaskStatus` union and is only ever surfaced through `LiveAgentPanel` directly, never through a `task_execution` payload. Drop the dead arm and add a comment so a future "let's also check paused here" doesn't get re-introduced. * fix(cli): apply panel-ownership filter once before compact-mode decision Mixed live groups (running subagent + sibling tool) leaked the panel-owned subagent into `CompactToolGroupDisplay`'s count and `getActiveTool` selection, because `showCompact` returned BEFORE the inline `.map()` filter ran. Compact-mode users would see e.g. `task × 2 Delegate task to subagent` even though LiveAgentPanel already owned the subagent row below the composer. Derive `inlineToolCalls` once via `useMemo` immediately after the existing hook block and use it consistently for the compact summary, sizing math, and the render map. The early-return for "all-entries-panel-owned" collapses into `inlineToolCalls.length === 0` (gated on `isPending` so the legacy empty-input committed-phase snapshot is preserved). Remove the inner `.map()` filter — the upstream derivation already excluded the same entries. JSDoc updates: - `ToolGroupMessageProps.isPending` now describes the real flow (build inlineToolCalls / force-expand / forward to ToolMessage for parity). - `ToolMessageProps.isPending` is documented as forwarded-but-inert (`SubagentExecutionRenderer` doesn't gate on it; the live-phase filter and the unconditional terminal summary do the actual work). Regression test: live mixed group in compact mode → sibling wins active-tool, count collapses to 1, no `× 2` suffix, no subagent description in the header. Addresses Copilot review comments 3205262972 / 3205263020 (doc/code mismatch) and gpt-5.5 critical 3205288299 (compact-mode leak). * fix(cli): force-expand compact groups on terminal subagent in live phase too Resolved comment 3203286936 codified the design intent that `SubagentScrollbackSummary` "fires in BOTH live and committed phases" to bridge `unregisterForeground`'s post-delete panel-snapshot drop and the parent turn committing. Non-compact mode honored that contract (terminal subagents render the summary inline whenever they appear in `inlineToolCalls`), but compact mode still gated `hasCommittedTerminalSubagent` on `!isPending`, so a foreground subagent finishing mid-turn under compact mode produced NOTHING inline until the parent committed — exactly the gap the bridge was meant to close. Drop the `!isPending` arm and rename `hasCommittedTerminalSubagent` → `hasTerminalSubagent`. The force-expand now applies to terminal subagents in either phase; compact-mode users see the same outcome line non-compact users already get. Mirrors `SubagentExecutionRenderer`'s ungated terminal-summary path and `mergeCompactToolGroups.isForceExpandGroup`'s no-isPending-gate preprocessing rule. Tests: - Flip "compact mode: live group with completed subagent stays compact" → "force-expands so the summary bridges the panel-snapshot drop". Update rationale to reflect post-#3921 reality (panel evicts terminal foreground rows immediately). - Add "compact mode: live mixed group with terminal subagent + sibling force-expands and renders both" — covers the bridge in mixed groups. - Update two stale `hasCommittedTerminalSubagent` cross-references in `mergeCompactToolGroups.{ts,test.ts}` comments.
…Change emit (QwenLM#3919) * fix(cli,core): isPending gate on subagent scrollback summary + post-delete statusChange emit Two follow-ups from PR QwenLM#3909 review. 1. **Re-introduce `isPending` gate on `SubagentExecutionRenderer`'s scrollback summary** (Copilot finding on PRRT_kwDOPB-92c6AUQHn). The verbose inline frame retirement collapsed `SubagentExecutionRenderer` to "render the summary whenever a subagent reaches a terminal status" — but with `isPending` removed in QwenLM#3909, that fired in BOTH live (pendingHistoryItems) AND committed (Static) phases. Live-phase rendering duplicated the row LiveAgentPanel already paints below the composer until the parent turn committed. Add `isPending` back to `ToolMessageProps` purely as a gate for this one render path: the summary fires only when `!isPending` (committed). `ToolGroupMessage` forwards the flag (it kept the prop on its own interface for upstream compat the whole time). Test gap closed by the new `live (isPending) terminal subagent → no scrollback summary (panel owns the row)` case. 2. **Emit `statusChange` AFTER delete in `unregisterForeground`** (Copilot finding on PRRT_kwDOPB-92c6AUQGc + the panel-only reconciliation it spawned). The shared snapshot in `useBackgroundTaskView` only refreshes on `statusChange`, and `unregisterForeground` previously fired exactly once — BEFORE delete — so the snapshot froze with the agent as "running" while `registry.get()` returned undefined. Result: `BackgroundTasksDialog` list mode showed a ghost "running" row with cancel hints whose `x` was a no-op, contradicting what the panel already showed (synthesized neutral terminal). Fire `statusChange` a second time AFTER `agents.delete()` so snapshot consumers see the registry-less state and stop surfacing the agent. The first emit still mirrors complete/fail/cancel/finalize ordering (callbacks that re-read `registry.get` see the entry); the second emit is the new contract for snapshot-based views. React batches the two resulting setState calls into one re-render so consumers re-render exactly once. Updated the existing "emits status change before removing the entry" test to capture both emits and explicitly assert that the second observes the registry-less state. Added a sibling test covering the post-delete `getAll()` count. Coverage: 190 passing tests across core + cli (background-view + ToolMessage + ToolGroupMessage + useBackgroundTaskView). * fix(cli,core): compact-mode terminal subagent expansion + statusChange context flag Five review findings on PR QwenLM#3919: 1. **Compact mode bypassed the scrollback summary** (gpt-5.5 via /qreview, ToolGroupMessage:324). `ToolGroupMessage` returns `CompactToolGroupDisplay` before the ToolMessage path when `compactMode === true`, so the new `isPending` gate on `SubagentExecutionRenderer` only protected the expanded path — committed terminal subagents in compact mode never reached `SubagentScrollbackSummary` and the LiveAgentPanel → committed- summary handoff broke for users who turned compact mode on. Force-expand the group when `!isPending` AND any tool call has a terminal `task_execution` resultDisplay. Stay compact while the parent turn is still live (`isPending`) — the panel below the composer owns that surface and an inline summary would duplicate it. Coverage: 4 new ToolGroupMessage cases (compact + completed-committed expands; compact + running-live stays compact; compact + completed-live stays compact; compact + failed-committed expands). 2. **Snapshot-coupled comment in `packages/core`** (Copilot, background-tasks.ts:292). The comment named CLI/UI consumers (`useBackgroundTaskView`, `BackgroundTasksDialog`) and asserted React batching guarantees from a core file. Reword to "snapshot-style consumers that re-pull `getAll()` from inside the callback" and drop the framework-specific batching claim. 3. **Two-phase emit needed an explicit signal** (Copilot, background-tasks.ts:283). Emitting `statusChange` twice without distinguishing the phases forced consumers to either do duplicate work or risk persisting a stale `entry` from the second callback. Add an optional second arg `context?: { removed?: boolean }` to `BackgroundStatusChangeCallback`; the post-delete emit passes `{ removed: true }` so consumers can disambiguate without re-querying the registry. Backwards compatible — existing callbacks ignore the new arg. Tests updated to assert both `mock.calls[0][1] === undefined` and `mock.calls[1][1] === { removed: true }`. 4. **`isPending` doc clarified** (Copilot, ToolMessage.tsx:507). Made the default semantics explicit: omitted/undefined is treated as committed (not pending); live-area renderers MUST pass `true` explicitly to suppress the scrollback summary. 5. (4 of the threads were duplicate Copilot fires of #2 + #3.) Coverage: 219 test files / 3369 passing across cli/ui + core/agents. * docs(cli): update ToolGroupMessageProps.isPending JSDoc The previous prop comment claimed `isPending` was "not consumed by the group body" — true at the time, but the body now reads it for two real purposes (compact-mode gating + forwarding to ToolMessage). Update the doc so future callers / tests don't treat it as legacy. Addresses Copilot finding on PRRT_kwDOPB-92c6AYE0V. * fix(cli): hide live-phase subagent tool entries — LiveAgentPanel owns the row User report: with compact mode OFF, a running subagent shows up twice — once as the parent tool group's `task` row (status icon + name + description), once as the LiveAgentPanel row beneath the composer. Same agent, two surfaces, redundant. Filter `task_execution` tool entries out of the expanded `ToolGroupMessage` while `isPending=true` so the panel is the single source of truth for in-flight subagents. The entry returns once the parent turn commits (`isPending=false`), letting `SubagentScrollbackSummary` land inside the parent's tool group as a persistent audit trail. Exception: subagents with a pending approval still render, because the focus-routed banner / queued marker is the only inline surface that lets users answer the prompt without opening the dialog. If a group is purely panel-owned (e.g. a single Task call with no sibling tools), the entire `ToolGroupMessage` returns `null` so an empty bordered container doesn't float above the panel. Coverage: +4 ToolGroupMessage cases — running entry hidden in live phase / mixed group keeps siblings / pending-approval entry still renders / committed entry comes back for the audit trail. * refactor(cli): tighten subagent-tool helper naming + ANSI-safe scrollback summary Self-audit + independent review found 5 cleanup items on the live-phase hide path; all addressed in one commit since none are behavioral changes: 1. **Move `allEntriesPanelOwned` short-circuit BEFORE `showCompact`** so a pure-subagent group in compact mode is also hidden during the live phase (previously CompactToolGroupDisplay rendered a single summary line above the panel — a mild duplicate on top of what the non-compact path already fixed). 2. **Rename `isLiveSubagentTool` → `isSubagentToolEntry`.** The helper identifies a tool's resultDisplay shape; it doesn't check live-state. The previous name conflated "predicate" with "use case" and read as if it returned true only during the live phase. 3. **DRY up `hasCommittedTerminalSubagent`** to use `isSubagentToolEntry` instead of inlining its own type-narrowing. 4. **ANSI-escape `subagentName` / `taskDescription` / `terminateReason`** in `SubagentScrollbackSummary`. Same threat model as the panel rows and HistoryItemDisplay — these strings come from subagent config (user-authored) and LLM output and could carry terminal control sequences. The stats fields (tool count / duration / tokens) flow through trusted formatters and don't need escaping. 5. **Doc comments updated** to reflect the four real responsibilities of `isPending` on `ToolGroupMessageProps` (hide pure groups, force-expand committed compact, per-tool filter, forward to ToolMessage), to clarify that the keyboard-focused subagent id can point at a hidden tool harmlessly (the iterator returns `null` before the focus prop is computed), and to drop the redundant "EXCEPT" clause on the per-tool filter in favor of a single sentence. Coverage unchanged: 251 passing tests across messages / background-view / core/agents; broader 3374-test sweep clean; TS clean on both cli and core packages. * fix(cli,core): address 3 critical review findings + ANSI/doc cleanups Three real bugs flagged by gpt-5.5 via /qreview, plus 4 doc / sanitization nits from Copilot. All 7 threads close together since they share the same surfaces. ## Critical fixes 1. **Foreground subagents disappeared mid-parent-turn** (PRRT_kwDOPB-92c6AYvL9). Post-QwenLM#3921 swap-order, `unregisterForeground` drops the entry from the panel snapshot the moment the subagent finishes. The previous round's `!isPending` gate on `SubagentScrollbackSummary` then suppressed the inline summary too, leaving the user with nothing on screen for the run until the parent committed. - Drop the `!isPending` gate — `unregisterForeground` already removes the row from the panel, so the inline summary can fire in BOTH live and committed phases without duplicating it. - Tighten the `ToolGroupMessage` live-phase hide so it only filters `running` / `paused` / `background` task entries (`isPanelOwnedSubagentTool`), not terminal ones. Terminal entries pass through immediately so the summary lands. - The "panel-owned" predicate is now distinct from the broader "subagent tool entry" predicate (`isSubagentToolEntry`) and the "terminal subagent" predicate (`isTerminalSubagentTool`); each usage site picks the one it actually means. 2. **Compact mode dropped the scrollback summary** (PRRT_kwDOPB-92c6AYvLw). Force-expanding the group made the container go through the expanded path, but `ToolMessage`'s own compact-mode gate (`!compactMode || forceShowResult ? renderer : 'none'`) still suppressed the result block, so `SubagentScrollbackSummary` never rendered for compact-mode users. Pass `forceShowResult={true}` for terminal subagent tool entries so the result block is always rendered. 3. **`mergeCompactToolGroups.isForceExpandGroup` didn't know about terminal subagents** (PRRT_kwDOPB-92c6AYvMC). The committed- history preprocessor merged adjacent tool_groups before render, so a terminal `task_execution` group could be absorbed into a compact batch (its `tool_use_summary` label dropped), and the render-time force-expand check never got a chance to override. Mirror the `hasCommittedTerminalSubagent` predicate inside `isForceExpandGroup` so preprocessing and rendering agree. ## Doc / sanitization nits - `BackgroundStatusChangeCallback` doc now lists every emitter (register / complete / fail / cancel / finalizeCancelled / finalizeCancellationIfPending / abandon / unregisterForeground / reset) and groups them by ordering camp (keeps-the-entry vs removes-the-entry — `reset` joins `unregisterForeground` in the delete-then-emit camp). - ANSI-escape `data.subagentName` in the focus-holder banner and the queued marker (`SubagentExecutionRenderer`) — same threat model as the panel rows and `SubagentScrollbackSummary`. ## Coverage delta - New ToolMessage case: live-phase terminal subagent now renders inline (replaces the prior "no scrollback summary" assertion that was the symptom of the AYvL9 bug). - New ToolGroupMessage cases: terminal subagent in live phase renders inline; `forceShowResult=true` propagates for terminal subagent tools (mock now exposes the prop). - New mergeCompactToolGroups parametrized cases: terminal subagent in any of completed / failed / cancelled stays its own batch. 280 tests pass across cli messages + utils + background-view + core/agents. TS clean. * fix(cli): drop `'paused'` arm from isPanelOwnedSubagentTool — not in AgentResultDisplay union CI Lint failed with TS2367: the previous round's `isPanelOwnedSubagentTool` checked for `status === 'paused'` but `AgentResultDisplay.status` (the tool-result-side type) only carries `'running' | 'completed' | 'failed' | 'cancelled' | 'background'`. The `'paused'` status lives on the registry-side `BackgroundTaskStatus` union and is only ever surfaced through `LiveAgentPanel` directly, never through a `task_execution` payload. Drop the dead arm and add a comment so a future "let's also check paused here" doesn't get re-introduced. * fix(cli): apply panel-ownership filter once before compact-mode decision Mixed live groups (running subagent + sibling tool) leaked the panel-owned subagent into `CompactToolGroupDisplay`'s count and `getActiveTool` selection, because `showCompact` returned BEFORE the inline `.map()` filter ran. Compact-mode users would see e.g. `task × 2 Delegate task to subagent` even though LiveAgentPanel already owned the subagent row below the composer. Derive `inlineToolCalls` once via `useMemo` immediately after the existing hook block and use it consistently for the compact summary, sizing math, and the render map. The early-return for "all-entries-panel-owned" collapses into `inlineToolCalls.length === 0` (gated on `isPending` so the legacy empty-input committed-phase snapshot is preserved). Remove the inner `.map()` filter — the upstream derivation already excluded the same entries. JSDoc updates: - `ToolGroupMessageProps.isPending` now describes the real flow (build inlineToolCalls / force-expand / forward to ToolMessage for parity). - `ToolMessageProps.isPending` is documented as forwarded-but-inert (`SubagentExecutionRenderer` doesn't gate on it; the live-phase filter and the unconditional terminal summary do the actual work). Regression test: live mixed group in compact mode → sibling wins active-tool, count collapses to 1, no `× 2` suffix, no subagent description in the header. Addresses Copilot review comments 3205262972 / 3205263020 (doc/code mismatch) and gpt-5.5 critical 3205288299 (compact-mode leak). * fix(cli): force-expand compact groups on terminal subagent in live phase too Resolved comment 3203286936 codified the design intent that `SubagentScrollbackSummary` "fires in BOTH live and committed phases" to bridge `unregisterForeground`'s post-delete panel-snapshot drop and the parent turn committing. Non-compact mode honored that contract (terminal subagents render the summary inline whenever they appear in `inlineToolCalls`), but compact mode still gated `hasCommittedTerminalSubagent` on `!isPending`, so a foreground subagent finishing mid-turn under compact mode produced NOTHING inline until the parent committed — exactly the gap the bridge was meant to close. Drop the `!isPending` arm and rename `hasCommittedTerminalSubagent` → `hasTerminalSubagent`. The force-expand now applies to terminal subagents in either phase; compact-mode users see the same outcome line non-compact users already get. Mirrors `SubagentExecutionRenderer`'s ungated terminal-summary path and `mergeCompactToolGroups.isForceExpandGroup`'s no-isPending-gate preprocessing rule. Tests: - Flip "compact mode: live group with completed subagent stays compact" → "force-expands so the summary bridges the panel-snapshot drop". Update rationale to reflect post-QwenLM#3921 reality (panel evicts terminal foreground rows immediately). - Add "compact mode: live mixed group with terminal subagent + sibling force-expands and renders both" — covers the bridge in mixed groups. - Update two stale `hasCommittedTerminalSubagent` cross-references in `mergeCompactToolGroups.{ts,test.ts}` comments.
…entPanel (QwenLM#3909) * feat(cli): replace inline AgentExecutionDisplay with always-on LiveAgentPanel Surface running subagents in a borderless, always-on roster anchored beneath the input footer (mirrors Claude Code's CoordinatorTaskPanel) and retire the verbose inline `AgentExecutionDisplay` frame whose per-tool-call mutations caused scrollback flicker. Detail / cancel / resume keep flowing through the existing BackgroundTasksDialog. LiveAgentPanel: - Two-column row: status icon + (optional) type + description + activity on the left (truncate-end), elapsed + tokens on the right in a flex-shrink:0 column so the cost/time fields are never hidden by long descriptions. - Re-pulls each agent from BackgroundTaskRegistry on every wall-clock tick so `recentActivities` stays fresh — the snapshot from `useBackgroundTaskView` only refreshes on `statusChange` to keep the footer pill / AppContainer quiet under heavy tool traffic. - Reaches for Config via raw ConfigContext (not useConfig) so the panel degrades to snapshot-only when no provider is mounted (test isolation). - Hides when any dialog is visible (auth / permission / bg tasks) and self-hides when no agent entries are live. - Drops `subagentType` from the row when it is the default `general-purpose` builtin to keep the line uncluttered; specialized types still bold-anchor the row. - Keeps terminal entries on screen for 8s so the user gets feedback when an agent finishes, then they fall off (BackgroundTasksDialog retains them long-term). Inline frame retirement: - ToolMessage's SubagentExecutionRenderer collapses to the focus- routed approval surfaces only (focus-holder banner + queued marker). All other agent state is owned by the panel + dialog. - AgentExecutionDisplay.tsx + test removed (-918 lines); the subagents/index export is dropped with a pointer to the new surfaces. Net diff: +97 / -1069. * fix(cli): move elapsed + tokens to the front of LiveAgentPanel rows The two-column layout used `flex-grow:1` on the left description column, which puffed it out to fill the row even when content was short — leaving a visible gap between the description tail and the right-pinned elapsed/tokens whenever the terminal was wider than the content. Worse, the gap made it look like display space was being wasted while the description still got truncated. Move elapsed + tokens to the front of the row (right after the status icon) so: - Time and cost are pinned at a stable left position and are NEVER at risk of being truncated, regardless of description / activity length. - The row reads as one tight left-to-right line — no flex-grow, no internal gap. On wide terminals the unused width sits at the row tail (invisible), where it belongs. - Description + activity become the truncatable tail; `truncate-end` cuts only when the line genuinely overflows the panel width. Also wrap the icon-plus-spaces span in a template literal so the two spaces of breathing room after the glyph survive a prettier pass. Verified at 60 / 100 / 200 cols: at 200 cols the row renders flush with no trailing ellipsis and no internal gap; at 60 cols the time + tokens stay at the front and the description tail truncates with `…`. * fix(cli): switch LiveAgentPanel row to layout C (right-pinned, no flex-grow) Iterating on the row layout based on visual review: - Layout A (time first, single Text) put numbers ahead of identity, which broke the natural left-to-right reading order. - Layout B (right-pinned with flex-grow:1 on left) puffed the left column out to fill the row, leaving a visible gap between the description tail and the right-pinned elapsed when the terminal was wider than the content. Layout C keeps the right column flex-shrink:0 so elapsed + tokens are never clipped, but DROPS flex-grow on the left so the two columns sit side-by-side: empty slack falls off the row tail (invisible) instead of opening a gap inside the row. Identity (type) and intent (description / activity) read first, cost reads last — matching the natural visual hierarchy. When the row overflows the panel width the left column truncates with `…` mid-row, while elapsed + tokens stay intact. Verified at 60 / 100 / 200 cols — at 200 cols there is no internal gap and no trailing ellipsis; at 60 cols time + tokens stay visible on the right and the description / activity tail truncates with `…`. * fix(cli): port Claude Code's bullet + arrow visual to LiveAgentPanel rows Adopt the leaked CoordinatorTaskPanel visual conventions: - `○` replaces `⊷` for live (running) slots — matches Claude Code's use of `figures.circle` for the active-agent bullet, gives a uniform list look across the running roster. Terminal states keep distinct check / cross marks (✔ / ✖) so they're easy to scan at a glance. - `▶` separates the description from elapsed / tokens, mirroring Claude's `PLAY_ICON` suffix marker. - Activity is wrapped in `( ... )` so it reads as an annotation on the description rather than a sibling field, and the type prefix switches from ` · ` to `: ` (e.g. `editor: tighten import order`) to match Claude's `name: description` pattern. The two-column flex layout from layout C is preserved — left column flex-shrink:1 with truncate-end, right column (` ▶ Ns · Nk tokens`) flex-shrink:0 so elapsed + tokens are never clipped, regardless of how long the description / activity grows. This is the one intentional divergence from Claude's literal pattern, which puts elapsed at the row tail without pinning and lets it disappear off narrow terminals. Verified at 60 / 100 / 200 cols: at 200 cols the row is flush with no internal gap; at 60 cols the description / activity tail truncates with `…` while elapsed + tokens stay visible on the right. * fix(cli): widen LiveAgentPanel, drop [in turn] marker, point overflow at dialog Three usability fixes from review: 1. Use `terminalWidth` instead of `mainAreaWidth`. The latter is capped at 100 cols (intended for markdown / code where soft-wrap matters), which on a 200-col terminal left half the screen empty to the right of an already-truncating row. Live progress lines have nothing to soft-wrap, so the panel wants the full width. 2. Drop the `[in turn]` foreground marker. The flavor distinction matters in BackgroundTasksDialog (cancel semantics differ for foreground vs background entries) but in the glance panel the marker reads as cryptic noise — users asked what it meant. Keep the dialog as the surface that surfaces it. 3. Annotate the overflow callout with `(↓ to view all)`. The panel is intentionally read-only (it has no keyboard focus so it can't steal input from the composer), so when the roster outgrows the row budget we point users at the existing dialog — same keystroke the footer pill uses, kept in sync so users only learn one gesture. * fix(cli): make Down on focused BackgroundTasksPill open the dialog The focus chain Composer → AgentTabBar → BackgroundTasksPill is walked with the Down arrow, but Down dead-ended at the pill — the pill only opened the dialog on Enter. Users who followed the LiveAgentPanel's "(↓ to view all)" overflow callout reached the highlighted pill and got stuck there, defeating the hint. Route Down on the focused pill into openDialog so the chain completes naturally: Composer ↓ → AgentTabBar ↓ → Pill ↓ → Dialog. Enter still works, so existing muscle memory keeps functioning. * fix(cli): address Copilot review on LiveAgentPanel — interval gate, ghost rows, dead doc Three findings from Copilot's PR review: 1. **1s interval kept ticking forever after expiry.** The gate was `entries.some(isAgentEntry)`, but `BackgroundTaskRegistry.getAll()` retains terminal entries indefinitely — once the last visible row passed its 8s window the panel returned null but the interval kept firing setNow each second, churning re-renders for nothing on screen. The gate now considers visibility (running / paused OR terminal-within-window) and the interval clears itself once the condition flips false. New entries restart the interval via the `entries` dep. 2. **Ghost rows when registry forgets an entry.** The live re-pull fell back to the snapshot when `registry.get()` returned undefined. The canonical case is a foreground subagent that unregisters silently after its statusChange fires (`unregisterForeground` deletes without emitting a follow-up transition) — the snapshot still says `running`, so the row would never clear. Trust the registry: when it says the entry is gone, drop the row. The snapshot-only fallback is preserved for the no-Config case (test fixtures). 3. **Dead doc reference.** The trailing comment in `subagents/index.ts` pointed at `docs/comparison/subagent-display-deep-dive.md`, which doesn't exist in this repo (it lives in the codeagents knowledge base, not qwen-code). Dropped the dead pointer; the in-tree pointers to `LiveAgentPanel` and `BackgroundTasksDialog` already tell readers where to look. Coverage delta: +2 cases on `LiveAgentPanel.test.tsx` — `drops snapshot rows the live registry no longer knows about` (issue QwenLM#2) and `still shows the snapshot when no Config is mounted` (locks in the test-fixture fallback that issue QwenLM#2's stricter rule would otherwise have broken). * fix(cli): address Copilot second-pass review — dialogOpen tick gate, isFocused doc Two further findings from Copilot: 1. **Interval kept ticking while bg-tasks dialog was open.** The first-pass fix already torn the tick down once all rows expired from the visibility window, but `dialogOpen` was a separate reason the panel returned null and was missed by the gate. Add `dialogOpen` to the useEffect deps and short-circuit when true, so the dialog's tenure is interval-free. 2. **Stale `isFocused` doc comment in `ToolMessageProps`.** The comment claimed the prop controlled the now-retired `Ctrl+E / Ctrl+F` display shortcuts (those died with the inline `AgentExecutionDisplay` frame). Rewrite the comment to describe the only remaining behavior — the focus-routed approval surface (focus-holder banner vs. queued-sibling marker). Coverage delta: +1 case on `LiveAgentPanel.test.tsx` — `tears the 1s tick down when the bg-tasks dialog opens` advances 60s of fake time with `dialogOpen=true` and asserts no panel state drift. * fix(cli): reconcile registry-missing snapshots as just-finished + address review nits Hot-fix: the previous round's "drop the row when registry.get() returns undefined" was too aggressive. `unregisterForeground` calls `emitStatusChange(entry)` BEFORE it deletes the entry, so the snapshot useBackgroundTaskView captures still says "running" while the very next render's registry.get sees nothing. Dropping the row outright made foreground subagents disappear from the panel the instant they finished — users saw "SubAgents 不显示了" on tasks that ran-and-immediately-completed. Reconciliation now has three branches: 1. live found → use live (newest recentActivities). 2. snap says still-live but registry forgot → synthesize a terminal version with endTime pinned to the FIRST observation so the 8s visibility window gives the user a "the agent finished" beat then evicts cleanly. The first-seen-missing timestamp is held in a useRef map (without it, each tick resets endTime to `now` and the row never expires). 3. snap is already terminal but registry forgot → drop (no useful state to keep showing). Also addresses three smaller review notes (deepseek-v4-pro via /review): - DEFAULT_SUBAGENT_TYPE is now imported from @qwen-code/qwen-code-core (a new DEFAULT_BUILTIN_SUBAGENT_TYPE export referenced by both BuiltinAgentRegistry's seed entry and the panel's default-type elision). A backend rename now propagates instead of silently re-introducing the redundant `general-purpose:` prefix on every row. - The useMemo body now reads `now` (as `reconcileAt`) so the dependency is semantically honest — a future "remove dead dep" cleanup can no longer silently freeze the panel on the first tool-call after a snapshot refresh. Coverage delta: +5 cases on LiveAgentPanel.test.tsx — token rendering on completed entries, status-icon routing for paused / failed / cancelled (parametrized), case-insensitive prefix stripping in descriptionWithoutPrefix, plus the rewritten ghost-row case (synthesized terminal lingers 8s then evicts) and a sibling case asserting already-terminal snapshots with empty registry still drop. 17 LiveAgentPanel tests pass. * refactor(cli): split LiveAgentPanelBody + drop dead isPending/isWaitingForOtherApproval props Two structural reviews from deepseek-v4-pro via /review: 1. **Hook-order footgun.** The `if (dialogOpen) return null` guard sat between the hook block and a substantial block of pure-render code; an extension that added `useMemo`/`useRef`/`useCallback` below the guard would crash with "Rendered fewer hooks than expected" the next time `dialogOpen` toggled. Extract the pure- render logic into `LiveAgentPanelBody` so the guard becomes the parent's last statement, and any future "add a hook to the body" refactor naturally lands in the inner component (hook-free today, hook-free for as long as it stays a presentational FC). 2. **Dead pass-through removed.** `isPending` and `isWaitingForOtherApproval` were dead in the subagent renderer (LiveAgentPanel + BackgroundTasksDialog own that surface) but ToolGroupMessage still computed `isWaitingForOtherApproval` and forwarded both into ToolMessage. Drop them from `ToolMessageProps`, drop the computation + forwarding in ToolGroupMessage, drop the test factory references. ToolGroupMessage keeps `isPending` on its own props for upstream caller compatibility (HistoryItemDisplay et al. forward it) but stops destructuring it; the doc comment now points at the surfaces that own that gating today. Coverage: existing 115 cases across LiveAgentPanel / BackgroundTasksDialog / BackgroundTasksPill / ToolMessage / ToolGroupMessage all green; the panel split is purely structural and the dead-prop removal was verified TS-clean before commit. * fix(cli): map internal tool names to user-facing display names in LiveAgentPanel rows `recentActivities[].name` carries the internal tool name from AgentToolCallEvent (e.g. `run_shell_command`, `glob`). Rendering it verbatim surfaced raw identifiers in the panel (`run_shell_command rg TODO`) while BackgroundTasksDialog already mapped through ToolDisplayNames to show user-facing names (`Shell rg TODO`) — the two surfaces' vocabularies drifted on the same data. Mirror the dialog's `TOOL_DISPLAY_BY_NAME` lookup so the panel and dialog speak the same vocabulary. Added a test asserting `run_shell_command` renders as `Shell` and the raw name is not surfaced. Coverage delta: +1 case on LiveAgentPanel.test.tsx (18 total). * fix(cli): keep already-terminal snapshots visible until TTL + close 4 test gaps 1. **Terminal snap + missing registry now follows the visibility window.** Cancelled / failed foreground subagents go through `cancel`/`fail` (which stamp `endTime` and emit statusChange) followed by `unregisterForeground` (which deletes silently). The snap captures the real `endTime`, so the previous "drop on missing registry" branch made cancelled / failed foreground tasks disappear instantly — contradicting the panel's "brief terminal visibility" contract that the synthesized-completion path also relies on. Keep the snap as-is when `endTime` is set; the visibleAgents filter evicts it after `TERMINAL_VISIBLE_MS` like any other terminal entry. Defensive fallback drops the pathological "terminal status with no endTime" shape. (Copilot finding on PRRT_kwDOPB-92c6AS4z9.) 2. **Close 4 test gaps flagged by tanzhenxin's review:** - `elides the default 'general-purpose' subagent type from the row` locks in DEFAULT_BUILTIN_SUBAGENT_TYPE comparison. - `truncates the description tail when the panel width is too narrow` exercises the `width` prop (existing cases ignored it) and anchors on the right-pinned `▶ 3s` tail staying intact. - `clears the 1s tick interval when unmounted with live work in flight` spies on setInterval / clearInterval so a discarded fiber can't keep firing setNow. - `keeps terminal snapshots visible until the TTL even when the registry forgot them` covers the cancelled / failed foreground reconciliation path with a `✖` glyph + 9s eviction assertion. Coverage: 22 LiveAgentPanel tests pass (was 18). All TS clean. * refactor(cli): rename FOREGROUND_ROW_PREFIX from `[in turn]` to `[blocking]` User feedback: `[in turn]` reads as "queued / sequential" — the opposite of what it actually means (the row is blocking the user's current turn). Even maintainers had to chase the source comment to confirm the semantics, which is a strong signal that end users are not getting the warning the prefix is supposed to deliver. `[blocking]` reads more directly: "this row is what's holding up your input", which is what cancelling it actually unblocks. Update the in-row prefix and the cancel-confirmation hint in lockstep (`x again to confirm stop · ends the blocking turn`) so the two surfaces share vocabulary. LiveAgentPanel still suppresses the marker in its row (the panel has no cancel surface, so the warning has nothing actionable to pair with); update the historical comment + reinforce the test guard so neither the legacy `[in turn]` nor the new `[blocking]` bleeds into the glance roster. * fix(cli): address 4 review findings — height budget, neutral synthesis glyph, stale comments 1. **`availableTerminalHeight` regression** (claude-opus-4-7 via /qreview, DefaultAppLayout.tsx:127). LiveAgentPanel rendered OUTSIDE the `mainControlsRef` Box, so its 2-7 rows were not measured by `controlsHeight` and not subtracted from `availableTerminalHeight = terminalHeight - controlsHeight - staticExtraHeight - 2 - tabBarHeight` in AppContainer. Pending tool results in MainContent could render past the visible area and push the composer / panel off-screen — a regression vs PR QwenLM#3768 which suppressed the inline frame in the live phase. Move the panel INSIDE the `mainControlsRef` Box so `measureElement` picks up its rows automatically; no new infrastructure needed. 2. **Neutral glyph for synthesized terminal rows** (claude-opus-4-7, LiveAgentPanel.tsx:278). The synthesis branch hardcoded `status: 'completed'` regardless of the actual outcome. Foreground subagents that errored or were cancelled were rendered with the green ✔ for 8s — directly contradicting the inline tool result the user just saw (`Subagent execution failed.` / `Agent was cancelled by the user.`). Add a `synthesized` flag on the synthesized rows; `statusIcon` checks it first and returns a neutral `·` + secondary color, so the panel never lies about an outcome it cannot determine. Status stays `'completed'` purely so the visibility-window filter treats the row as terminal. 3. **PR description claim retired** (Copilot, ToolMessage.tsx:411). The body's Reviewer Notes section claimed `isPending` / `isWaitingForOtherApproval` were kept on `ToolMessageProps` as vestigial pass-through; the props were actually removed in commit 93036b1. Will update the PR description in a follow-up non-code edit (the comment thread itself is on now-outdated line 411). 4. **Stale comment in ToolGroupMessage** (Copilot, ToolGroupMessage.tsx:303). The comment above the focus-routing logic referenced the retired Ctrl+E / Ctrl+F display shortcuts. Rewrite it to describe the current behavior — focus routing for inline approval prompts only — and call out where the live progress / drill-down moved (LiveAgentPanel + BackgroundTasksDialog). Coverage delta: existing `reconciles snapshots…` test rewritten to assert `·` instead of `✔` (and explicitly assert `not.toContain('✔')`); new sibling `keeps the success glyph for entries the registry still tracks (non-synthesized)` locks the non-synthesis path against a future regression that always returns the neutral glyph. 68 background-view tests + 128 messages/layouts tests pass. * fix(cli): ANSI-escape user-controlled strings + restore one-line scrollback summary Two findings from deepseek-v4-pro via /review: 1. **ANSI sanitization on the panel** (LiveAgentPanel.tsx:368). The row was rendering `entry.subagentType`, `descriptionWithoutPrefix` output, and `recentActivities[].description` straight through Ink's `<Text>` — both subagent config (user-authored) and tool-call description (LLM-generated) can contain terminal control sequences that bleed through and corrupt the panel chrome. Apply `escapeAnsiCtrlCodes` to all three (HistoryItemDisplay does the same on its user-facing content for the same reason). 2. **One-line scrollback summary for terminal subagents** (ToolMessage.tsx:295). The previous round retired the verbose inline frame entirely and SubagentExecutionRenderer returned null for all non-approval states. The result: completed subagent results disappeared from scrollback the moment LiveAgentPanel's 8s window expired. Reopening a session or dismissing the dialog left no record. Add a single-line `SubagentScrollbackSummary` for completed / failed / cancelled states — `<icon> <name>: <description> · N tools · Xs · Yk tokens` (plus terminateReason on non-success). One row per agent, no flicker risk; the verbose frame stays retired. Coverage: +3 cases — `escapes ANSI control codes in user-controlled strings` (LiveAgentPanel), `completed subagent → renders a one-line scrollback summary` and `failed subagent → renders summary with terminate reason` (ToolMessage). 24 + 34 panel/ToolMessage tests pass; broader 2894-test cli/ui sweep clean. Note: this restores some inline rendering that the original feature choice ("inline AgentExecutionDisplay 完全移除") removed entirely. The compromise — one line per terminal agent vs the old 15-row frame — preserves history while keeping the flicker fix that motivated the retirement. * fix(cli): paused agents count as active in tally + sync reconciliation comment Two findings from Copilot: 1. **Header tally now matches what the user sees.** The numerator was `runningCount` (status === 'running'), but the panel also renders paused entries as active rows (warning color, ⏸ glyph). With only paused agents present the header read "(0/1)" while the row was clearly visible — a confusing mismatch. Rename to `activeCount` and include paused in the count. New test `counts paused agents as active in the header tally` locks the tally + glyph in. 2. **Reconciliation block comment now describes the four real paths**, not the older three: 1. live found → use live 2. snap still-live + registry forgot → synthesize neutral terminal 3. snap terminal + endTime present → keep snap, let TTL filter evict it (the cancelled / failed foreground path the previous round added) 4. snap terminal + no endTime → drop (upstream invariant violation) Comment is now in lockstep with the implementation; the prior "drop terminal-with-empty-registry outright" wording was stale after the TTL-keep change. Coverage: 25 LiveAgentPanel tests pass (was 24).
…Change emit (QwenLM#3919) * fix(cli,core): isPending gate on subagent scrollback summary + post-delete statusChange emit Two follow-ups from PR QwenLM#3909 review. 1. **Re-introduce `isPending` gate on `SubagentExecutionRenderer`'s scrollback summary** (Copilot finding on PRRT_kwDOPB-92c6AUQHn). The verbose inline frame retirement collapsed `SubagentExecutionRenderer` to "render the summary whenever a subagent reaches a terminal status" — but with `isPending` removed in QwenLM#3909, that fired in BOTH live (pendingHistoryItems) AND committed (Static) phases. Live-phase rendering duplicated the row LiveAgentPanel already paints below the composer until the parent turn committed. Add `isPending` back to `ToolMessageProps` purely as a gate for this one render path: the summary fires only when `!isPending` (committed). `ToolGroupMessage` forwards the flag (it kept the prop on its own interface for upstream compat the whole time). Test gap closed by the new `live (isPending) terminal subagent → no scrollback summary (panel owns the row)` case. 2. **Emit `statusChange` AFTER delete in `unregisterForeground`** (Copilot finding on PRRT_kwDOPB-92c6AUQGc + the panel-only reconciliation it spawned). The shared snapshot in `useBackgroundTaskView` only refreshes on `statusChange`, and `unregisterForeground` previously fired exactly once — BEFORE delete — so the snapshot froze with the agent as "running" while `registry.get()` returned undefined. Result: `BackgroundTasksDialog` list mode showed a ghost "running" row with cancel hints whose `x` was a no-op, contradicting what the panel already showed (synthesized neutral terminal). Fire `statusChange` a second time AFTER `agents.delete()` so snapshot consumers see the registry-less state and stop surfacing the agent. The first emit still mirrors complete/fail/cancel/finalize ordering (callbacks that re-read `registry.get` see the entry); the second emit is the new contract for snapshot-based views. React batches the two resulting setState calls into one re-render so consumers re-render exactly once. Updated the existing "emits status change before removing the entry" test to capture both emits and explicitly assert that the second observes the registry-less state. Added a sibling test covering the post-delete `getAll()` count. Coverage: 190 passing tests across core + cli (background-view + ToolMessage + ToolGroupMessage + useBackgroundTaskView). * fix(cli,core): compact-mode terminal subagent expansion + statusChange context flag Five review findings on PR QwenLM#3919: 1. **Compact mode bypassed the scrollback summary** (gpt-5.5 via /qreview, ToolGroupMessage:324). `ToolGroupMessage` returns `CompactToolGroupDisplay` before the ToolMessage path when `compactMode === true`, so the new `isPending` gate on `SubagentExecutionRenderer` only protected the expanded path — committed terminal subagents in compact mode never reached `SubagentScrollbackSummary` and the LiveAgentPanel → committed- summary handoff broke for users who turned compact mode on. Force-expand the group when `!isPending` AND any tool call has a terminal `task_execution` resultDisplay. Stay compact while the parent turn is still live (`isPending`) — the panel below the composer owns that surface and an inline summary would duplicate it. Coverage: 4 new ToolGroupMessage cases (compact + completed-committed expands; compact + running-live stays compact; compact + completed-live stays compact; compact + failed-committed expands). 2. **Snapshot-coupled comment in `packages/core`** (Copilot, background-tasks.ts:292). The comment named CLI/UI consumers (`useBackgroundTaskView`, `BackgroundTasksDialog`) and asserted React batching guarantees from a core file. Reword to "snapshot-style consumers that re-pull `getAll()` from inside the callback" and drop the framework-specific batching claim. 3. **Two-phase emit needed an explicit signal** (Copilot, background-tasks.ts:283). Emitting `statusChange` twice without distinguishing the phases forced consumers to either do duplicate work or risk persisting a stale `entry` from the second callback. Add an optional second arg `context?: { removed?: boolean }` to `BackgroundStatusChangeCallback`; the post-delete emit passes `{ removed: true }` so consumers can disambiguate without re-querying the registry. Backwards compatible — existing callbacks ignore the new arg. Tests updated to assert both `mock.calls[0][1] === undefined` and `mock.calls[1][1] === { removed: true }`. 4. **`isPending` doc clarified** (Copilot, ToolMessage.tsx:507). Made the default semantics explicit: omitted/undefined is treated as committed (not pending); live-area renderers MUST pass `true` explicitly to suppress the scrollback summary. 5. (4 of the threads were duplicate Copilot fires of QwenLM#2 + QwenLM#3.) Coverage: 219 test files / 3369 passing across cli/ui + core/agents. * docs(cli): update ToolGroupMessageProps.isPending JSDoc The previous prop comment claimed `isPending` was "not consumed by the group body" — true at the time, but the body now reads it for two real purposes (compact-mode gating + forwarding to ToolMessage). Update the doc so future callers / tests don't treat it as legacy. Addresses Copilot finding on PRRT_kwDOPB-92c6AYE0V. * fix(cli): hide live-phase subagent tool entries — LiveAgentPanel owns the row User report: with compact mode OFF, a running subagent shows up twice — once as the parent tool group's `task` row (status icon + name + description), once as the LiveAgentPanel row beneath the composer. Same agent, two surfaces, redundant. Filter `task_execution` tool entries out of the expanded `ToolGroupMessage` while `isPending=true` so the panel is the single source of truth for in-flight subagents. The entry returns once the parent turn commits (`isPending=false`), letting `SubagentScrollbackSummary` land inside the parent's tool group as a persistent audit trail. Exception: subagents with a pending approval still render, because the focus-routed banner / queued marker is the only inline surface that lets users answer the prompt without opening the dialog. If a group is purely panel-owned (e.g. a single Task call with no sibling tools), the entire `ToolGroupMessage` returns `null` so an empty bordered container doesn't float above the panel. Coverage: +4 ToolGroupMessage cases — running entry hidden in live phase / mixed group keeps siblings / pending-approval entry still renders / committed entry comes back for the audit trail. * refactor(cli): tighten subagent-tool helper naming + ANSI-safe scrollback summary Self-audit + independent review found 5 cleanup items on the live-phase hide path; all addressed in one commit since none are behavioral changes: 1. **Move `allEntriesPanelOwned` short-circuit BEFORE `showCompact`** so a pure-subagent group in compact mode is also hidden during the live phase (previously CompactToolGroupDisplay rendered a single summary line above the panel — a mild duplicate on top of what the non-compact path already fixed). 2. **Rename `isLiveSubagentTool` → `isSubagentToolEntry`.** The helper identifies a tool's resultDisplay shape; it doesn't check live-state. The previous name conflated "predicate" with "use case" and read as if it returned true only during the live phase. 3. **DRY up `hasCommittedTerminalSubagent`** to use `isSubagentToolEntry` instead of inlining its own type-narrowing. 4. **ANSI-escape `subagentName` / `taskDescription` / `terminateReason`** in `SubagentScrollbackSummary`. Same threat model as the panel rows and HistoryItemDisplay — these strings come from subagent config (user-authored) and LLM output and could carry terminal control sequences. The stats fields (tool count / duration / tokens) flow through trusted formatters and don't need escaping. 5. **Doc comments updated** to reflect the four real responsibilities of `isPending` on `ToolGroupMessageProps` (hide pure groups, force-expand committed compact, per-tool filter, forward to ToolMessage), to clarify that the keyboard-focused subagent id can point at a hidden tool harmlessly (the iterator returns `null` before the focus prop is computed), and to drop the redundant "EXCEPT" clause on the per-tool filter in favor of a single sentence. Coverage unchanged: 251 passing tests across messages / background-view / core/agents; broader 3374-test sweep clean; TS clean on both cli and core packages. * fix(cli,core): address 3 critical review findings + ANSI/doc cleanups Three real bugs flagged by gpt-5.5 via /qreview, plus 4 doc / sanitization nits from Copilot. All 7 threads close together since they share the same surfaces. ## Critical fixes 1. **Foreground subagents disappeared mid-parent-turn** (PRRT_kwDOPB-92c6AYvL9). Post-QwenLM#3921 swap-order, `unregisterForeground` drops the entry from the panel snapshot the moment the subagent finishes. The previous round's `!isPending` gate on `SubagentScrollbackSummary` then suppressed the inline summary too, leaving the user with nothing on screen for the run until the parent committed. - Drop the `!isPending` gate — `unregisterForeground` already removes the row from the panel, so the inline summary can fire in BOTH live and committed phases without duplicating it. - Tighten the `ToolGroupMessage` live-phase hide so it only filters `running` / `paused` / `background` task entries (`isPanelOwnedSubagentTool`), not terminal ones. Terminal entries pass through immediately so the summary lands. - The "panel-owned" predicate is now distinct from the broader "subagent tool entry" predicate (`isSubagentToolEntry`) and the "terminal subagent" predicate (`isTerminalSubagentTool`); each usage site picks the one it actually means. 2. **Compact mode dropped the scrollback summary** (PRRT_kwDOPB-92c6AYvLw). Force-expanding the group made the container go through the expanded path, but `ToolMessage`'s own compact-mode gate (`!compactMode || forceShowResult ? renderer : 'none'`) still suppressed the result block, so `SubagentScrollbackSummary` never rendered for compact-mode users. Pass `forceShowResult={true}` for terminal subagent tool entries so the result block is always rendered. 3. **`mergeCompactToolGroups.isForceExpandGroup` didn't know about terminal subagents** (PRRT_kwDOPB-92c6AYvMC). The committed- history preprocessor merged adjacent tool_groups before render, so a terminal `task_execution` group could be absorbed into a compact batch (its `tool_use_summary` label dropped), and the render-time force-expand check never got a chance to override. Mirror the `hasCommittedTerminalSubagent` predicate inside `isForceExpandGroup` so preprocessing and rendering agree. ## Doc / sanitization nits - `BackgroundStatusChangeCallback` doc now lists every emitter (register / complete / fail / cancel / finalizeCancelled / finalizeCancellationIfPending / abandon / unregisterForeground / reset) and groups them by ordering camp (keeps-the-entry vs removes-the-entry — `reset` joins `unregisterForeground` in the delete-then-emit camp). - ANSI-escape `data.subagentName` in the focus-holder banner and the queued marker (`SubagentExecutionRenderer`) — same threat model as the panel rows and `SubagentScrollbackSummary`. ## Coverage delta - New ToolMessage case: live-phase terminal subagent now renders inline (replaces the prior "no scrollback summary" assertion that was the symptom of the AYvL9 bug). - New ToolGroupMessage cases: terminal subagent in live phase renders inline; `forceShowResult=true` propagates for terminal subagent tools (mock now exposes the prop). - New mergeCompactToolGroups parametrized cases: terminal subagent in any of completed / failed / cancelled stays its own batch. 280 tests pass across cli messages + utils + background-view + core/agents. TS clean. * fix(cli): drop `'paused'` arm from isPanelOwnedSubagentTool — not in AgentResultDisplay union CI Lint failed with TS2367: the previous round's `isPanelOwnedSubagentTool` checked for `status === 'paused'` but `AgentResultDisplay.status` (the tool-result-side type) only carries `'running' | 'completed' | 'failed' | 'cancelled' | 'background'`. The `'paused'` status lives on the registry-side `BackgroundTaskStatus` union and is only ever surfaced through `LiveAgentPanel` directly, never through a `task_execution` payload. Drop the dead arm and add a comment so a future "let's also check paused here" doesn't get re-introduced. * fix(cli): apply panel-ownership filter once before compact-mode decision Mixed live groups (running subagent + sibling tool) leaked the panel-owned subagent into `CompactToolGroupDisplay`'s count and `getActiveTool` selection, because `showCompact` returned BEFORE the inline `.map()` filter ran. Compact-mode users would see e.g. `task × 2 Delegate task to subagent` even though LiveAgentPanel already owned the subagent row below the composer. Derive `inlineToolCalls` once via `useMemo` immediately after the existing hook block and use it consistently for the compact summary, sizing math, and the render map. The early-return for "all-entries-panel-owned" collapses into `inlineToolCalls.length === 0` (gated on `isPending` so the legacy empty-input committed-phase snapshot is preserved). Remove the inner `.map()` filter — the upstream derivation already excluded the same entries. JSDoc updates: - `ToolGroupMessageProps.isPending` now describes the real flow (build inlineToolCalls / force-expand / forward to ToolMessage for parity). - `ToolMessageProps.isPending` is documented as forwarded-but-inert (`SubagentExecutionRenderer` doesn't gate on it; the live-phase filter and the unconditional terminal summary do the actual work). Regression test: live mixed group in compact mode → sibling wins active-tool, count collapses to 1, no `× 2` suffix, no subagent description in the header. Addresses Copilot review comments 3205262972 / 3205263020 (doc/code mismatch) and gpt-5.5 critical 3205288299 (compact-mode leak). * fix(cli): force-expand compact groups on terminal subagent in live phase too Resolved comment 3203286936 codified the design intent that `SubagentScrollbackSummary` "fires in BOTH live and committed phases" to bridge `unregisterForeground`'s post-delete panel-snapshot drop and the parent turn committing. Non-compact mode honored that contract (terminal subagents render the summary inline whenever they appear in `inlineToolCalls`), but compact mode still gated `hasCommittedTerminalSubagent` on `!isPending`, so a foreground subagent finishing mid-turn under compact mode produced NOTHING inline until the parent committed — exactly the gap the bridge was meant to close. Drop the `!isPending` arm and rename `hasCommittedTerminalSubagent` → `hasTerminalSubagent`. The force-expand now applies to terminal subagents in either phase; compact-mode users see the same outcome line non-compact users already get. Mirrors `SubagentExecutionRenderer`'s ungated terminal-summary path and `mergeCompactToolGroups.isForceExpandGroup`'s no-isPending-gate preprocessing rule. Tests: - Flip "compact mode: live group with completed subagent stays compact" → "force-expands so the summary bridges the panel-snapshot drop". Update rationale to reflect post-QwenLM#3921 reality (panel evicts terminal foreground rows immediately). - Add "compact mode: live mixed group with terminal subagent + sibling force-expands and renders both" — covers the bridge in mixed groups. - Update two stale `hasCommittedTerminalSubagent` cross-references in `mergeCompactToolGroups.{ts,test.ts}` comments.
Why
Today the inline
AgentExecutionDisplayis the default surface for subagent execution, but it has three problems:BackgroundTasksDialogto see what's happening.This PR ports Claude Code's
CoordinatorTaskPanelpattern: a borderless, always-on roster anchored beneath the input footer, one line per agent, with elapsed + tokens pinned to aflex-shrink:0right column so they're never clipped. Detail / cancel / resume keep flowing through the existingBackgroundTasksDialog.Visual conventions (
○bullet,▶separator,name: desc (activity)format) are ported verbatim from the leakedCoordinatorTaskPanelsource so the two products feel consistent for users coming from Claude Code.Before / After
Live phase — agent running
Before (nothing inline; user must press Down to see what's running):
After (always-on roster beneath the footer):
Committed phase — agent has finished, frame lands in scrollback
Before (verbose ~15 row frame per agent):
After (nothing inline — completion summary stays on the panel for 8s, then
BackgroundTasksDialogretains it long-term):Layout behavior across terminal widths
200 cols (no gap, no ellipsis — left column is intrinsic width, slack falls off the row tail):
100 cols (left column truncates with
…, right column intact):60 cols (truncates earlier; time + tokens still pinned right):
Roster overflow — 9 agents with
maxRows=5(the panel is a glance surface, not a full list):The hint points at
BackgroundTasksDialog(↓arrow) for full scroll / select / cancel / resume — same gesture the footer pill already uses, kept in sync so users only learn one keystroke.Design
CoordinatorTaskPanel:○bullet for live (running) slots;name: description (activity) ▶ elapsed · tokensrow format. Terminal-state slots keep distinct check / cross marks (✔/✖/⏸) so they're easy to scan without cross-referencing color.flex-shrink:1,truncate-end(bullet + optional bold type + description + activity). Right column =flex-shrink:0(▶ Ns · Nk tokens). Crucially the left column does NOT haveflex-grow:1, so the two columns sit side by side at intrinsic widths — empty slack falls off the row tail (invisible) instead of opening a visual gap between description and elapsed (which is whatflex-growwould have produced). This is the one intentional divergence from Claude's literal pattern, which puts elapsed at the row tail without pinning and lets it disappear off narrow terminals.terminalWidth, notmainAreaWidth— the latter is hard-capped at 100 cols (intended for markdown / code blocks where soft-wrap matters), which on a 200-col terminal would have left half the screen empty to the right of an already-truncating row. Live progress lines have nothing to soft-wrap.useBackgroundTaskView's snapshot only refreshes onstatusChange(intentionally —appendActivityis silent there to keep the footer pill /AppContainerquiet under heavy tool traffic). The panel re-reads each agent fromBackgroundTaskRegistryon every wall-clock tick sorecentActivitiesstays fresh without re-introducing that churn for everybody else. Mirrors the patternBackgroundTasksDialog's detail body already uses.subagentType === 'general-purpose'is dropped from the row to keep the line uncluttered (the default builtin carries no useful identity beyond the description); user-defined / specialized types still bold-anchor the row.(↓ to view all)hint on the overflow callout points users atBackgroundTasksDialog, which already supports selection / scroll / cancel / resume.dialogsVisible(auth / permission / bg tasks), in agent-tab view, or when the agent roster is empty.CoordinatorTaskPanel(marginTop={1}+ plain rows). Border weight stays withBackgroundTasksDialog, the actual modal overlay.Deliberate trade-off — committed-phase summary is leaner
The retired inline
AgentExecutionDisplaypainted ~15 rows per committed agent: task prompt, full tool list, execution summary, AND a per-tool usage table with success rate / pass / fail breakdowns. The replacement surfaces are leaner:LiveAgentPanel(this PR) — one-line per agent: status icon · type · description · activity · elapsed · tokens.BackgroundTasksDialogdetail view (existing) — title, status, elapsed, total tokens, total tool count, recent activity buffer (last 5), prompt preview. No success rate, no success / fail breakdown, no per-tool usage table.Net effect: the per-tool breakdown + success-rate breakdown are no longer surfaced anywhere in live UI. The data still flows through to session export and
/resumehistory, so nothing is lost on disk — but a user reading committed scrollback (or opening the dialog mid-session) cannot see the breakdown. This is a deliberate cost-of-the-tradeoff, justified by the "committed phase is heavy" framing in the Why section: 15 rows × N agents × M completed turns made scrollback unreadable. If a future PR wants to bring the breakdown back, the natural place is a new "Stats" section onBackgroundTasksDialog's agent detail body — not a return to the inline frame.Reviewer notes
useMemofor the live-registry re-pull MUST come before theif (dialogOpen) return nullearly-return; conditional skipping of a subsequent hook is a violation. The pre-commit hook caught this and there's an inlineNOTE:documenting the order constraint so future edits don't slip back.isPending/isWaitingForOtherApprovalremoved fromToolMessageProps. Both were dead pass-through after the inline frame retired (SubagentExecutionRenderernever read them,ToolGroupMessageonly computedisWaitingForOtherApprovalto forward it). The cleanup drops them from the type, removes the computation + forward inToolGroupMessage, and strips the test factory references.ToolGroupMessagekeepsisPendingon its OWN props so upstream callers (HistoryItemDisplay,MainContent,AgentChatContent, etc.) don't churn — the group body just stops destructuring it.AgentExecutionDisplay. Verified via repo-wide grep before deleting — onlyToolMessageimported it, and that import is now gone.[in turn](theBackgroundTasksDialogconvention), but in glance context the marker reads as cryptic. The dialog still surfaces the flavor where it actually matters (cancel semantics differ).○ name: desc (activity) ▶ elapsed) with ourflex-shrink:0right column to keep elapsed + tokens on screen at narrow terminal widths.Test plan
Reproduce locally:
Manual visual verification (any task that fires an agent tool):
editor,reviewer, etc.) render bold;general-purposedoes not…, elapsed + tokens stay visible; at wide widths the row uses the full terminal width without an internal gap^ N more above (↓ to view all)) and pressing Down opens BackgroundTasksDialog with the full rosterCoverage delta: 8
LiveAgentPanel.test.tsxcases (empty roster / shell-only / dialog gate / single-agent header & row / foreground flavor — no inline marker / tail windowing with^N more abovecallout pointing at the dialog / live registry re-pull / terminal visibility window expiry); 5ToolMessage.test.tsxcases rewritten to assert no inline frame in either phase while approval banner + queued marker still render.