feat(cli): wire Monitor entries into combined Background tasks dialog#3791
Conversation
Phase C mirror follow-up of Issue #3634, structurally a clean repeat of #3720 but for Monitor (third consumer of the kind framework). Core (MonitorRegistry): - Add `setStatusChangeCallback` mirroring `BackgroundShellRegistry` / `BackgroundTaskRegistry`. Fires synchronously inside `register()` (so subscribers see lifecycle start) and inside `settle()` (so subscribers see every running → terminal transition: complete / fail / cancel / emitEvent's auto-stop at maxEvents). - Subscriber failures are caught and logged but do not poison the registry — same defensive contract as the other two registries. CLI: - `useBackgroundTaskView` subscribes to all three registries (agent + shell + monitor) and merges by `startTime`. `DialogEntry` union extended with `(MonitorEntry & { kind: 'monitor' })`. `entryId` switches over the three kinds and returns the right id field. - `BackgroundTasksPill.getPillLabel` adds monitor to its KIND_NAMES table; grouping order is shell → agent → monitor (monitor last because it tends to be the longest-lived, least urgent to glance at). - `BackgroundTasksDialog`: - `rowLabel` returns `[monitor] <description>` for monitor rows. - New `MonitorDetailBody` showing command / status / pid / event count / dropped lines. No Progress block (monitors don't fire activity callbacks per-event). - DetailBody dispatcher gains the monitor branch. - `BackgroundTaskViewContext.cancelSelected` routes monitor cancels via `monitorRegistry.cancel(monitorId)`. Monitor's cancel is synchronous (settle + abort happen inside the registry), matching the same path task_stop already uses. Tests: 6 new core tests (registry callbacks fire on register / each terminal transition / not on emitEvent / on auto-stop / clear stops notifications / subscriber-failure isolation), 4 new pill tests (singular / plural / 3-kind grouping / running-only filter), dialog mock extended with `getMonitorRegistry`.
ESLint's `default-case` rule requires every switch to have a default
arm even when TypeScript can prove the union is exhaustive. Add
`default: { const _exhaustive: never = entry; throw ... }` to the
five switches added in this PR — same pattern keeps both the runtime
guard and the compile-time exhaustiveness check.
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — Qwen3.6-Plus-DogFooding via Qwen Code /review
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.
[Critical] packages/core/src/services/monitorRegistry.ts — reset() clears all monitor entries without notifying the new status-change subscriber. The background task view now refreshes monitor rows via monitorRegistry.setStatusChangeCallback(refresh), so a /clear or session reset can leave stale monitor rows visible until an unrelated registry event happens.
Suggested fix:
this.monitors.clear();
this.fireStatusChange();— gpt-5.5 via Qwen Code /review
The newly-added `setStatusChangeCallback` subscriber misses `reset()`, so a `/clear` or session reset leaves stale monitor rows visible in the combined Background tasks dialog until an unrelated register/settle event happens. Both BackgroundShellRegistry and BackgroundTaskRegistry already fire statusChange on their reset paths — Monitor was the outlier. Fix: fire `statusChange()` (no arg) after `monitors.clear()`, with an early return when the registry is already empty so we don't notify on a no-op reset. Two new tests cover both branches.
|
Fixed in f26b700. Mirrors the pattern in |
wenshao
left a comment
There was a problem hiding this comment.
Review of #3791 — Monitor entries wired into the combined Background tasks dialog.
Four inline comments below. All Suggestion-level (no blockers); CI is green and the deterministic checks (eslint, tsc on cli + core) are clean against this branch.
Two additional notes that don't anchor cleanly to a diff line:
-
Stale JSDoc at
packages/cli/src/ui/contexts/BackgroundTaskViewContext.tsx:33-37—BackgroundTaskViewState.entriesstill says "across both registries (subagents + managed shells)" and "agent vs shell". The hook's own header comment was updated to mention all three; this sibling docstring drifted. -
/tasksslash command not updated —packages/cli/src/ui/commands/tasksCommand.tsstill definesTaskEntry = AgentTaskEntry | ShellTaskEntryand only merges agent + shell registries. After this PR,Ctrl+Tshows monitors and the pill counts them, but/tasksis silent on monitors — inconsistent with the "single combined Background tasks" framing. If deferred to a follow-up, a// TODO: include monitorsbreadcrumb would help discoverability when the next kind lands.
Nice-to-have items not posted inline (cumulative drag if all addressed at once): MonitorDetailBody rendering tests for the pid / event-count / droppedLines / terminal-status branches; a [monitor] rowLabel assertion; no useBackgroundTaskView.test.ts exists at all (3-registry merge order + cleanup untested through the hook surface, only via TS); the JSON.stringify(_exhaustive) runtime would throw circular-ref if a future 4th kind carries a Timeout (since MonitorEntry.idleTimer already proves this shape exists in the union).
— claude-opus-4-7 via Claude Code /qreview
Fixed in f26b700. |
There was a problem hiding this comment.
Pull request overview
Wires MonitorRegistry entries into the CLI’s unified Background tasks view so monitors appear in the footer pill, combined tasks dialog, and support the same cancel keybinding flow as agents/shells.
Changes:
- Add
setStatusChangeCallbacktoMonitorRegistry(fires on register/settle/reset) to support UI subscriptions. - Extend
useBackgroundTaskView’s mergedDialogEntryunion to includemonitorentries and stableentryIdhandling. - Update Background tasks pill/dialog/context to render/cancel monitor entries, including a new
MonitorDetailBody.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/core/src/services/monitorRegistry.ts | Adds status-change subscription callback and emits it on register/settle/reset. |
| packages/core/src/services/monitorRegistry.test.ts | Adds unit tests for the new status-change callback semantics. |
| packages/cli/src/ui/hooks/useBackgroundTaskView.ts | Merges monitor entries into the dialog snapshot and extends entryId for monitor. |
| packages/cli/src/ui/contexts/BackgroundTaskViewContext.tsx | Routes cancel keybinding to monitorRegistry.cancel(monitorId) for monitor entries. |
| packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx | Adds monitor counts/labels and includes monitors in grouping order. |
| packages/cli/src/ui/components/background-view/BackgroundTasksPill.test.tsx | Adds monitor pill-label test cases. |
| packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx | Adds monitor row labeling, detail-body dispatch, and MonitorDetailBody. |
| packages/cli/src/ui/components/background-view/BackgroundTasksDialog.test.tsx | Updates dialog tests/mocks to support monitor entry IDs and cancel path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Four review threads from /review's second pass on top of f26b700: 1. **MonitorDetailBody live counters were stale.** `eventCount` and `droppedLines` came from the `useBackgroundTaskView` snapshot, which only refreshes on `statusChange`, and `emitEvent` deliberately doesn't fire `statusChange` (the per-event churn would burn the pill / AppContainer). Fix: extend the existing `selectedEntry` useMemo to re-resolve monitors from `monitorRegistry.get()` on each `activityTick`, mirroring the agent path. The pre-existing 1s wall-clock interval already drives the recompute while the entry is running, so no new callback wiring is needed. 2. **Settle reasons weren't persisted on `MonitorEntry`.** `fail()`, `complete(exitCode)`, max-events auto-stop, and idle-timeout all passed their reason strings only to the chat-history terminal notification — opening the dialog detail view after the monitor died showed the bare status with no clue why. Add `exitCode?` and `error?` fields (mirrors `BackgroundShellEntry`); populate them before `settle()` in each path; render them in `MonitorDetailBody` with appropriate colour (red for `failed`, warning for auto-stopped `completed`). 3. **`monitorCancel` mock had no test asserting it.** Add a dedicated test that opens detail on a monitor entry, presses `x`, and verifies `monitorRegistry.cancel(monitorId)` was called and the agent registry's cancel was NOT called. Pins the kind switch in `cancelSelected` so a regression flipping the monitor branch to anything else (e.g. `requestCancel`) would fail loudly. 4. **`MonitorStatusChangeCallback` docstring was wrong.** It claimed the callback fires on running → terminal transitions, but `register()` (nothing → running) and `reset()` (mass clear, fired with no entry) also fire it. Rewrite the docstring to enumerate the actual fire sites and explicitly note that `emitEvent` is excluded (per-event churn).
wenshao
left a comment
There was a problem hiding this comment.
No review findings on the current PR head. Downgraded from Approve to Comment: self-PR; CI checks are still pending. — gpt-5.5 via Qwen Code /review
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Two doc-precision fixes from copilot's PR 3791 review pass: - `MonitorEntry.error` enumerated max-events as the only auto-stop reason that populates the field, but `idle-timeout` also writes it (was added in the same earlier commit). Rewrote to list both current reasons and explicitly note: any future auto-stop reason should populate this field too. Also clarified that `cancelled` and natural-exit `completed` paths intentionally don't. - `rowLabel`'s shell-branch comment claimed long commands are "truncated by the row renderer's MaxSizedBox", but ListBody renders rows with plain `<Text>` (no MaxSizedBox / truncation helper). Long text wraps. Rewrote to describe actual behaviour and note the intentional decision to leave it wrapping (the dialog is what users open to see context — truncating defeats the purpose).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…kView Two coverage gaps from /review's third pass on PR 3791: - New file `useBackgroundTaskView.test.ts` (6 cases) directly exercises the merge logic with a stub config: empty when config is null, merges three registries, sorts by startTime, tags `kind`, subscribes on mount, refreshes when any registry fires statusChange, clears all three subscriptions on unmount. - New `MonitorDetailBody render branches` describe block in `BackgroundTasksDialog.test.tsx` (8 cases) covers the conditional pieces my last commit added: title + Command, pid show/hide, eventCount singular vs plural, droppedLines show/hide, exitCode display, Error block (failed) vs Stopped because (auto-stop), and the all-undefined no-block case.
End-to-end tmux verificationBuilt locally and exercised the full UI flow against a live LLM session — pill, dialog, detail view, and cancel keybinding all work as described in the PR summary. Setupnpm run build
# Temp HOME with a clean settings.json (the qwen-code currently in main has
# a known proxy-shape regression unrelated to this PR; use --bare or strip
# the `proxy` field if you hit it).
mkdir -p /tmp/qwen-test-home/.qwen && cp ~/.qwen/settings.json /tmp/qwen-test-home/.qwen/
# Allow the monitor tool (default permission is `ask`/`deny`)
# either via approvalMode=yolo or by adding `monitor` to permissions.allow.
tmux new-session -d -s qwen-monitor -x 200 -y 60 \
"HOME=/tmp/qwen-test-home node packages/cli/dist/index.js -y -i '<PROMPT BELOW>'"Prompt
Results
SnapshotsOpen dialog (Down on empty composer focuses pill, Enter opens dialog): Detail view (Enter on a row): After Coverage gaps acknowledged
For reference, the unit tests added in this PR ( LGTM 👍 |
Follow-up: pill collapse + dialog trigger correctionTwo items from the manual smoke checklist still needed an explicit check, so I ran a minimal supplementary test: Pill collapse to Spawned two short monitors that exit naturally after ~5s:
Matches Dialog trigger — the test plan item says With pill collapse confirmed and the dialog trigger clarified, all manual-smoke items in the test plan are now verified end-to-end. |
doudouOUC
left a comment
There was a problem hiding this comment.
Technical Review: PR #3791
Scope: +791 / −49, 9 files — wires MonitorRegistry into the combined Background tasks pill + dialog UI.
Architecture Consistency — Excellent
The change strictly follows the established patterns from BackgroundTaskRegistry and BackgroundShellRegistry:
MonitorRegistry.setStatusChangeCallback()mirrors the same single-subscriber, error-safe callback contract.useBackgroundTaskViewsymmetrically subscribes to all three registries and merges bystartTime.DialogEntryunion extended with(MonitorEntry & { kind: 'monitor' })— same pattern as the shell branch.- The 5 new
switchstatements usedefault: { const _exhaustive: never = entry; throw ... }— excellent defensive pattern that gives both compile-time exhaustiveness and runtime safety if a 4th kind is ever added.
Core Layer — Good
setStatusChangeCallback: Fires on register() (nothing→running) and settle() (running→terminal). Intentionally does NOT fire on emitEvent() — avoids per-event pill/dialog churn under heavy traffic. Sound design.
New fields exitCode and error on MonitorEntry: Well-documented JSDoc explaining when each is populated vs not. The error field now captures auto-stop reasons (Max events reached, Idle timeout) which the dialog surfaces meaningfully.
reset() empty guard: if (this.monitors.size === 0) return; — minor optimization, correctly tested.
UI Layer — Good, with observations
MonitorDetailBody: Clean rendering of command, status, pid, event count (singular/plural), dropped lines, exit code, and error/stop-reason blocks. The intentional omission of a Progress block is well-reasoned — monitor events stream through task_notification to the agent, and double-rendering would be noisy and create per-event redraw burden.
Inconsistency note between MonitorDetailBody and ShellDetailBody:
| Aspect | ShellDetailBody | MonitorDetailBody |
|---|---|---|
| Exit code display | Only when status === 'completed' |
Whenever exitCode !== undefined |
| Error display | Only when status === 'failed' && entry.error |
Whenever Boolean(entry.error) |
This inconsistency is justified — monitor's error field doubles as auto-stop reason on completed entries, while shell's error is spawn-only. Different semantics warrant different rendering logic. Not a bug.
Cancel Semantics — Correct
cancelSelected routes monitor → monitorRegistry.cancel(monitorId) (synchronous: abort + settle + endTime inline), vs shell → requestCancel() (async-ish: abort only, settle happens later). This matches the task_stop paths and is documented clearly. No race window.
Test Coverage — Good
| File | New tests | Coverage |
|---|---|---|
monitorRegistry.test.ts |
8 | Callback lifecycle, error safety, reset, unmount cleanup |
useBackgroundTaskView.test.ts |
6 | Merge, sort, subscription, cleanup |
BackgroundTasksDialog.test.tsx |
9 | Cancel routing, detail body render, singular/plural, error vs auto-stop |
BackgroundTasksPill.test.tsx |
4 | Monitor singular/plural, three-kind grouping, terminal filtering |
Test quality highlights:
- Cancel test verifies
monitorCancelis called AND agent cancel is NOT called — prevents kind-dispatch fallback bugs. - Singular/plural test checks
"1 event"doesn't match"1 events"— prevents substring false positives. - Error block test verifies
'failed'shows "Error" but not "Stopped because", and vice versa — prevents render-branch collapse. - Unmount cleanup test asserts
setStatusChangeCallbackcalled with function thenundefined— prevents stale subscribers.
Minor Issues (non-blocking)
-
groupAndFormatuses dynamiccounts[e.kind]++instead of exhaustiveswitch— every other kind-dispatch in the codebase uses switch + exhaustive guard. IfDialogEntryunion is ever extended without updatingcounts, this silently producesNaN. Not a current bug, but a maintenance hazard that's inconsistent with the rest of the PR's approach. -
No direct test for
selectedEntrymonitor re-read path — the agent re-read path has indirect coverage, but the newif (fromSnapshot.kind === 'monitor') { ... get(). ... }branch in the dialog'suseMemoisn't directly unit-tested. The code path is simple (get + spread), but it's a new branch worth a test case. -
exitCoderendered regardless of status inMonitorDetailBody— currently impossible for afailedentry to haveexitCodeset (onlycomplete()writes it), so this is theoretical. Just noting the asymmetry withShellDetailBodywhich guards onstatus === 'completed'.
Performance — No Regressions
statusChangeintentionally excludes per-emitEventfiring — correct for high-throughput monitors.- The existing 1s wall-clock tick now also drives monitor counter refresh. No new timer overhead.
- Merge+sort remains O(n log n) with negligible n.
Security — No Concerns
- New
error/exitCodefields are internal data, not user-supplied. MonitorDetailBodyrenders text through React/Ink Text nodes — no injection vector.- Registry's XML escaping and control-char stripping are untouched.
Backward Compatibility — Excellent
- New fields are optional on
MonitorEntry. DialogEntryunion extension is additive.cancelSelectedrefactor fromif/elsetoswitchis functionally equivalent.setStatusChangeCallbackis a new method — no breaking change.
Verdict: Approve. The issues above are all minor/style-level and don't affect correctness. This PR is well-structured, well-tested, and turns the generic kind framework from claim into evidence with three real consumers — a significant architectural milestone.
— from GLM-5.1
Tmux verification on PR #3801 caught that the hint string says "Ctrl+T opens the interactive Background tasks dialog" but Ctrl+T is actually bound to the MCP tool descriptions toggle (ContextSummaryDisplay.tsx lines 110-115). The dialog opens via Down arrow on an empty composer (focuses the footer pill) followed by Enter (InputPrompt.tsx 947-968). Same misattribution slipped into PR #3791's first description and was caught + fixed there before merge — this PR carried the wrong wording forward in code. Updates four sites: - The hint string itself: "Tip: press ↓ from an empty composer then Enter to open the interactive Background tasks dialog with detail view + live updates." - The slash-command description: "interactive UI is Ctrl+T" → "interactive dialog opens via the footer pill" - Two inline comments referencing Ctrl+T as the dialog opener - The interactive-mode hint test now pins on `↓` + `Enter` and asserts `not.toContain('Ctrl+T')` so a regression to the wrong wording fails loudly.
) * feat(cli): include monitors in /tasks + add interactive-mode hint Phase B closure for Issue #3634. Two coupled changes to /tasks: 1. **Bug fix — include monitors.** The command was last touched before #3684 / #3791 landed, so it merged only agent + shell entries while monitors silently disappeared from the headless / non-interactive / ACP listing path. Add a third registry pull from `getMonitorRegistry()` and wire monitor through statusLabel / taskLabel / taskId / taskOutputPath. Status line includes eventCount (`running (N events)`, `completed (exit 0, N events)`, `completed (Max events reached, N events)` for auto-stop) and pid where defined. 2. **Soft deprecation hint, scoped to interactive mode only.** Once the richer Ctrl+T dialog (#3488 + #3720 + #3791) is available, the text dump is the long-form fallback rather than the primary surface. Show `Tip: Ctrl+T opens the interactive Background tasks dialog with detail view + live updates.` at the top of the output when `executionMode === 'interactive'`. Headless / ACP get the bare list — they have no dialog to point at and the hint would just clutter. Description string also clarified to call out the modal split. Kept on all three executionModes (no deletion) — `/tasks` is the only way headless / ACP / SDK consumers can inspect background-task state. Tests: 4 new cases in tasksCommand.test.ts cover monitor entry formatting (running with pid, natural completion with exitCode, auto-stop with error string, failed), the singular `1 event` form, the interactive-mode hint gating, and the cross-kind merge order. * fix(cli): address PR 3801 review — exhaustive switch + i18n + extra tests Three actionable Suggestions from /review's pass: - `taskLabel` rewritten as a `switch` with a `never`-typed `default` arm, matching the structural-safety pattern already used by `taskId`. Adding a 4th DialogEntry kind in the future will now flip both helpers to compile errors instead of letting `taskLabel` silently fall through to `entry.description` (which the new kind may not have). - Hint string wrapped in `t()` for i18n consistency with the rest of the file. The literal stays as the i18n key default, so today's output is unchanged. - Tests: cover `cancelled` monitor status (was the only one without an inline assertion) and explicit `acp` execution mode hint suppression (pins the suppression rationale so a future regression flipping the check to `!== 'non_interactive'` would fail loudly). * fix(cli): correct /tasks dialog-open hint — Ctrl+T was wrong Tmux verification on PR #3801 caught that the hint string says "Ctrl+T opens the interactive Background tasks dialog" but Ctrl+T is actually bound to the MCP tool descriptions toggle (ContextSummaryDisplay.tsx lines 110-115). The dialog opens via Down arrow on an empty composer (focuses the footer pill) followed by Enter (InputPrompt.tsx 947-968). Same misattribution slipped into PR #3791's first description and was caught + fixed there before merge — this PR carried the wrong wording forward in code. Updates four sites: - The hint string itself: "Tip: press ↓ from an empty composer then Enter to open the interactive Background tasks dialog with detail view + live updates." - The slash-command description: "interactive UI is Ctrl+T" → "interactive dialog opens via the footer pill" - Two inline comments referencing Ctrl+T as the dialog opener - The interactive-mode hint test now pins on `↓` + `Enter` and asserts `not.toContain('Ctrl+T')` so a regression to the wrong wording fails loudly. * fix(cli): address PR 3801 review — exhaustive switch consistency + path-agnostic hint Four Suggestions from the latest /review pass: - `statusLabel` rewritten as a single top-level switch with a `never`-typed default, matching `taskLabel` / `taskId` / `taskOutputPath`. The previous `if`/`if`/fallthrough form would silently apply monitor formatting to a future 4th kind. - `taskOutputPath` gained the same exhaustive default — was the only per-kind helper still relying on implicit fallthrough; would silently omit a 4th-kind output path while the adjacent helpers flip to compile errors. - Hint wording de-specifies the exact keystroke count: `'Tip: focus the Background tasks pill in the footer (use ↓ from an empty composer) and press Enter ...'`. Previous "press ↓ then Enter" phrasing was wrong when the Arena agent tab bar is present — `InputPrompt`'s focus chain routes Down through the tab bar first, so a single Down lands there, not on the bg pill. - Test pin tightened: `[mon_fail] failed: spawn ENOENT (0 events)` is now a full-string assertion instead of a prefix match, so a regression that drops the `(N events)` suffix from monitor's failed branch fails loudly. * fix(cli): sanitize ANSI escape sequences in /tasks output deepseek's review pass flagged that monitor description / error fields are user / process-supplied strings rendered directly to the terminal. A maliciously-crafted tool description or spawn error containing raw ANSI control sequences (clear-screen, cursor-move, colour) would otherwise reach stdout verbatim and corrupt display. Same risk applies to agent error / description and shell error / command — all already-existing renderers with the same exposure that this PR didn't introduce but inherits. So instead of per-field sprinkling, wrap the joined output once with `escapeAnsiCtrlCodes` (no-op when no control chars present, so cost is zero in the common case). One line change in the renderer covers every kind including any future one. Test pins the behaviour: a monitor entry with `\x1b[2J` / `\x1b[31m...` content produces output with no raw ESC bytes and visible escaped `�[...]` sequences. * docs(cli): tighten escapeAnsiCtrlCodes comments to match actual scope Two doc-precision Suggestions from copilot's pass on 0840e32: - Source comment claimed `escapeAnsiCtrlCodes` is "a no-op when no control chars" but it's narrower than that — it only handles sequences matched by `ansi-regex` (CSI / OSC / SGR — anything starting with ESC). Isolated C0/C1 control bytes like BEL, BS, VT pass through untouched. Updated the comment to enumerate the actual scope and call out that `node:util`'s `stripVTControlCharacters` would be needed if those become a concern. - Test comment had a literal raw ESC byte (octal 033) embedded in the source — visually showed `^[[...]` in editors that render ESC, but was a real ESC byte in the file rather than the escaped `�` form the sanitizer produces. Rewrote with a literal `�` text description so what the comment shows matches what the assertions check for. * fix(cli): broaden /tasks sanitization + tighten inner switch exhaustiveness Addresses 3 of 5 items from doudouOUC's PR 3801 review: - **Issue 1 (Low) — C0/C1 control byte gap**: switched from `escapeAnsiCtrlCodes` (only handles ESC-initiated ANSI sequences) to `stripUnsafeCharacters` (one-pass strip of ANSI + VT + C0/C1, with TAB/CR/LF preserved). The pre-existing exposure to bare BEL / BS / FF / VT bytes via shell entry strings is now closed for all three kinds. Test rewritten to cover both ANSI sequences AND bare control bytes (BEL, BS), and pins that surrounding printable text and line breaks survive. - **Issue 2 (Low) — inner status switches inconsistent**: the three inner `switch (entry.status)` blocks (agent / shell / monitor) used `case 'running': default: return 'running'` (or duplicated bodies). All three now have explicit `running` cases plus a `never`-typed default that throws — matches the outer `switch (entry.kind)` pattern and means a future status added to any of `BackgroundTaskEntry` / `BackgroundShellEntry` / `MonitorStatus` flips to a compile error here instead of silently returning `'running'`. - **Issue 5 (Nit) — beforeEach default change**: added an inline comment explaining why the test default overrides `createMockCommandContext`'s `'interactive'` default (`'non_interactive'` lets the hint-suppression assertions work without each test rebinding context). Issues 3 and 4 from the review are nits with no action needed (3 is already documented as intentional; 4 is a UX call about hint length that's better handled by user feedback than guess-tweaking). * fix(cli): bind status to local before exhaustive switch — fixes tsc build CI's `tsc --build` (full mode, vs `--noEmit` locally) caught that `switch (entry.status)` followed by a `never`-typed default reading `entry.status` doesn't compile. After the case arms exhaust the discriminated union, TS narrows `entry` itself to `never`, so the `.status` access in the default arm becomes "Property 'status' does not exist on type 'never'" + the resulting `any` value can't be assigned to `never`. Fix: bind `entry.status` to a local `status` const before the inner switch. The local stays typed as the per-kind status union and narrows correctly to `never` at the default arm — `const _exhaustive: never = status` is then `never = never`, valid. Standard exhaustive-switch-on-discriminator pattern; doesn't change runtime behavior or test surface, just gets past TS narrowing on the nested case.
…and the dialog (#3808) * docs(core): point background-shell guidance at both /tasks and the dialog Follow-up to PR #3801, fulfilling the "separate small PR" commitment in its description. The two model-facing strings (`shell.ts` after spawning a background shell, `task-stop.ts` after requesting cancel) referenced only `/tasks` as the inspection path, predating the interactive Background tasks dialog landing at #3488 / #3720 / #3791. Now that the dialog handles all three kinds (agent / shell / monitor), both surfaces should be visible to the LLM so it can suggest the right one based on the user's mode. Updates: - `shell.ts:865` (LLM message after `is_background: true` spawn) now surfaces both `/tasks` (text, any mode) AND the interactive dialog (footer pill + Enter, with detail view + live updates). Output file guidance retained. - `task-stop.ts:110` (LLM message after `task_stop` on a shell) same pattern: both surfaces named. - `task-stop.ts:95` comment updated to enumerate all observation paths (including the dialog). - `monitorRegistry.ts:197` comment fixed — said "/tasks dialog" which conflated two distinct surfaces. Split to "Background tasks dialog reopens or `/tasks` listings". - `backgroundShellRegistry.ts:10` (module docstring) and `:31` (shellId doc) now mention all three consumers (agent, dialog, slash command). No behavior change — pure documentation/string update. Tests untouched (none asserted on these exact strings); build + lint + 152-test core suite all clean. * docs(core): address PR 3808 review — 'captured output' + consistent ordering Three review nits: 1. (LoqU — copilot) `shell.ts:865` said the output file holds "raw content", but `shellExecutionService` runs each chunk through stripAnsi and skips non-string/binary chunks before writing. Reword to "captured output" so callers don't expect a byte-for-byte stream. 2. (LqKr — wenshao) The PR mentioned both surfaces in two different orders depending on the file: `backgroundShellRegistry.ts` listed the dialog first, while `task-stop.ts` and `shell.ts` listed `/tasks` first. Unify on the LLM-facing order — `/tasks` first, then the interactive Background tasks dialog — across all four sites. Also flips the line-31 docstring on the `shellId` field for the same reason. 3. (LqKt — wenshao, flagged for awareness only) Trim the redundant keystroke detail in shell.ts:865 to match `task-stop.ts:111`'s shorter "(footer pill + Enter)" form. Saves ~7 tokens per background shell launch in `llmContent` while still naming both surfaces. The PR description's rationale (LLM should know both surfaces exist so it can suggest the right one for the user's mode) is preserved — only the operational verbosity is trimmed. 581 tests pass; lint + typecheck clean. Pure docs / string update. * docs(core): grammar polish on PR 3808 strings Two more wording nits from copilot review: - backgroundShellRegistry.ts:10 — change "metadata the agent…need to query" to "metadata that the agent…use to query". The original phrasing reads as if the metadata itself is performing the query. - shell.ts:865 — change "Read the output file directly for the captured output." to "Read the output file directly to view the captured output." — clearer instruction to the model/user. Pure wording, no behavior change. * docs(core): grammar fix on PR 3808 monitor comment 'not visible from later Background tasks dialog reopens' read as if 'reopens' was a noun. Reword to 'not visible after reopening the Background tasks dialog or from /tasks listings'. * docs(core): round 4 wording polish on PR 3808 Four more nits from copilot: - shell.ts:865 + task-stop.ts:96,111: "footer pill + Enter" was ambiguous now that the footer renders multiple pills (background tasks vs other status indicators). Disambiguate to "focus the footer Background tasks pill, then Enter". - monitorRegistry.ts:198: re-tweak my round-3 phrasing — "after reopening the Background tasks dialog or from /tasks listings" → "in later Background tasks dialog reopens or /tasks listings". Reads as "from those surfaces" rather than "after reopening", which the reviewer found ungrammatical. - backgroundShellRegistry.ts:10,31: clarify "/tasks" as the slash command, since the codebase also uses "<projectDir>/tasks/..." on-disk paths in agent-transcript contexts. Pure wording, no behavior change. 87 affected tests pass. * docs(core): mirror /tasks + dialog guidance to monitor llmContent paths Address @doudouOUC review on PR #3808 — two Medium findings: this PR updated shell-facing strings to mention both inspection surfaces but left the parallel monitor strings without any inspection guidance, even though monitors render in the same /tasks output and the same Background tasks dialog. Restore symmetry: - monitor.ts:587-598 — append the same "/tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter — detail view + live updates)" sentence to the Monitor-started llmContent, mirroring shell.ts:865. - task-stop.ts:125-131 — the monitor cancellation llmContent had no guidance at all. Add the same "Final status will be visible via /tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter) once the process drains" line that already existed for shells at task-stop.ts:111. The (Low) commit-churn observation is a maintainer call (squash on merge); the (Info) snapshot-test gap is pre-existing and not in scope. 78 monitor + task-stop tests pass; lint + typecheck clean. * docs(core): drop drain phrasing for monitor cancel + restructure dialog comment Address PR #3808 review round 5 (doudouOUC + copilot × 2): 1. (XNoH copilot, XSBu doudouOUC — Medium) The monitor cancellation message inherited "once the process drains" from the shell branch, but `monitorRegistry.cancel()` settles synchronously — when the tool returns, the entry is already `cancelled`, not waiting on a child process. The drain qualifier is accurate for shells (which use `requestCancel()` + the AbortController and settle when the real process exits) but misleading for monitors. Reword the monitor branch in `task-stop.ts:121-130` to drop the drain phrasing and add an explanatory comment about the sync vs. async difference so future maintainers don't replicate the wording from the shell branch by reflex. 2. (XNod copilot — wording, third revision on the same comment) Restructure rather than re-litigate the preposition. The "reopens" noun framing has gone through three rounds of churn (`from later... reopens` → `after reopening...` → `in later... reopens` → and now back to `from`). Sidestep the loop by making the comment a proper sentence about WHAT the surfaces actually read: the persisted `entry.error` is the source of truth; the chat-history notification is a separate, ephemeral side channel. Avoids the noun-form "reopens" entirely. Updated test assertion to match the new "Monitor \"...\" cancelled" prefix. 51 tests pass; lint + typecheck clean.
…#3791) * feat(cli): wire Monitor entries into combined Background tasks dialog Phase C mirror follow-up of Issue #3634, structurally a clean repeat of #3720 but for Monitor (third consumer of the kind framework). Core (MonitorRegistry): - Add `setStatusChangeCallback` mirroring `BackgroundShellRegistry` / `BackgroundTaskRegistry`. Fires synchronously inside `register()` (so subscribers see lifecycle start) and inside `settle()` (so subscribers see every running → terminal transition: complete / fail / cancel / emitEvent's auto-stop at maxEvents). - Subscriber failures are caught and logged but do not poison the registry — same defensive contract as the other two registries. CLI: - `useBackgroundTaskView` subscribes to all three registries (agent + shell + monitor) and merges by `startTime`. `DialogEntry` union extended with `(MonitorEntry & { kind: 'monitor' })`. `entryId` switches over the three kinds and returns the right id field. - `BackgroundTasksPill.getPillLabel` adds monitor to its KIND_NAMES table; grouping order is shell → agent → monitor (monitor last because it tends to be the longest-lived, least urgent to glance at). - `BackgroundTasksDialog`: - `rowLabel` returns `[monitor] <description>` for monitor rows. - New `MonitorDetailBody` showing command / status / pid / event count / dropped lines. No Progress block (monitors don't fire activity callbacks per-event). - DetailBody dispatcher gains the monitor branch. - `BackgroundTaskViewContext.cancelSelected` routes monitor cancels via `monitorRegistry.cancel(monitorId)`. Monitor's cancel is synchronous (settle + abort happen inside the registry), matching the same path task_stop already uses. Tests: 6 new core tests (registry callbacks fire on register / each terminal transition / not on emitEvent / on auto-stop / clear stops notifications / subscriber-failure isolation), 4 new pill tests (singular / plural / 3-kind grouping / running-only filter), dialog mock extended with `getMonitorRegistry`. * fix(cli): add exhaustive default arms to DialogEntry switches ESLint's `default-case` rule requires every switch to have a default arm even when TypeScript can prove the union is exhaustive. Add `default: { const _exhaustive: never = entry; throw ... }` to the five switches added in this PR — same pattern keeps both the runtime guard and the compile-time exhaustiveness check. * fix(core): fire statusChange in MonitorRegistry.reset() The newly-added `setStatusChangeCallback` subscriber misses `reset()`, so a `/clear` or session reset leaves stale monitor rows visible in the combined Background tasks dialog until an unrelated register/settle event happens. Both BackgroundShellRegistry and BackgroundTaskRegistry already fire statusChange on their reset paths — Monitor was the outlier. Fix: fire `statusChange()` (no arg) after `monitors.clear()`, with an early return when the registry is already empty so we don't notify on a no-op reset. Two new tests cover both branches. * fix(cli,core): address PR 3791 review feedback Four review threads from /review's second pass on top of f26b700: 1. **MonitorDetailBody live counters were stale.** `eventCount` and `droppedLines` came from the `useBackgroundTaskView` snapshot, which only refreshes on `statusChange`, and `emitEvent` deliberately doesn't fire `statusChange` (the per-event churn would burn the pill / AppContainer). Fix: extend the existing `selectedEntry` useMemo to re-resolve monitors from `monitorRegistry.get()` on each `activityTick`, mirroring the agent path. The pre-existing 1s wall-clock interval already drives the recompute while the entry is running, so no new callback wiring is needed. 2. **Settle reasons weren't persisted on `MonitorEntry`.** `fail()`, `complete(exitCode)`, max-events auto-stop, and idle-timeout all passed their reason strings only to the chat-history terminal notification — opening the dialog detail view after the monitor died showed the bare status with no clue why. Add `exitCode?` and `error?` fields (mirrors `BackgroundShellEntry`); populate them before `settle()` in each path; render them in `MonitorDetailBody` with appropriate colour (red for `failed`, warning for auto-stopped `completed`). 3. **`monitorCancel` mock had no test asserting it.** Add a dedicated test that opens detail on a monitor entry, presses `x`, and verifies `monitorRegistry.cancel(monitorId)` was called and the agent registry's cancel was NOT called. Pins the kind switch in `cancelSelected` so a regression flipping the monitor branch to anything else (e.g. `requestCancel`) would fail loudly. 4. **`MonitorStatusChangeCallback` docstring was wrong.** It claimed the callback fires on running → terminal transitions, but `register()` (nothing → running) and `reset()` (mass clear, fired with no entry) also fire it. Rewrite the docstring to enumerate the actual fire sites and explicitly note that `emitEvent` is excluded (per-event churn). * docs(cli,core): tighten MonitorEntry.error and rowLabel comments Two doc-precision fixes from copilot's PR 3791 review pass: - `MonitorEntry.error` enumerated max-events as the only auto-stop reason that populates the field, but `idle-timeout` also writes it (was added in the same earlier commit). Rewrote to list both current reasons and explicitly note: any future auto-stop reason should populate this field too. Also clarified that `cancelled` and natural-exit `completed` paths intentionally don't. - `rowLabel`'s shell-branch comment claimed long commands are "truncated by the row renderer's MaxSizedBox", but ListBody renders rows with plain `<Text>` (no MaxSizedBox / truncation helper). Long text wraps. Rewrote to describe actual behaviour and note the intentional decision to leave it wrapping (the dialog is what users open to see context — truncating defeats the purpose). * test(cli): cover MonitorDetailBody render branches + useBackgroundTaskView Two coverage gaps from /review's third pass on PR 3791: - New file `useBackgroundTaskView.test.ts` (6 cases) directly exercises the merge logic with a stub config: empty when config is null, merges three registries, sorts by startTime, tags `kind`, subscribes on mount, refreshes when any registry fires statusChange, clears all three subscriptions on unmount. - New `MonitorDetailBody render branches` describe block in `BackgroundTasksDialog.test.tsx` (8 cases) covers the conditional pieces my last commit added: title + Command, pid show/hide, eventCount singular vs plural, droppedLines show/hide, exitCode display, Error block (failed) vs Stopped because (auto-stop), and the all-undefined no-block case.
) * feat(cli): include monitors in /tasks + add interactive-mode hint Phase B closure for Issue #3634. Two coupled changes to /tasks: 1. **Bug fix — include monitors.** The command was last touched before #3684 / #3791 landed, so it merged only agent + shell entries while monitors silently disappeared from the headless / non-interactive / ACP listing path. Add a third registry pull from `getMonitorRegistry()` and wire monitor through statusLabel / taskLabel / taskId / taskOutputPath. Status line includes eventCount (`running (N events)`, `completed (exit 0, N events)`, `completed (Max events reached, N events)` for auto-stop) and pid where defined. 2. **Soft deprecation hint, scoped to interactive mode only.** Once the richer Ctrl+T dialog (#3488 + #3720 + #3791) is available, the text dump is the long-form fallback rather than the primary surface. Show `Tip: Ctrl+T opens the interactive Background tasks dialog with detail view + live updates.` at the top of the output when `executionMode === 'interactive'`. Headless / ACP get the bare list — they have no dialog to point at and the hint would just clutter. Description string also clarified to call out the modal split. Kept on all three executionModes (no deletion) — `/tasks` is the only way headless / ACP / SDK consumers can inspect background-task state. Tests: 4 new cases in tasksCommand.test.ts cover monitor entry formatting (running with pid, natural completion with exitCode, auto-stop with error string, failed), the singular `1 event` form, the interactive-mode hint gating, and the cross-kind merge order. * fix(cli): address PR 3801 review — exhaustive switch + i18n + extra tests Three actionable Suggestions from /review's pass: - `taskLabel` rewritten as a `switch` with a `never`-typed `default` arm, matching the structural-safety pattern already used by `taskId`. Adding a 4th DialogEntry kind in the future will now flip both helpers to compile errors instead of letting `taskLabel` silently fall through to `entry.description` (which the new kind may not have). - Hint string wrapped in `t()` for i18n consistency with the rest of the file. The literal stays as the i18n key default, so today's output is unchanged. - Tests: cover `cancelled` monitor status (was the only one without an inline assertion) and explicit `acp` execution mode hint suppression (pins the suppression rationale so a future regression flipping the check to `!== 'non_interactive'` would fail loudly). * fix(cli): correct /tasks dialog-open hint — Ctrl+T was wrong Tmux verification on PR #3801 caught that the hint string says "Ctrl+T opens the interactive Background tasks dialog" but Ctrl+T is actually bound to the MCP tool descriptions toggle (ContextSummaryDisplay.tsx lines 110-115). The dialog opens via Down arrow on an empty composer (focuses the footer pill) followed by Enter (InputPrompt.tsx 947-968). Same misattribution slipped into PR #3791's first description and was caught + fixed there before merge — this PR carried the wrong wording forward in code. Updates four sites: - The hint string itself: "Tip: press ↓ from an empty composer then Enter to open the interactive Background tasks dialog with detail view + live updates." - The slash-command description: "interactive UI is Ctrl+T" → "interactive dialog opens via the footer pill" - Two inline comments referencing Ctrl+T as the dialog opener - The interactive-mode hint test now pins on `↓` + `Enter` and asserts `not.toContain('Ctrl+T')` so a regression to the wrong wording fails loudly. * fix(cli): address PR 3801 review — exhaustive switch consistency + path-agnostic hint Four Suggestions from the latest /review pass: - `statusLabel` rewritten as a single top-level switch with a `never`-typed default, matching `taskLabel` / `taskId` / `taskOutputPath`. The previous `if`/`if`/fallthrough form would silently apply monitor formatting to a future 4th kind. - `taskOutputPath` gained the same exhaustive default — was the only per-kind helper still relying on implicit fallthrough; would silently omit a 4th-kind output path while the adjacent helpers flip to compile errors. - Hint wording de-specifies the exact keystroke count: `'Tip: focus the Background tasks pill in the footer (use ↓ from an empty composer) and press Enter ...'`. Previous "press ↓ then Enter" phrasing was wrong when the Arena agent tab bar is present — `InputPrompt`'s focus chain routes Down through the tab bar first, so a single Down lands there, not on the bg pill. - Test pin tightened: `[mon_fail] failed: spawn ENOENT (0 events)` is now a full-string assertion instead of a prefix match, so a regression that drops the `(N events)` suffix from monitor's failed branch fails loudly. * fix(cli): sanitize ANSI escape sequences in /tasks output deepseek's review pass flagged that monitor description / error fields are user / process-supplied strings rendered directly to the terminal. A maliciously-crafted tool description or spawn error containing raw ANSI control sequences (clear-screen, cursor-move, colour) would otherwise reach stdout verbatim and corrupt display. Same risk applies to agent error / description and shell error / command — all already-existing renderers with the same exposure that this PR didn't introduce but inherits. So instead of per-field sprinkling, wrap the joined output once with `escapeAnsiCtrlCodes` (no-op when no control chars present, so cost is zero in the common case). One line change in the renderer covers every kind including any future one. Test pins the behaviour: a monitor entry with `\x1b[2J` / `\x1b[31m...` content produces output with no raw ESC bytes and visible escaped `�[...]` sequences. * docs(cli): tighten escapeAnsiCtrlCodes comments to match actual scope Two doc-precision Suggestions from copilot's pass on 0840e32: - Source comment claimed `escapeAnsiCtrlCodes` is "a no-op when no control chars" but it's narrower than that — it only handles sequences matched by `ansi-regex` (CSI / OSC / SGR — anything starting with ESC). Isolated C0/C1 control bytes like BEL, BS, VT pass through untouched. Updated the comment to enumerate the actual scope and call out that `node:util`'s `stripVTControlCharacters` would be needed if those become a concern. - Test comment had a literal raw ESC byte (octal 033) embedded in the source — visually showed `^[[...]` in editors that render ESC, but was a real ESC byte in the file rather than the escaped `�` form the sanitizer produces. Rewrote with a literal `�` text description so what the comment shows matches what the assertions check for. * fix(cli): broaden /tasks sanitization + tighten inner switch exhaustiveness Addresses 3 of 5 items from doudouOUC's PR 3801 review: - **Issue 1 (Low) — C0/C1 control byte gap**: switched from `escapeAnsiCtrlCodes` (only handles ESC-initiated ANSI sequences) to `stripUnsafeCharacters` (one-pass strip of ANSI + VT + C0/C1, with TAB/CR/LF preserved). The pre-existing exposure to bare BEL / BS / FF / VT bytes via shell entry strings is now closed for all three kinds. Test rewritten to cover both ANSI sequences AND bare control bytes (BEL, BS), and pins that surrounding printable text and line breaks survive. - **Issue 2 (Low) — inner status switches inconsistent**: the three inner `switch (entry.status)` blocks (agent / shell / monitor) used `case 'running': default: return 'running'` (or duplicated bodies). All three now have explicit `running` cases plus a `never`-typed default that throws — matches the outer `switch (entry.kind)` pattern and means a future status added to any of `BackgroundTaskEntry` / `BackgroundShellEntry` / `MonitorStatus` flips to a compile error here instead of silently returning `'running'`. - **Issue 5 (Nit) — beforeEach default change**: added an inline comment explaining why the test default overrides `createMockCommandContext`'s `'interactive'` default (`'non_interactive'` lets the hint-suppression assertions work without each test rebinding context). Issues 3 and 4 from the review are nits with no action needed (3 is already documented as intentional; 4 is a UX call about hint length that's better handled by user feedback than guess-tweaking). * fix(cli): bind status to local before exhaustive switch — fixes tsc build CI's `tsc --build` (full mode, vs `--noEmit` locally) caught that `switch (entry.status)` followed by a `never`-typed default reading `entry.status` doesn't compile. After the case arms exhaust the discriminated union, TS narrows `entry` itself to `never`, so the `.status` access in the default arm becomes "Property 'status' does not exist on type 'never'" + the resulting `any` value can't be assigned to `never`. Fix: bind `entry.status` to a local `status` const before the inner switch. The local stays typed as the per-kind status union and narrows correctly to `never` at the default arm — `const _exhaustive: never = status` is then `never = never`, valid. Standard exhaustive-switch-on-discriminator pattern; doesn't change runtime behavior or test surface, just gets past TS narrowing on the nested case.
…and the dialog (#3808) * docs(core): point background-shell guidance at both /tasks and the dialog Follow-up to PR #3801, fulfilling the "separate small PR" commitment in its description. The two model-facing strings (`shell.ts` after spawning a background shell, `task-stop.ts` after requesting cancel) referenced only `/tasks` as the inspection path, predating the interactive Background tasks dialog landing at #3488 / #3720 / #3791. Now that the dialog handles all three kinds (agent / shell / monitor), both surfaces should be visible to the LLM so it can suggest the right one based on the user's mode. Updates: - `shell.ts:865` (LLM message after `is_background: true` spawn) now surfaces both `/tasks` (text, any mode) AND the interactive dialog (footer pill + Enter, with detail view + live updates). Output file guidance retained. - `task-stop.ts:110` (LLM message after `task_stop` on a shell) same pattern: both surfaces named. - `task-stop.ts:95` comment updated to enumerate all observation paths (including the dialog). - `monitorRegistry.ts:197` comment fixed — said "/tasks dialog" which conflated two distinct surfaces. Split to "Background tasks dialog reopens or `/tasks` listings". - `backgroundShellRegistry.ts:10` (module docstring) and `:31` (shellId doc) now mention all three consumers (agent, dialog, slash command). No behavior change — pure documentation/string update. Tests untouched (none asserted on these exact strings); build + lint + 152-test core suite all clean. * docs(core): address PR 3808 review — 'captured output' + consistent ordering Three review nits: 1. (LoqU — copilot) `shell.ts:865` said the output file holds "raw content", but `shellExecutionService` runs each chunk through stripAnsi and skips non-string/binary chunks before writing. Reword to "captured output" so callers don't expect a byte-for-byte stream. 2. (LqKr — wenshao) The PR mentioned both surfaces in two different orders depending on the file: `backgroundShellRegistry.ts` listed the dialog first, while `task-stop.ts` and `shell.ts` listed `/tasks` first. Unify on the LLM-facing order — `/tasks` first, then the interactive Background tasks dialog — across all four sites. Also flips the line-31 docstring on the `shellId` field for the same reason. 3. (LqKt — wenshao, flagged for awareness only) Trim the redundant keystroke detail in shell.ts:865 to match `task-stop.ts:111`'s shorter "(footer pill + Enter)" form. Saves ~7 tokens per background shell launch in `llmContent` while still naming both surfaces. The PR description's rationale (LLM should know both surfaces exist so it can suggest the right one for the user's mode) is preserved — only the operational verbosity is trimmed. 581 tests pass; lint + typecheck clean. Pure docs / string update. * docs(core): grammar polish on PR 3808 strings Two more wording nits from copilot review: - backgroundShellRegistry.ts:10 — change "metadata the agent…need to query" to "metadata that the agent…use to query". The original phrasing reads as if the metadata itself is performing the query. - shell.ts:865 — change "Read the output file directly for the captured output." to "Read the output file directly to view the captured output." — clearer instruction to the model/user. Pure wording, no behavior change. * docs(core): grammar fix on PR 3808 monitor comment 'not visible from later Background tasks dialog reopens' read as if 'reopens' was a noun. Reword to 'not visible after reopening the Background tasks dialog or from /tasks listings'. * docs(core): round 4 wording polish on PR 3808 Four more nits from copilot: - shell.ts:865 + task-stop.ts:96,111: "footer pill + Enter" was ambiguous now that the footer renders multiple pills (background tasks vs other status indicators). Disambiguate to "focus the footer Background tasks pill, then Enter". - monitorRegistry.ts:198: re-tweak my round-3 phrasing — "after reopening the Background tasks dialog or from /tasks listings" → "in later Background tasks dialog reopens or /tasks listings". Reads as "from those surfaces" rather than "after reopening", which the reviewer found ungrammatical. - backgroundShellRegistry.ts:10,31: clarify "/tasks" as the slash command, since the codebase also uses "<projectDir>/tasks/..." on-disk paths in agent-transcript contexts. Pure wording, no behavior change. 87 affected tests pass. * docs(core): mirror /tasks + dialog guidance to monitor llmContent paths Address @doudouOUC review on PR #3808 — two Medium findings: this PR updated shell-facing strings to mention both inspection surfaces but left the parallel monitor strings without any inspection guidance, even though monitors render in the same /tasks output and the same Background tasks dialog. Restore symmetry: - monitor.ts:587-598 — append the same "/tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter — detail view + live updates)" sentence to the Monitor-started llmContent, mirroring shell.ts:865. - task-stop.ts:125-131 — the monitor cancellation llmContent had no guidance at all. Add the same "Final status will be visible via /tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter) once the process drains" line that already existed for shells at task-stop.ts:111. The (Low) commit-churn observation is a maintainer call (squash on merge); the (Info) snapshot-test gap is pre-existing and not in scope. 78 monitor + task-stop tests pass; lint + typecheck clean. * docs(core): drop drain phrasing for monitor cancel + restructure dialog comment Address PR #3808 review round 5 (doudouOUC + copilot × 2): 1. (XNoH copilot, XSBu doudouOUC — Medium) The monitor cancellation message inherited "once the process drains" from the shell branch, but `monitorRegistry.cancel()` settles synchronously — when the tool returns, the entry is already `cancelled`, not waiting on a child process. The drain qualifier is accurate for shells (which use `requestCancel()` + the AbortController and settle when the real process exits) but misleading for monitors. Reword the monitor branch in `task-stop.ts:121-130` to drop the drain phrasing and add an explanatory comment about the sync vs. async difference so future maintainers don't replicate the wording from the shell branch by reflex. 2. (XNod copilot — wording, third revision on the same comment) Restructure rather than re-litigate the preposition. The "reopens" noun framing has gone through three rounds of churn (`from later... reopens` → `after reopening...` → `in later... reopens` → and now back to `from`). Sidestep the loop by making the comment a proper sentence about WHAT the surfaces actually read: the persisted `entry.error` is the source of truth; the chat-history notification is a separate, ephemeral side channel. Avoids the noun-form "reopens" entirely. Updated test assertion to match the new "Monitor \"...\" cancelled" prefix. 51 tests pass; lint + typecheck clean.
Co-authored-by: matt korwel <matt.korwel@gmail.com> Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
…QwenLM#3791) * feat(cli): wire Monitor entries into combined Background tasks dialog Phase C mirror follow-up of Issue QwenLM#3634, structurally a clean repeat of QwenLM#3720 but for Monitor (third consumer of the kind framework). Core (MonitorRegistry): - Add `setStatusChangeCallback` mirroring `BackgroundShellRegistry` / `BackgroundTaskRegistry`. Fires synchronously inside `register()` (so subscribers see lifecycle start) and inside `settle()` (so subscribers see every running → terminal transition: complete / fail / cancel / emitEvent's auto-stop at maxEvents). - Subscriber failures are caught and logged but do not poison the registry — same defensive contract as the other two registries. CLI: - `useBackgroundTaskView` subscribes to all three registries (agent + shell + monitor) and merges by `startTime`. `DialogEntry` union extended with `(MonitorEntry & { kind: 'monitor' })`. `entryId` switches over the three kinds and returns the right id field. - `BackgroundTasksPill.getPillLabel` adds monitor to its KIND_NAMES table; grouping order is shell → agent → monitor (monitor last because it tends to be the longest-lived, least urgent to glance at). - `BackgroundTasksDialog`: - `rowLabel` returns `[monitor] <description>` for monitor rows. - New `MonitorDetailBody` showing command / status / pid / event count / dropped lines. No Progress block (monitors don't fire activity callbacks per-event). - DetailBody dispatcher gains the monitor branch. - `BackgroundTaskViewContext.cancelSelected` routes monitor cancels via `monitorRegistry.cancel(monitorId)`. Monitor's cancel is synchronous (settle + abort happen inside the registry), matching the same path task_stop already uses. Tests: 6 new core tests (registry callbacks fire on register / each terminal transition / not on emitEvent / on auto-stop / clear stops notifications / subscriber-failure isolation), 4 new pill tests (singular / plural / 3-kind grouping / running-only filter), dialog mock extended with `getMonitorRegistry`. * fix(cli): add exhaustive default arms to DialogEntry switches ESLint's `default-case` rule requires every switch to have a default arm even when TypeScript can prove the union is exhaustive. Add `default: { const _exhaustive: never = entry; throw ... }` to the five switches added in this PR — same pattern keeps both the runtime guard and the compile-time exhaustiveness check. * fix(core): fire statusChange in MonitorRegistry.reset() The newly-added `setStatusChangeCallback` subscriber misses `reset()`, so a `/clear` or session reset leaves stale monitor rows visible in the combined Background tasks dialog until an unrelated register/settle event happens. Both BackgroundShellRegistry and BackgroundTaskRegistry already fire statusChange on their reset paths — Monitor was the outlier. Fix: fire `statusChange()` (no arg) after `monitors.clear()`, with an early return when the registry is already empty so we don't notify on a no-op reset. Two new tests cover both branches. * fix(cli,core): address PR 3791 review feedback Four review threads from /review's second pass on top of 918250f: 1. **MonitorDetailBody live counters were stale.** `eventCount` and `droppedLines` came from the `useBackgroundTaskView` snapshot, which only refreshes on `statusChange`, and `emitEvent` deliberately doesn't fire `statusChange` (the per-event churn would burn the pill / AppContainer). Fix: extend the existing `selectedEntry` useMemo to re-resolve monitors from `monitorRegistry.get()` on each `activityTick`, mirroring the agent path. The pre-existing 1s wall-clock interval already drives the recompute while the entry is running, so no new callback wiring is needed. 2. **Settle reasons weren't persisted on `MonitorEntry`.** `fail()`, `complete(exitCode)`, max-events auto-stop, and idle-timeout all passed their reason strings only to the chat-history terminal notification — opening the dialog detail view after the monitor died showed the bare status with no clue why. Add `exitCode?` and `error?` fields (mirrors `BackgroundShellEntry`); populate them before `settle()` in each path; render them in `MonitorDetailBody` with appropriate colour (red for `failed`, warning for auto-stopped `completed`). 3. **`monitorCancel` mock had no test asserting it.** Add a dedicated test that opens detail on a monitor entry, presses `x`, and verifies `monitorRegistry.cancel(monitorId)` was called and the agent registry's cancel was NOT called. Pins the kind switch in `cancelSelected` so a regression flipping the monitor branch to anything else (e.g. `requestCancel`) would fail loudly. 4. **`MonitorStatusChangeCallback` docstring was wrong.** It claimed the callback fires on running → terminal transitions, but `register()` (nothing → running) and `reset()` (mass clear, fired with no entry) also fire it. Rewrite the docstring to enumerate the actual fire sites and explicitly note that `emitEvent` is excluded (per-event churn). * docs(cli,core): tighten MonitorEntry.error and rowLabel comments Two doc-precision fixes from copilot's PR 3791 review pass: - `MonitorEntry.error` enumerated max-events as the only auto-stop reason that populates the field, but `idle-timeout` also writes it (was added in the same earlier commit). Rewrote to list both current reasons and explicitly note: any future auto-stop reason should populate this field too. Also clarified that `cancelled` and natural-exit `completed` paths intentionally don't. - `rowLabel`'s shell-branch comment claimed long commands are "truncated by the row renderer's MaxSizedBox", but ListBody renders rows with plain `<Text>` (no MaxSizedBox / truncation helper). Long text wraps. Rewrote to describe actual behaviour and note the intentional decision to leave it wrapping (the dialog is what users open to see context — truncating defeats the purpose). * test(cli): cover MonitorDetailBody render branches + useBackgroundTaskView Two coverage gaps from /review's third pass on PR 3791: - New file `useBackgroundTaskView.test.ts` (6 cases) directly exercises the merge logic with a stub config: empty when config is null, merges three registries, sorts by startTime, tags `kind`, subscribes on mount, refreshes when any registry fires statusChange, clears all three subscriptions on unmount. - New `MonitorDetailBody render branches` describe block in `BackgroundTasksDialog.test.tsx` (8 cases) covers the conditional pieces my last commit added: title + Command, pid show/hide, eventCount singular vs plural, droppedLines show/hide, exitCode display, Error block (failed) vs Stopped because (auto-stop), and the all-undefined no-block case.
…enLM#3801) * feat(cli): include monitors in /tasks + add interactive-mode hint Phase B closure for Issue QwenLM#3634. Two coupled changes to /tasks: 1. **Bug fix — include monitors.** The command was last touched before QwenLM#3684 / QwenLM#3791 landed, so it merged only agent + shell entries while monitors silently disappeared from the headless / non-interactive / ACP listing path. Add a third registry pull from `getMonitorRegistry()` and wire monitor through statusLabel / taskLabel / taskId / taskOutputPath. Status line includes eventCount (`running (N events)`, `completed (exit 0, N events)`, `completed (Max events reached, N events)` for auto-stop) and pid where defined. 2. **Soft deprecation hint, scoped to interactive mode only.** Once the richer Ctrl+T dialog (QwenLM#3488 + QwenLM#3720 + QwenLM#3791) is available, the text dump is the long-form fallback rather than the primary surface. Show `Tip: Ctrl+T opens the interactive Background tasks dialog with detail view + live updates.` at the top of the output when `executionMode === 'interactive'`. Headless / ACP get the bare list — they have no dialog to point at and the hint would just clutter. Description string also clarified to call out the modal split. Kept on all three executionModes (no deletion) — `/tasks` is the only way headless / ACP / SDK consumers can inspect background-task state. Tests: 4 new cases in tasksCommand.test.ts cover monitor entry formatting (running with pid, natural completion with exitCode, auto-stop with error string, failed), the singular `1 event` form, the interactive-mode hint gating, and the cross-kind merge order. * fix(cli): address PR 3801 review — exhaustive switch + i18n + extra tests Three actionable Suggestions from /review's pass: - `taskLabel` rewritten as a `switch` with a `never`-typed `default` arm, matching the structural-safety pattern already used by `taskId`. Adding a 4th DialogEntry kind in the future will now flip both helpers to compile errors instead of letting `taskLabel` silently fall through to `entry.description` (which the new kind may not have). - Hint string wrapped in `t()` for i18n consistency with the rest of the file. The literal stays as the i18n key default, so today's output is unchanged. - Tests: cover `cancelled` monitor status (was the only one without an inline assertion) and explicit `acp` execution mode hint suppression (pins the suppression rationale so a future regression flipping the check to `!== 'non_interactive'` would fail loudly). * fix(cli): correct /tasks dialog-open hint — Ctrl+T was wrong Tmux verification on PR QwenLM#3801 caught that the hint string says "Ctrl+T opens the interactive Background tasks dialog" but Ctrl+T is actually bound to the MCP tool descriptions toggle (ContextSummaryDisplay.tsx lines 110-115). The dialog opens via Down arrow on an empty composer (focuses the footer pill) followed by Enter (InputPrompt.tsx 947-968). Same misattribution slipped into PR QwenLM#3791's first description and was caught + fixed there before merge — this PR carried the wrong wording forward in code. Updates four sites: - The hint string itself: "Tip: press ↓ from an empty composer then Enter to open the interactive Background tasks dialog with detail view + live updates." - The slash-command description: "interactive UI is Ctrl+T" → "interactive dialog opens via the footer pill" - Two inline comments referencing Ctrl+T as the dialog opener - The interactive-mode hint test now pins on `↓` + `Enter` and asserts `not.toContain('Ctrl+T')` so a regression to the wrong wording fails loudly. * fix(cli): address PR 3801 review — exhaustive switch consistency + path-agnostic hint Four Suggestions from the latest /review pass: - `statusLabel` rewritten as a single top-level switch with a `never`-typed default, matching `taskLabel` / `taskId` / `taskOutputPath`. The previous `if`/`if`/fallthrough form would silently apply monitor formatting to a future 4th kind. - `taskOutputPath` gained the same exhaustive default — was the only per-kind helper still relying on implicit fallthrough; would silently omit a 4th-kind output path while the adjacent helpers flip to compile errors. - Hint wording de-specifies the exact keystroke count: `'Tip: focus the Background tasks pill in the footer (use ↓ from an empty composer) and press Enter ...'`. Previous "press ↓ then Enter" phrasing was wrong when the Arena agent tab bar is present — `InputPrompt`'s focus chain routes Down through the tab bar first, so a single Down lands there, not on the bg pill. - Test pin tightened: `[mon_fail] failed: spawn ENOENT (0 events)` is now a full-string assertion instead of a prefix match, so a regression that drops the `(N events)` suffix from monitor's failed branch fails loudly. * fix(cli): sanitize ANSI escape sequences in /tasks output deepseek's review pass flagged that monitor description / error fields are user / process-supplied strings rendered directly to the terminal. A maliciously-crafted tool description or spawn error containing raw ANSI control sequences (clear-screen, cursor-move, colour) would otherwise reach stdout verbatim and corrupt display. Same risk applies to agent error / description and shell error / command — all already-existing renderers with the same exposure that this PR didn't introduce but inherits. So instead of per-field sprinkling, wrap the joined output once with `escapeAnsiCtrlCodes` (no-op when no control chars present, so cost is zero in the common case). One line change in the renderer covers every kind including any future one. Test pins the behaviour: a monitor entry with `\x1b[2J` / `\x1b[31m...` content produces output with no raw ESC bytes and visible escaped `�[...]` sequences. * docs(cli): tighten escapeAnsiCtrlCodes comments to match actual scope Two doc-precision Suggestions from copilot's pass on d392747a4: - Source comment claimed `escapeAnsiCtrlCodes` is "a no-op when no control chars" but it's narrower than that — it only handles sequences matched by `ansi-regex` (CSI / OSC / SGR — anything starting with ESC). Isolated C0/C1 control bytes like BEL, BS, VT pass through untouched. Updated the comment to enumerate the actual scope and call out that `node:util`'s `stripVTControlCharacters` would be needed if those become a concern. - Test comment had a literal raw ESC byte (octal 033) embedded in the source — visually showed `^[[...]` in editors that render ESC, but was a real ESC byte in the file rather than the escaped `�` form the sanitizer produces. Rewrote with a literal `�` text description so what the comment shows matches what the assertions check for. * fix(cli): broaden /tasks sanitization + tighten inner switch exhaustiveness Addresses 3 of 5 items from doudouOUC's PR 3801 review: - **Issue 1 (Low) — C0/C1 control byte gap**: switched from `escapeAnsiCtrlCodes` (only handles ESC-initiated ANSI sequences) to `stripUnsafeCharacters` (one-pass strip of ANSI + VT + C0/C1, with TAB/CR/LF preserved). The pre-existing exposure to bare BEL / BS / FF / VT bytes via shell entry strings is now closed for all three kinds. Test rewritten to cover both ANSI sequences AND bare control bytes (BEL, BS), and pins that surrounding printable text and line breaks survive. - **Issue 2 (Low) — inner status switches inconsistent**: the three inner `switch (entry.status)` blocks (agent / shell / monitor) used `case 'running': default: return 'running'` (or duplicated bodies). All three now have explicit `running` cases plus a `never`-typed default that throws — matches the outer `switch (entry.kind)` pattern and means a future status added to any of `BackgroundTaskEntry` / `BackgroundShellEntry` / `MonitorStatus` flips to a compile error here instead of silently returning `'running'`. - **Issue 5 (Nit) — beforeEach default change**: added an inline comment explaining why the test default overrides `createMockCommandContext`'s `'interactive'` default (`'non_interactive'` lets the hint-suppression assertions work without each test rebinding context). Issues 3 and 4 from the review are nits with no action needed (3 is already documented as intentional; 4 is a UX call about hint length that's better handled by user feedback than guess-tweaking). * fix(cli): bind status to local before exhaustive switch — fixes tsc build CI's `tsc --build` (full mode, vs `--noEmit` locally) caught that `switch (entry.status)` followed by a `never`-typed default reading `entry.status` doesn't compile. After the case arms exhaust the discriminated union, TS narrows `entry` itself to `never`, so the `.status` access in the default arm becomes "Property 'status' does not exist on type 'never'" + the resulting `any` value can't be assigned to `never`. Fix: bind `entry.status` to a local `status` const before the inner switch. The local stays typed as the per-kind status union and narrows correctly to `never` at the default arm — `const _exhaustive: never = status` is then `never = never`, valid. Standard exhaustive-switch-on-discriminator pattern; doesn't change runtime behavior or test surface, just gets past TS narrowing on the nested case.
…and the dialog (QwenLM#3808) * docs(core): point background-shell guidance at both /tasks and the dialog Follow-up to PR QwenLM#3801, fulfilling the "separate small PR" commitment in its description. The two model-facing strings (`shell.ts` after spawning a background shell, `task-stop.ts` after requesting cancel) referenced only `/tasks` as the inspection path, predating the interactive Background tasks dialog landing at QwenLM#3488 / QwenLM#3720 / QwenLM#3791. Now that the dialog handles all three kinds (agent / shell / monitor), both surfaces should be visible to the LLM so it can suggest the right one based on the user's mode. Updates: - `shell.ts:865` (LLM message after `is_background: true` spawn) now surfaces both `/tasks` (text, any mode) AND the interactive dialog (footer pill + Enter, with detail view + live updates). Output file guidance retained. - `task-stop.ts:110` (LLM message after `task_stop` on a shell) same pattern: both surfaces named. - `task-stop.ts:95` comment updated to enumerate all observation paths (including the dialog). - `monitorRegistry.ts:197` comment fixed — said "/tasks dialog" which conflated two distinct surfaces. Split to "Background tasks dialog reopens or `/tasks` listings". - `backgroundShellRegistry.ts:10` (module docstring) and `:31` (shellId doc) now mention all three consumers (agent, dialog, slash command). No behavior change — pure documentation/string update. Tests untouched (none asserted on these exact strings); build + lint + 152-test core suite all clean. * docs(core): address PR 3808 review — 'captured output' + consistent ordering Three review nits: 1. (LoqU — copilot) `shell.ts:865` said the output file holds "raw content", but `shellExecutionService` runs each chunk through stripAnsi and skips non-string/binary chunks before writing. Reword to "captured output" so callers don't expect a byte-for-byte stream. 2. (LqKr — wenshao) The PR mentioned both surfaces in two different orders depending on the file: `backgroundShellRegistry.ts` listed the dialog first, while `task-stop.ts` and `shell.ts` listed `/tasks` first. Unify on the LLM-facing order — `/tasks` first, then the interactive Background tasks dialog — across all four sites. Also flips the line-31 docstring on the `shellId` field for the same reason. 3. (LqKt — wenshao, flagged for awareness only) Trim the redundant keystroke detail in shell.ts:865 to match `task-stop.ts:111`'s shorter "(footer pill + Enter)" form. Saves ~7 tokens per background shell launch in `llmContent` while still naming both surfaces. The PR description's rationale (LLM should know both surfaces exist so it can suggest the right one for the user's mode) is preserved — only the operational verbosity is trimmed. 581 tests pass; lint + typecheck clean. Pure docs / string update. * docs(core): grammar polish on PR 3808 strings Two more wording nits from copilot review: - backgroundShellRegistry.ts:10 — change "metadata the agent…need to query" to "metadata that the agent…use to query". The original phrasing reads as if the metadata itself is performing the query. - shell.ts:865 — change "Read the output file directly for the captured output." to "Read the output file directly to view the captured output." — clearer instruction to the model/user. Pure wording, no behavior change. * docs(core): grammar fix on PR 3808 monitor comment 'not visible from later Background tasks dialog reopens' read as if 'reopens' was a noun. Reword to 'not visible after reopening the Background tasks dialog or from /tasks listings'. * docs(core): round 4 wording polish on PR 3808 Four more nits from copilot: - shell.ts:865 + task-stop.ts:96,111: "footer pill + Enter" was ambiguous now that the footer renders multiple pills (background tasks vs other status indicators). Disambiguate to "focus the footer Background tasks pill, then Enter". - monitorRegistry.ts:198: re-tweak my round-3 phrasing — "after reopening the Background tasks dialog or from /tasks listings" → "in later Background tasks dialog reopens or /tasks listings". Reads as "from those surfaces" rather than "after reopening", which the reviewer found ungrammatical. - backgroundShellRegistry.ts:10,31: clarify "/tasks" as the slash command, since the codebase also uses "<projectDir>/tasks/..." on-disk paths in agent-transcript contexts. Pure wording, no behavior change. 87 affected tests pass. * docs(core): mirror /tasks + dialog guidance to monitor llmContent paths Address @doudouOUC review on PR QwenLM#3808 — two Medium findings: this PR updated shell-facing strings to mention both inspection surfaces but left the parallel monitor strings without any inspection guidance, even though monitors render in the same /tasks output and the same Background tasks dialog. Restore symmetry: - monitor.ts:587-598 — append the same "/tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter — detail view + live updates)" sentence to the Monitor-started llmContent, mirroring shell.ts:865. - task-stop.ts:125-131 — the monitor cancellation llmContent had no guidance at all. Add the same "Final status will be visible via /tasks (text) or the interactive Background tasks dialog (focus the footer Background tasks pill, then Enter) once the process drains" line that already existed for shells at task-stop.ts:111. The (Low) commit-churn observation is a maintainer call (squash on merge); the (Info) snapshot-test gap is pre-existing and not in scope. 78 monitor + task-stop tests pass; lint + typecheck clean. * docs(core): drop drain phrasing for monitor cancel + restructure dialog comment Address PR QwenLM#3808 review round 5 (doudouOUC + copilot × 2): 1. (XNoH copilot, XSBu doudouOUC — Medium) The monitor cancellation message inherited "once the process drains" from the shell branch, but `monitorRegistry.cancel()` settles synchronously — when the tool returns, the entry is already `cancelled`, not waiting on a child process. The drain qualifier is accurate for shells (which use `requestCancel()` + the AbortController and settle when the real process exits) but misleading for monitors. Reword the monitor branch in `task-stop.ts:121-130` to drop the drain phrasing and add an explanatory comment about the sync vs. async difference so future maintainers don't replicate the wording from the shell branch by reflex. 2. (XNod copilot — wording, third revision on the same comment) Restructure rather than re-litigate the preposition. The "reopens" noun framing has gone through three rounds of churn (`from later... reopens` → `after reopening...` → `in later... reopens` → and now back to `from`). Sidestep the loop by making the comment a proper sentence about WHAT the surfaces actually read: the persisted `entry.error` is the source of truth; the chat-history notification is a separate, ephemeral side channel. Avoids the noun-form "reopens" entirely. Updated test assertion to match the new "Monitor \"...\" cancelled" prefix. 51 tests pass; lint + typecheck clean.
Summary
Phase C mirror follow-up of Issue #3634 — wires the Monitor tool (#3684) into the combined Background tasks pill + dialog so the third task kind is actually visible / controllable from the UI, not just from
task_stop. Structurally similar to #3720 (which did the same for shells) but with several non-trivial differences called out below.After this PR, the kind framework introduced in #3488 / generic-ized in #3720 has three real consumers (agent / shell / monitor), which is what turns "generic framework" from a claim into evidence.
Size: +357 / −40 across 8 files, 2 commits.
Commits
67424f0— main wiring (core MonitorRegistry callback + CLI hook / pill / dialog / context).7999b85— lint fix: ESLint'sdefault-caserule requires everyswitchto have a default arm even when TypeScript can prove the union exhaustive. The 5 new switches added in commit 1 each gaineddefault: { const _exhaustive: never = entry; throw ... }— this pattern keeps both the runtime guard and the compile-time exhaustiveness check (if a 4th kind ever lands, TS immediately points at the 5 sites that need a new arm; without: neverthe unhandled kind would silently fall through to thethrow).Before / After
Before
1 shell, 1 local agent— monitors invisible to the dialog/pill even thoughMonitorRegistrytracks them.↓from empty composer, thenEnter): agent + shell rows only. Monitors have no UI surface.x): can't reach monitors.After
1 shell, 1 local agent, 2 monitorswhile running; collapses toN tasks doneonce everything is terminal.Background taskssection with all three kinds, monitor rows prefixed[monitor] <description>.MonitorDetailBodyshowing command / status / pid / event count / dropped lines.x): routes monitor →monitorRegistry.cancel(monitorId)(synchronous settle, matchingtask_stop's monitor path).What changed
Core —
MonitorRegistry:setStatusChangeCallbackmirroringBackgroundShellRegistry/BackgroundTaskRegistry. Fires synchronously insideregister()(so subscribers see lifecycle start) and insidesettle()(so subscribers see every running → terminal transition: complete / fail / cancel / emitEvent's auto-stop at maxEvents).CLI:
useBackgroundTaskViewsubscribes to all three registries and merges bystartTime.DialogEntryunion extended with(MonitorEntry & { kind: 'monitor' }).entryIdswitches over the three kinds.BackgroundTasksPill.getPillLabeladds monitor to KIND_NAMES; grouping order is shell → agent → monitor (monitor last because it tends to be the longest-lived, least urgent to glance at).BackgroundTasksDialog:rowLabelreturns[monitor] <description>for monitor rows.MonitorDetailBody: command / status / pid / event count / dropped lines.BackgroundTaskViewContext.cancelSelectedroutes monitor cancels viamonitorRegistry.cancel(monitorId).Non-trivial differences vs #3720 (the shell follow-up)
Calling these out so reviewers don't have to dig:
Monitor's
cancel()is synchronous, shell's isrequestCancel()(async-ish).requestCancel()only triggersAbortController.abort()and lets the spawn's settle path record the real terminal moment + outcome (mirrors thetask_stoptool path in feat(core): wire background shells into the task_stop tool #3687).cancel()does abort + settle + endTime stamp + (optional) terminal notification all inline. SocancelSelectedcallscancel()directly, no race window.task_stopalready does for monitor (feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684), so cancel behavior is consistent across the model-facing and user-facing entry points.MonitorDetailBodydeliberately has no Progress block.recentActivities) — those are tool calls the agent made.task_notificationdirectly to the agent (not buffered for the UI), and the agent decides what to do with it.MonitorRegistrylackedsetStatusChangeCallbackentirely before this PR.BackgroundShellRegistryonly hadsetRegisterCallback. Same situation here —MonitorRegistryonly had register + notification callbacks. So the core change is real work, not "thread an existing callback through."中文版
Phase C 后续任务(Issue #3634 跟踪)— 把 #3684 引入的 Monitor 工具接入组合 Background tasks pill + dialog,让第三种 task kind 在 UI 上真正可见 / 可控(之前只能通过
task_stop控制)。结构上跟 #3720 类似(同样把 shell 接进来),但有几处非平凡差异,下面单独列。落地后,#3488 引入、#3720 通用化的 kind 框架就有了三个真实消费者(agent / shell / monitor)— 这是把"通用框架"从口号变成实证的关键节点。
体量:+357 / -40,8 个文件,2 个 commit。
Commit 列表:
67424f0— 主接入(coreMonitorRegistry加 callback + CLI hook / pill / dialog / context 改动)。7999b85— lint 修复:ESLint 的default-case规则要求每个 switch 都有 default 分支,即使 TS 已经能证明 union 穷尽。新加的 5 个 switch 全部补上default: { const _exhaustive: never = entry; throw ... }— 这个模式同时保留运行时守卫和编译期穷尽检查(如果以后加第 4 种 kind,TS 会立刻指出哪 5 处要改;没有: never的话未处理的 kind 会静默落到 throw)。改动覆盖:
MonitorRegistry):加setStatusChangeCallback,跟另外两个 registry 对齐。register()同步触发(订阅者能看到生命周期起点),settle()同步触发(每次 running → terminal 都看到,包括emitEvent在 maxEvents 时的 auto-stop)。订阅者抛错被捕获 + 记日志,不污染 registry。useBackgroundTaskView同时订阅三个 registry,按 startTime 合并。DialogEntry联合加(MonitorEntry & { kind: 'monitor' }),entryId用 switch 覆盖三种 kind。Pill 按 shell → agent → monitor 顺序分组(monitor 放最后,最长寿、最不急 glance)。Dialog 加[monitor] <description>行 + 新MonitorDetailBody(命令 / pid / 事件数 / dropped 行数)。cancelSelected路由 monitor 走monitorRegistry.cancel()。跟 #3720(shell follow-up)的非平凡差异:
cancel()同步,shell 的是requestCancel()(异步性质)。 Shell 走requestCancel()只 abort,让 spawn settle 路径记录真实终态(沿用 feat(core): wire background shells into the task_stop tool #3687)。Monitor 的cancel()内联完成 abort + settle + endTime + (可选)terminal notification,无竞态窗口。cancelSelected直接调cancel()。这跟task_stop给 monitor 走的路径一致(feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 已落),model-facing 和 user-facing 入口行为统一。MonitorDetailBody故意不渲染 Progress block。 Agent 详情显示recentActivities(agent 调用的 tool)。Monitor 没有这个形态:事件流走task_notification直接给 agent(不为 UI 缓冲),agent 决定怎么处理。同样的内容显示两次(notification + UI log)既冗余又给 dialog 带来 per-event 重绘负担。要看实时事件流,现有的 notification UI 已经在显示了。MonitorRegistry之前完全没有setStatusChangeCallback。Shell 在 feat(cli): wire background shells into combined Background tasks dialog #3720 我也是新加的(BackgroundShellRegistry之前只有setRegisterCallback)。这里同理 —MonitorRegistry之前只有 register + notification callback。所以 core 改动是实质工作,不是"把已有回调串过来"。Test plan
vitest runcore/services/monitorRegistry.test.ts: 39 / 39 pass (33 existing + 6 new for the callback)vitest runcli/components/background-view/ + cli/hooks/useBackgroundTaskView*: 20 / 20 pass (8 dialog + 12 pill, +4 new pill cases for monitor)tsc --noEmit(cli): clean for this PR's surface (pre-existing unrelated errors elsewhere left untouched)7999b85)2 monitorswhile running, dialog (focus pill via↓thenEnter) lists monitor rows with[monitor]prefix, detail view shows status / pid / event count,xcancels, pill collapses to2 tasks doneonce both terminal.Related
task_stop↔ shell; feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 already wired the equivalent for monitor)