feat(cli): route foreground subagents through pill+dialog while running#3768
Conversation
Foreground (synchronous) subagents currently render a live AgentExecutionDisplay inside the parent's pendingHistoryItems block. The frame mutates on every tool call and approval; once it grows past the terminal height (verbose mode, parallel subagents, long tool-call lists) the live-area repaint flickers visibly. This change extends BackgroundTaskRegistry with a flavor: 'foreground' | 'background' discriminator. Foreground entries register at the start of the synchronous tool-call and unregister in its finally path. The pill counts them; the dialog drills into their activity. The inline frame is suppressed during the live phase — only an active, focus-locked approval prompt renders, as a small banner labeled with the originating agent. Once the parent turn commits, the full AgentExecutionDisplay appears in scrollback via Ink's <Static>, exactly as before. Foreground entries skip the XML task-notification (the parent receives the result through the normal tool-result channel) and skip the headless holdback (the parent's await already pins the loop). The dialog gates per-agent cancellation behind a two-step confirm so a stray 'x' can't end the user's current turn.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
wenshao
left a comment
There was a problem hiding this comment.
Changes requested: I found one SDK lifecycle mismatch in the foreground subagent registration path.
— gpt-5.5 via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Suggestion: Detail mode left key doesn't reset pendingCancelEntryId
In BackgroundTasksDialog.tsx, pressing left in detail mode calls exitDetail() without clearing pendingCancelEntryId. If the user arms a cancel (x) in detail mode and presses left to go back, the armed state carries into list mode. If the same entry is still selected, the hint bar still shows "x again to confirm stop" and a subsequent x unexpectedly confirms the cancellation. The list-mode left handler properly checks and resets; the detail-mode path should do the same.
Suggested fix: add setPendingCancelEntryId(null); before exitDetail();.
— deepseek-v4-pro via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Two additional findings beyond the existing inline threads (the SDK lifecycle gap, the unregisterForeground ordering asymmetry, the missing grace-timer fallback, the detail-mode left reset, the queued-approval invisibility, and the dead isWaitingForOtherApproval branch are all already covered).
The directional design is good. The two items I'm posting both stem from foreground termination not yet being symmetric with background termination — one is a UX gap (dialog detail subtitle), one is a contract gap (parent model can't tell a cancel from a success).
— claude-opus-4.7 via Qwen Code /review
Code reviewWhat the PR doesForeground (synchronous) subagents currently render an
Strengths
Issues1. Merge conflict — needs rebase before mergePR is function rowLabel(entry: DialogEntry): string {
switch (entry.kind) {
case 'agent': ...
case 'shell': ...
case 'monitor': return `[monitor] ${entry.description}`;
default: { const _exhaustive: never = entry; ... }
}
}The PR collapses this to 2. Dead code in the approval banner —
|
…-display # Conflicts: # packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx # packages/core/src/agents/background-tasks.ts
- Gate `registerCallback` on background flavor so foreground entries don't leak orphaned `task_started` SDK events without a matching terminal notification. - Render a queued-approval marker for non-focus subagents instead of returning null, so a queued approval is visible in the main view. - Move `emitStatusChange` before `agents.delete` in `unregisterForeground` to match the ordering used by complete/fail/cancel/finalize. - Prefix the foreground tool result with a cancel marker when `terminateMode === CANCELLED`, so the parent model can distinguish a user-cancelled run from a successful completion. - Mirror the background path's stats wiring on the foreground path so `entry.stats` stays current and the dialog detail subtitle shows tool count + tokens for foreground runs. - Remove the unreachable `isWaitingForOtherApproval` branch (subsumed by the queued-approval marker above). - Reset the foreground confirm-step on detail-mode `left` and ignore `x` on terminal entries so an armed cancel can't carry into list mode and the hint footer/handler stay in sync. - Test factory uses a `baseProps` spread instead of `as` cast so a future required field on `ToolMessageProps` is a compile-time miss.
|
Thanks for the detailed pass. Status:
Detail-mode |
Code ReviewOverviewThe PR fixes a flicker problem where the inline Net change is well-scoped: ~900 lines of substantive changes plus ~100 lines of unrelated formatting churn. Strengths
IssuesMedium
Minor
Question / verify
Test CoverageStrong on the `BackgroundTaskRegistry` gating (10 new test cases for foreground flavor, all four lifecycle gates exercised). Good on the dialog confirm-step (5 cases including the detail-mode carry-over). Subagent-render gating in `ToolMessage.test.tsx` covers the four-cell matrix (pending × confirmation × focused). Gaps:
Risk Assessment
RecommendationApprove with minor follow-ups (split the formatting churn into its own PR; gate |
wenshao
left a comment
There was a problem hiding this comment.
No issues found.
— gpt-5.5 via Qwen Code /review
wenshao
left a comment
There was a problem hiding this comment.
Verification on a fresh checkout passes:
- typecheck (after
npm run buildto refresh core dist), lint, full vitest suite (4929 passed / 7 skipped across 312 files), focused suites (166 passed across the 4 changed test files),npm run bundle+dist/cli.js --version/--help. CI is green on 13/13 checks. - All 7 prior review items are landed or argued away with a sound rationale (SDK
registerCallbackgating, queued-approval marker,unregisterForegroundemit-before-delete, foreground stats wiring, CANCELLED prefix on the parent's tool-result, deadisWaitingForOtherApprovalbranch removed; grace-timer skip rationale is fine because thefinallyalways unregisters). - Architecture is sound: composed AbortController is parent-down only with
once: trueplus an explicitfinallyremove;flavor: 'foreground'correctly gatesemitNotification,hasUnfinalizedTasks,registerCallback, and the cancel grace timer; default-absent flavor preserves background semantics for older callers. - Two non-blocking notes for follow-up if convenient: (1) merge commit
20a0d64a6brought ~167 lines of pure prettier whitespace reformatting in 13 unrelated files into the PR range — actual PR commits only touch 10 files, so an interactive rebase to flatten the merge would tighten the diff; (2) in detail mode, Enter/Space closes the dialog without clearingpendingCancelEntryId, so the armed state persists across reopen on the same entry — the hint footer still showsx again to confirm stopso it's self-explanatory, but it's inconsistent with theleft/Esc paths.
…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.
…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).
…ng (#3768) * feat(cli): route foreground subagents through pill+dialog while running Foreground (synchronous) subagents currently render a live AgentExecutionDisplay inside the parent's pendingHistoryItems block. The frame mutates on every tool call and approval; once it grows past the terminal height (verbose mode, parallel subagents, long tool-call lists) the live-area repaint flickers visibly. This change extends BackgroundTaskRegistry with a flavor: 'foreground' | 'background' discriminator. Foreground entries register at the start of the synchronous tool-call and unregister in its finally path. The pill counts them; the dialog drills into their activity. The inline frame is suppressed during the live phase — only an active, focus-locked approval prompt renders, as a small banner labeled with the originating agent. Once the parent turn commits, the full AgentExecutionDisplay appears in scrollback via Ink's <Static>, exactly as before. Foreground entries skip the XML task-notification (the parent receives the result through the normal tool-result channel) and skip the headless holdback (the parent's await already pins the loop). The dialog gates per-agent cancellation behind a two-step confirm so a stray 'x' can't end the user's current turn. * fix(cli): address review findings on foreground subagent routing - Gate `registerCallback` on background flavor so foreground entries don't leak orphaned `task_started` SDK events without a matching terminal notification. - Render a queued-approval marker for non-focus subagents instead of returning null, so a queued approval is visible in the main view. - Move `emitStatusChange` before `agents.delete` in `unregisterForeground` to match the ordering used by complete/fail/cancel/finalize. - Prefix the foreground tool result with a cancel marker when `terminateMode === CANCELLED`, so the parent model can distinguish a user-cancelled run from a successful completion. - Mirror the background path's stats wiring on the foreground path so `entry.stats` stays current and the dialog detail subtitle shows tool count + tokens for foreground runs. - Remove the unreachable `isWaitingForOtherApproval` branch (subsumed by the queued-approval marker above). - Reset the foreground confirm-step on detail-mode `left` and ignore `x` on terminal entries so an armed cancel can't carry into list mode and the hint footer/handler stay in sync. - Test factory uses a `baseProps` spread instead of `as` cast so a future required field on `ToolMessageProps` is a compile-time miss.
…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).
…ng (QwenLM#3768) * feat(cli): route foreground subagents through pill+dialog while running Foreground (synchronous) subagents currently render a live AgentExecutionDisplay inside the parent's pendingHistoryItems block. The frame mutates on every tool call and approval; once it grows past the terminal height (verbose mode, parallel subagents, long tool-call lists) the live-area repaint flickers visibly. This change extends BackgroundTaskRegistry with a flavor: 'foreground' | 'background' discriminator. Foreground entries register at the start of the synchronous tool-call and unregister in its finally path. The pill counts them; the dialog drills into their activity. The inline frame is suppressed during the live phase — only an active, focus-locked approval prompt renders, as a small banner labeled with the originating agent. Once the parent turn commits, the full AgentExecutionDisplay appears in scrollback via Ink's <Static>, exactly as before. Foreground entries skip the XML task-notification (the parent receives the result through the normal tool-result channel) and skip the headless holdback (the parent's await already pins the loop). The dialog gates per-agent cancellation behind a two-step confirm so a stray 'x' can't end the user's current turn. * fix(cli): address review findings on foreground subagent routing - Gate `registerCallback` on background flavor so foreground entries don't leak orphaned `task_started` SDK events without a matching terminal notification. - Render a queued-approval marker for non-focus subagents instead of returning null, so a queued approval is visible in the main view. - Move `emitStatusChange` before `agents.delete` in `unregisterForeground` to match the ordering used by complete/fail/cancel/finalize. - Prefix the foreground tool result with a cancel marker when `terminateMode === CANCELLED`, so the parent model can distinguish a user-cancelled run from a successful completion. - Mirror the background path's stats wiring on the foreground path so `entry.stats` stays current and the dialog detail subtitle shows tool count + tokens for foreground runs. - Remove the unreachable `isWaitingForOtherApproval` branch (subsumed by the queued-approval marker above). - Reset the foreground confirm-step on detail-mode `left` and ignore `x` on terminal entries so an armed cancel can't carry into list mode and the hint footer/handler stay in sync. - Test factory uses a `baseProps` spread instead of `as` cast so a future required field on `ToolMessageProps` is a compile-time miss.
…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).
Summary
AgentExecutionDisplayis suppressed and the run is surfaced through the existing footer pill +BackgroundTasksDialoginstead. After the parent turn commits, the full frame appears in scrollback unchanged. An active approval prompt that holds the focus lock still renders inline as a small banner labeled with the originating agent's name.pendingHistoryItemsrepaint produced visible flicker — jittery scrolling and stuttering text for the duration of the run. Routing the live phase through the pill removes the flicker class entirely without changing the committed scrollback view.flavor: 'foreground' | 'background'discriminator onBackgroundTaskEntryand the gating it adds foremitNotification,hasUnfinalizedTasks, and the cancel grace timer — foreground entries bypass all three.AbortControllerinagent.ts's synchronous path: parent abort propagates down, child cancel does not propagate up. Cleanup is infinallyso we always unregister on success, failure, cancel, or unexpected throw.xcan't end the current turn).isPendingplumbing throughHistoryItemDisplay → ToolGroupMessage → ToolMessage → SubagentExecutionRenderer. Committed renders are unchanged; the live render branch decides between "approval banner" and "nothing".Design doc:
knowledge/qwen-code/design/unified-subagent-display.md(local).Validation
npm run build && npm run bundlenode dist/cli.js --approval-mode yoloand ask the model to launch a long-running general-purpose subagent.Scope / Risk
recentActivitiesrendering, not anAgentExecutionDisplay-grade view. The design doc calls that out as a deferred follow-up — the registry intentionally does not store a liveAgentResultDisplayreference because the agent runtime'supdateDisplayreplaces the object on every event, leaving any stored reference stale.flavoris optional onBackgroundTaskEntryand defaults to background semantics when absent, so older callers and persisted state behave as before.Testing Matrix
Testing matrix notes:
npm run build && npm run bundle && node dist/cli.js. Linux/Windows not tested in this PR — the change is renderer + registry logic, no platform-specific paths touched.Linked Issues / Bugs
None.