Validated path-like renderer inputs before calling path helpers so provider-supplied arrays or objects cannot crash TUI rendering before schema validation reports the bad tool call.
Added renderer regression coverage for read, write, and edit call/result components with array and object path arguments.
Fixes#4525
`#filterModels` in `packages/coding-agent/src/modes/components/model-selector.ts`
used to auto-switch to the ALL tab on any non-empty query. Typing a search from
a provider tab silently escaped the scope, so selecting the apparent match could
persist a same-named model from a different provider (e.g. a custom-proxy
`glm-5.2` while the user was on the openrouter tab wanting
`z-ai/glm-5.2`) under the default role.
Keep the search scoped to the active provider tab; empty results now name the
active tab and point at ALL as the explicit escape. Regression tests in
`test/model-selector-provider-search-scope.test.ts` cover the scoped search,
the still-global ALL-tab search, and the empty-state hint.
Fixes#4522
Once the assistant reply stops streaming, `vocalizer.clear()` was only invoked from the aborted-stream cascade in EventController. Escaping after the model finished fell through InputController to the empty-editor double-Esc gesture while StreamingAudioPlayer kept draining buffered Kokoro PCM.
Add `Vocalizer.isSpeaking()` (true while any live player, stream handle, or in-flight abort is around) and consult it in the Esc handler before the double-Esc branch: if speech is still audible, a single Esc calls `vocalizer.clear()` and resets `lastEscapeTime` so tree/branch stays reachable via the next press.
Fixes#4521
Rejected slash-only glob search paths before resolveToCwd can collapse them to the session cwd.
Added contract coverage for both / and // root inputs.
Fixes#4516
Reverted the defensive typeof guard; the assistant component contract guarantees the method, and test doubles now mock it. Keeping the production call strict avoids masking broken mocks or silently skipping persistence-key recovery.
- Mocked messagePersistenceKey in event-controller-error-banner.test.ts and safe-guarded it in event-controller.ts to prevent TypeError.
- Updated thinking loop retry test expectations to handle new dynamic recoveredErrors structure.
- Updated schema version assertions in auth-storage-email-dedupe.test.ts to v5, preserving v6 for future schema test.
- Simulated scrollback commitment in event-controller-message-start.test.ts by rendering container and committing rows before advancing timers.
Skipped binary inline-image payload bytes after the PDF ID operator so tokenizer scanning resumes at post-image content.
Advanced past stray closing delimiters while recovering malformed content streams to avoid zero-width tokenizer iterations.
Fixes#4512
- Added comprehensive unit tests for `TranscriptContainer` to verify uncommitted block tracking.
- Created integration tests ensuring `AssistantMessageComponent` correctly streams thinking and answer content into scrollback.
- Added tests verifying that expanded tool evaluation output records rows correctly without duplication after settling.
- Updated `AssistantMessageComponent` test suite to cover table streaming scenarios in the unsettled tail.
- Enforced strict history protection by gating ephemeral block removal on uncommitted state across controllers and UI components.
- Optimized settled-row calculations using explicit mermaid fence detection and improved scrollback integrity.
- Refactored transience management to target only actively streaming blocks, preventing redundant label rendering.
- Implemented persistent compaction for auto-retry errors and enabled consistent terminal title updates during session renaming.
- Moved terminal title update logic to a single listener onSessionNameChanged.
- Removed redundant setSessionTerminalTitle calls from ExtensionUiController, InputController, and InteractiveMode.
- Ensured consistent side-effect execution for terminal titles and editor accents across all session name change triggers.
- Refactored abort reason handling to rely on shouldRenderAbortReason instead of isSilentAbort.
- Updated documentation to clarify that both silent and user-interrupt aborts yield no label.
- Replaced commit-based stability checks with a unified `isTranscriptBlockFinalized` tracking mechanism.
- Removed deprecated provisional rendering configuration and flags across tool and renderer interfaces.
- Standardized native scrollback boundary logic to pin at the first unfinalized block using settled row verification.
- Updated and refactored test suites to validate block finalization and settled row boundaries instead of deprecated commit stability methods.
- Introduced an automated retry recovery system to track, manage, and persist recovered error states within agent sessions.
- Enabled compact transcript rendering for recovered auto-retry errors by removing heuristic commit machinery.
- Improved raw read tracking and provenance in the ReadTool to support refined file snapshot recording and hashline editing.
- Excluded recovered assistant messages from default model context and updated event controllers to handle retry recovery life cycles.
- Removed complex snapshot caching and volatile/stable state tracking logic.
- Replaced multi-zone audit logic with streamlined tail-sample checks.
- Simplified render boundaries by deriving a single final boundary from the live region.
- Eliminated redundant audit state management and auxiliary safe-end interfaces.
Codex review of #4502 flagged that a bare `.catch(() => undefined)`
neutralizes the unhandledRejection but leaves the affected `runInTab`
call blocked inside `runCmuxCode` until timeout when the in-flight
code does not make another cmux socket request (e.g. `await
wait(60_000)`). `releaseTab` was signaling the run only by rejecting
an orphaned promise.
Wire the tab-close event all the way into the cmux run body:
- `PendingRun` gains a `closeAc: AbortController` that `releaseTab`
aborts BEFORE calling `pending.reject`. `wait(...)` (via
`waitForBrowserRun` -> `untilAborted`), in-flight cmux socket calls
(via CmuxTab's `#request` -> `untilAborted`), and facade proxies
(via `bindBrowserRunFacade`) all consume the composed signal, so
the run body unwinds within a microtask instead of blocking to its
own timeout.
- `runInTabWithSnapshot`'s cmux branch composes `closeAc.signal` into
the run's signal (`AbortSignal.any([opts.signal, closeAc.signal])`)
and now publishes `runCmuxCode(...)`'s outcome to the shared
`promise` via `.then(resolve, reject)` and returns `await promise`.
Both branches thus await the same promise, so `pending.reject`
always has an attached handler (removing the original crash) AND
the caller sees `Tab "..." was closed` immediately instead of
waiting on the run's timeout.
- Drop the defensive `promise.catch(() => undefined)` — the promise
is now actively consumed on both backends.
The new regression test adds a second case that exercises the
reviewer's exact scenario (`await wait(60_000);`) and asserts:
1. `pending.closeAc.signal.aborted` flips from `false` to `true`
across `releaseTab`, with the tab-close error as its reason.
2. The awaited `runInTab(...)` rejects with `Tab "..." was closed`.
3. No `unhandledRejection` fires.
Verified locally by temporarily removing `closeAc.abort(...)` in
`releaseTab` — the new assertions fail; restoring it makes them pass.
Fixes#4499
The cmux branch of `runInTabWithSnapshot` awaits `runCmuxCode(...)`
directly and never awaits/`.catch`es the `Promise.withResolvers()`
promise it stashes on `tab.pending`. When `releaseTab` walks pending
runs and calls `pending.reject(new ToolError("Tab ... was closed"))`
(a sibling subagent's `browser close --all`, session-scoped reap, etc.),
that orphaned promise had zero handlers and Bun surfaced the rejection
as `unhandledRejection`, which the CLI's top-level handler treats as
fatal — killing every other tab and subagent sharing the process, not
just the affected run.
Attach a no-op `.catch(() => undefined)` to the promise immediately
after creation. Inert for the worker branch (which still awaits the
same promise via `raceWithTimeout`, and attaching a second handler is
safe) and neutralizes the orphan on the cmux branch.
Adds a regression test that drives real `acquireBrowser` /
`acquireTab` / `runInTab` / `releaseTab` against a mocked
`CmuxSocketClient`, races `releaseTab` against an in-flight cmux run,
and asserts no `unhandledRejection` fires.
Fixes#4499
- Normalized string-array modelRoles values at the settings accessor boundary so existing role resolution paths keep receiving comma-list strings.
- Added regression coverage for resolving pi/task from a YAML-equivalent modelRoles.task list.
Fixes#4492
- Made resolveShapeForText choose silver16-bw for CJK-heavy auto transcripts while preserving explicit variants and unsafe glyph protection.
- Added silver16-bw to the snapcompact shape settings submenu and renamed unsupported-glyph warnings.
- Covered auto shape selection, explicit variant precedence, unsafe glyph scans, and settings option parity.
Fixes#4486
A configured secret's friendlyName is baked verbatim into every
placeholder minted for it via an exact deobfuscateMap entry - not
through the alias-fallback path fixed earlier. If the sanitized label
happens to contain another configured secret's literal (or is matched
by a configured regex), that secret is smuggled into every use of the
unrelated placeholder and is never scanned, since the token is
recognized as already-generated on an exact match.
createPlaceholder() now drops the friendly-name prefix for a mint
whenever it collides with a configured plain secret or a configured
regex pattern. Regex entries are now compiled in a dedicated
constructor pass before any placeholder is minted, so the collision
check sees the full picture regardless of entries[] order.
Two follow-up fixes on the friendly-placeholder alias check:
- isGeneratedPlaceholder() now also rejects a forged prefix that
contains any regex-discovered secret's value (tracked in
#obfuscateMappings), not just statically configured plain secrets,
closing the same smuggling path for type: "regex" entries.
- The regex match collector's placeholder-prefix clamp branch now
keeps the full original match length for the short-match guard
instead of the clamped prefix's shorter length, so a full-size
match whose wholly-outside prefix is under MIN_OBFUSCATE_SECRET_LEN
is no longer wrongly skipped as noise.
Untrusted text could wrap a real secret's plaintext in a fabricated
friendly-name prefix around a hash suffix borrowed from any OTHER
already-obfuscated secret. obfuscate() treated the whole forged token
- including the exposed secret literal standing in for the prefix - as
already redacted, letting it reach the provider untouched.
isGeneratedPlaceholder() now refuses the friendly-name-independent
alias fallback whenever the dropped prefix contains a configured
secret's literal value, while still accepting a stale prefix left over
from a legitimate friendly-name rename (same secret, same key).
Skipped the workflow notice on raw selectors so bounded raw reads stay byte-for-byte, and taught resolveToolSearchScope to request pathOnly resolution so ast_grep/ast_edit scope-only calls no longer trip the inline-content cap on large artifacts.
Fixes#4482
When the error body already advertises OAuth endpoints, `/mcp add` and `/mcp reauth` use `authResult.oauth` directly and skip `discoverOAuthEndpoints`, so scopes advertised only in the RFC 9728 protected-resource metadata document never reach the grant.
Add exported `fetchResourceMetadataScopes(url, opts?)` that fetches the metadata doc and returns `scopes_supported` / `scopes` / `scope`. Hoist the shared `readMetadataScopes` reader out of `discoverOAuthEndpoints`. At all three call sites (wizard, `/mcp add`, `/mcp reauth`), when `oauth` is populated from the JSON body but `oauth.scopes` is empty and `authResult.resourceMetadataUrl` was advertised, fetch the metadata and merge scopes onto `oauth`.
Regression tests cover the resource-metadata fetch and its failure/empty-doc paths.
Refs #4467
Watched-file notifications sent from the LSP writethrough now use the same bounded operation signal as sync/save notifications, so a stalled server cannot hang the write after disk commit.
Extended the freshness regression to assert a bounded signal reaches watched-file notification sends.
Refs #4459
shortenPath() the artifact.path leaked into the read-tool 'Artifact storage' and 'Unbounded raw read blocked' notices so $HOME never appears verbatim; details.resolvedPath keeps the absolute path for tooling.
Fixes#4482
Bash URL expansion and search/grep only need sourcePath; they now request pathOnly resolution so large artifacts stay usable for search/copy workflows while unbounded content materialization stays blocked.
Fixes#4482
Resolved artifact:// reads to backing files before selector handling, streamed bounded reads, and blocked unbounded raw reads for large artifacts with recovery guidance.
Fixes#4482
Extended ONNX Runtime CUDA sidecar repair to PI_TINY_DEVICE=auto and gpu on Linux x64, matching the CUDA-capable diagnostics path.
Added focused regression coverage for both generic accelerated settings.
Refs #4475
Deferred ONNX Runtime CUDA sidecar repair failure into the runtime metadata so loadTransformersRuntime keeps loading and loadPipelineWithDeviceFallback still gets its CUDA→CPU retry when NuGet is offline or the ort install script is unavailable. The failure surfaces through the CUDA diagnostics helper instead of hard-erroring the tiny worker.
Refs #4475
Downloaded missing ONNX Runtime CUDA provider sidecars when the compiled tiny-model side runtime is used with PI_TINY_DEVICE=cuda.
Preserved actionable CUDA worker diagnostics in tiny-models text output and added focused regression coverage.
Fixes#4475
@DylanBohlender's follow-up caught that MCPAuthorizationLinkPrompt.render
still ignored `width` and emitted `Copy URL: <full URL>` as one composed
row. On any viewport narrower than the row (~272 columns for a
Linear-shaped authorize URL), TUI#prepareLine's
`truncateToWidth(..., Ellipsis.Omit)` silently clipped the trailing
`code_challenge_method=S256` — the exact #4418 fingerprint reappearing
inside the remote-safety fix. A remote user on a narrow terminal
copying the rendered line would lose the S256 method again; the local
shortcut below cannot help them (localhost isn't reachable), and the
OSC 52 clipboard staged full URL isn't visible in their local browser.
Component-level fix: honor `width` in render.
- New `wrapUrlRows(label, url, width)` helper.
- When `label + " " + url` fits in `width`, emit one inline row.
- Otherwise emit the label on its own row and slice the URL into
chunks of `width - indent`, each on its own row.
- Floors the effective width at 16 columns so degenerately narrow
terminals still emit every character; browsers strip whitespace
when a multi-row selection is pasted into the address bar, so the
reassembled URL is byte-identical.
- `render(width)` now uses the helper for both the primary `Copy URL:`
row and the additive `Local shortcut (this machine only):` row.
Regression tests in
`packages/coding-agent/test/modes/controllers/mcp-authorization-link.test.ts`:
- Wide viewport (1000 cols): inline `Copy URL: <url>` layout preserved.
- Narrow viewport (80 cols) + Linear-shaped URL: every row's visible
width ≤ 80, and the chunks reassemble byte-for-byte to the URL —
explicitly asserting the trailing `code_challenge_method=S256`
survives.
- Launch shortcut also wrapped at 80 cols; every row fits.
- Degenerate viewport (4 cols): URL still reconstructs exactly; the
16-col floor governs chunk width.
- Full URL remains the primary target even when a launch URL is
present, and the shortcut row is omitted when launchUrl is absent
or identical to the full URL.
- Added a Usage.orchestration sidecar for provider-side service tokens so Responses/Codex totals and costs stay accurate without inflating visible prompt input/cache buckets.
- Updated Codex/WebSocket usage, session/status aggregates, and usage reporting to preserve orchestration-aware totals.
- Added regressions for OpenAI Responses accounting, Codex WebSocket terminal usage, cost calculation, and session aggregation.
Fixes#4469
Scheduled a TUI repaint after extension-driven prompt mutations so pasteToEditor and setEditorText do not leave the editor visually stale until the next input event.
Fixes#4341