computeNonMessageTokens / computeNonMessageBreakdown re-tokenize the system
prompt and every tool's wire schema (per-tool JSON.stringify) on each call,
but the per-turn compaction and context-threshold paths call them several
times (getContextBreakdown twice, #estimateStoredContextTokens once) over
inputs that change at most once per turn. Memoize on the identity of
(systemPrompt, tools, skills) -- the same stable refs the StatusLineComponent
cache already trusts -- so the expensive parts run at most once per input
change instead of per call.
The real agent_end event always carries messages: AgentMessage[], but
this fixture used a bare { type: "agent_end" }. That was harmless
while sendCompletionNotification() ignored its event argument, but the
prior commit made it read event.messages, so this fixture now crashes
#finishAgentEnd with a TypeError. Add the missing field.
sendErrorNotification now reads the settled turn from event.messages,
but sendCompletionNotification still read viewSession.getLastAssistantMessage().
For a classifier-refusal turn that stale/undefined lookup no longer
matched 'aborted'/'error', so with completion.notify=on the same
failed turn fired both the error toast and a misleading 'Complete'
toast.
Thread the same agent_end event into sendCompletionNotification so
both gates read one consistent source of truth.
Classifier-refusal failures end a turn with stopReason === "error" but
get pruned from the active context (agent-session.ts's
#removeAssistantMessageFromActiveContext) before agent_end fires.
sendErrorNotification() read viewSession.getLastAssistantMessage(),
which reflects that mutated context and silently missed the
notification for exactly the turns it should fire on.
Thread the agent_end event through #handleAgentEnd -> #finishAgentEnd
so sendErrorNotification reads the turn's own outcome from
agent_end.messages instead.
Resolves the two Codex P2s raised on #4420 that merged unaddressed:
- wrapUrlRows indented every continuation chunk. A multi-row terminal
selection includes the newline plus that indent; address bars strip
newlines but preserve or percent-encode embedded spaces, so the
reassembled URL was corrupted at every chunk boundary - silently,
when the damage landed inside a query value. Chunk rows now carry
zero leading bytes (label rows keep their indent), and the test
reassembly helper concatenates chunks raw instead of stripping the
indent that previously masked exactly this defect.
- #launchUrlIfSafe advertised a localhost /launch copy target for
flows whose redirectUri never returns to the loopback server. Its
catch-comment assumed custom-scheme URIs are non-parseable, but
new URL('vscode://gitlab.gitlab-workflow/authentication') parses
fine and sailed through the pathname check. The guard now requires
an http(s) loopback redirectUri (localhost / 127.0.0.1 / [::1]);
custom schemes, non-loopback hosts, and unparseable URIs all
suppress the launch URL. Regression tests cover the GitLab Duo
vscode:// shape and a fixed non-loopback HTTPS redirect.
Refs #4418
- Updated event controller fixture to include requestComponentRender mock.
- Added test case for resolving deferred role-alias model patterns.
- Added test case for parsing and falling back comma-delimited model patterns.
macOS laptops have no dedicated Forward Delete key. Fn+Backspace is the
only way to send \e[3~, and many macOS terminals (Terminal.app, some
iTerm2 profiles) deliver \x7f for that combo instead — so the keystroke
landed in the search box, not the delete handler, making session deletion
unreachable for those users.
Add a Backspace-on-empty-search handler alongside the existing Delete
check. With a typed query, Backspace stays bound to the search Input so
users can still edit their filter text. The existing confirmation dialog
guards against accidents.
Footer hint updated: [Del delete] -> [Del/⌫ delete].
- Exposed retry fallback chains in the model settings panel.
- Added a /model action that assigns the selected model as the default retry fallback.
- Cleared retry cooldown suppression when users manually switch models.
Fixes#4533
- 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.
- 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.
- 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.
- 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
@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.
Codex review flagged that advertising `launchUrl`
(http://localhost:<omp-port>/launch) as the visible `Copy URL:` breaks
SSH/WSL/headless users: their local browser resolves the URL against
the local machine (no OMP listening) and fails before ever hitting the
provider. On terminals without OSC 8 support, they lose the manual
`/login <redirect>` path entirely.
Every OAuth-facing surface now shows the full authorization URL as the
primary copy target and offers `launchUrl` as an additional "Local
shortcut (this machine only)" line for wide-terminal local users who
want the truncation-safe convenience:
- MCPAuthorizationLinkPrompt renders `Copy URL:` with the full URL and
appends the local-shortcut row only when `launchUrl` differs. OSC 52
clipboard staging in the MCP onAuth handler switches to the full URL
(OSC 52 is a wire-level protocol — the terminal writes to the
caller's LOCAL clipboard even when OMP is on a remote SSH box).
- LoginDialogComponent.showAuth, selector-controller onAuth,
setup-wizard sign-in, and the auth-broker CLI mirror the pattern:
full URL first, launchUrl as an optional local shortcut.
- Setup wizard uses `wrapTextWithAnsi`, not truncation, so the RFC
7636 §4.3 downgrade bug that motivated launchUrl is unreachable
through it; still surfaces launchUrl for wide-terminal convenience.
Regression tests in
`packages/coding-agent/test/modes/controllers/mcp-authorization-link.test.ts`
now assert:
- Full URL is the primary `Copy URL:` line so SSH sessions can complete.
- launchUrl still appears beneath as `Local shortcut (this machine only): …`
when it differs from the full URL.
- No shortcut row when launchUrl is absent OR equals the full URL.
Two independent defects broke /mcp reauth against S256-only providers on
Windows boxes whose PATH no longer references System32:
1. openPath spawned bare rundll32 and swallowed the
`Executable not found in $PATH` throw with a bare `catch {}`, so the MCP
controller's outer try/catch was dead and the transcript unconditionally
claimed "Opening browser automatically...".
2. TUI#prepareLine silently truncates any composed row wider than the
viewport. MCPAuthorizationLinkPrompt rendered `Copy URL: <full URL>` as a
single ~271-column line whose trailing parameter is
code_challenge_method=S256. On the reporter's 270-col terminal the cut
landed inside that parameter, dropping the method while keeping
code_challenge — which RFC 7636 §4.3 treats as plain PKCE, which Linear
correctly rejects with "The plain PKCE method is not allowed. Use S256
instead."
OAuthCallbackFlow now hosts a `GET /launch` route on the same loopback
callback server it already runs; the route 302-redirects to the pending
authorization URL and is advertised as `OAuthAuthInfo.launchUrl` — a
~30-char copy target no viewport can meaningfully truncate. The MCP OAuth
fallback, /login, setup wizard, auth-broker CLI, and login-dialog all
prefer the launch URL for the visible copy target, keep the full URL in
the OSC 8 hyperlink for click-through, and the MCP flow additionally
stages the copy target on the clipboard via OSC 52 (same pattern the
setup wizard uses).
openPath now resolves rundll32.exe through %SystemRoot%\System32 (with a
C:\Windows fallback when SystemRoot is unset) and logs both synchronous
spawn throws and non-zero exits via the shared logger, so silent
misconfigurations show up in ~/.omp/logs/omp.*.log. The dead try/catch
around openPath in the MCP controller is removed.
Fixes#4418
Timer-driven reveal and spinner ticks (streaming reveal, tool-args reveal,
tool-execution spinner, todo strike animation) now hand the changed
component to `TUI.requestComponentRender(component)` instead of forcing a
full-tree render at 30fps. Every other root subtree reuses its previous
frame rows, cutting the Box/Container tree walk out of the compose
pipeline while the transcript grows.
Shimmer:
- Intern the working-message palette per accent (WeakMap-keyed) so the
Symbol-slot compiled-ANSI cache in `shimmerSegments.compile` actually
hits between frames — the fresh palette literal in `renderWorkingMessage`
guaranteed a per-tick miss.
- Add an `activeBand` fast-path: outside the sweep window the intensity
is guaranteed zero, so those code points coalesce into a single low-tier
run without running `intensityFn` or `tierFor`. On the typical ~60-char
working message the classic band is 12 cells wide, so ~80% of the per-char
loop disappears.
Widen `ToolExecutionHandle` to extend `Component` (matches every
concrete impl — `ToolExecutionComponent`, `ReadToolGroupComponent` —
which already extend `Container`) so the reveal controller callback
sites are type-checked.
Fixes#4377
Four tightly-scoped hot-path fixes covering the highest-impact items in the
reporter's CPU profile (13.1 s profiled / 30 s window):
1. `event-controller.ts:handleEvent` no longer fires a blanket
`statusLine.invalidate() + ui.requestRender()` before every session event.
The pre-render was a leftover from #4145 when `updateEditorTopBorder()`
still eagerly rebuilt the border; the lazy provider added in #4145 made
it redundant. It fired on every `message_update`/`tool_execution_update`
during streaming — the pre-render's frame ran while the handler was
awaiting, then the handler's own `requestRender` scheduled a second
identical frame. Every handler that mutates visible state already calls
`requestRender()`.
2. `shimmer.ts:shimmerSegments` iterates the segment string in place instead
of building a code-point array with `Array.from(seg.text)` every animation
frame. Runs of same-tier chars are emitted via a single `slice` per run
rather than accumulating into `runBuf`. Surrogate pairs stay atomic — the
code-point index still advances by 1 per emoji. Microbench over 30k
frames: 45 ms → 18 ms (2.53x), allocation rate down from ~N-per-frame to
a handful per frame. New tests cover mixed BMP+surrogate and all-emoji
inputs. `Array.from` was the #1 self-time hotspot in the reporter's
profile at 10.2%.
3. `Markdown.setText` gains an equality guard mirroring `Text.setText`
(returns `false` when `text === #text`). Providers re-emit identical text
on ticks with no delta (throttled frames, reconciled tool-execution
updates); each of those now short-circuits instead of dropping
`#cachedLines` and forcing a full lex + wrap on the accumulated paragraph
(the reporter's #3 hotspot at 8.4%). New test asserts render-reference
stability + return-value semantics.
4. `SPINNER_RENDER_INTERVAL_MS` aligned with `SPINNER_GLYPH_ADVANCE_MS`
(both 80 ms). The previous 33 ms cadence emitted ~2.4 paints per glyph
step; the differential-output dedup only skips the write, not the
compose walk. Visually identical (glyph advance was already 12.5fps),
halves paints during tool execution.
Skipped (out of scope for a bug fix, deserve dedicated PRs):
- Freezing streaming prefix on single `\n` boundaries — correctness-bound
to `\n\n` block separators (CommonMark loose-list continuation).
- Compose-phase idle gate + adaptive-backpressure moving-average — need a
component-level dirty flag; the 200 ms cap in `#scheduleRender` was set
for a reason (#4145 tail-latency guard).
Tests updated: two IRC-expiry tests in event-controller-message-start.test.ts
that were asserting the pre-render's second `requestRender` call now expect
one.
Fixes#4353
The github renderer materializes plain Text per display rebuild, so its
animatedPendingPreview opt-in requested 30fps repaints with a frozen
glyph for the whole run_watch wait; drop the flag. Custom tools with
only one of renderCall/renderResult never route to the animated generic
fallback, so gate the unregistered-renderer spinner on both being
absent. Regression tests for both no-tick contracts.
- Extracted decodeStreamedToolArgs into tool-args-reveal.ts and used it from both the live event path and transcript rebuilds, so mid-write theme/settings/focus replays no longer show stale streamed write/edit/eval content.
- Fixed the smoothing-off live path returning stale provider-parsed args.
- Documented the mandatory shared decode in the AGENTS.md streaming-preview hazard note; added changelog entries for this batch.
Ports only the thinking double-format fix: resolveThinkingDisplay reuses block.thinking when rawThinking is set (buildDisplayMessage already formatted it), plus a single-entry memo in formatThinkingForDisplay and a rawThinking regression test. The PR's incremental reveal slicing is superseded by the already-merged #3848 (memoized grapheme slicing).
Added renderer metadata for pending and partial result paths that visibly consume spinner frames.
Stopped headerless bash pending previews from scheduling repaint ticks while preserving eval and shell header animation.
Excluded running async result snapshots from live partial spinner intervals so finalized background tool rows do not repaint scrollback.
Added regression coverage for async bash snapshots staying static.
Started live partial tool spinner intervals for non-static tool blocks and forwarded spinner frames through eval and shell-style renderers.
Added regression coverage for live eval and shell preview spinner frames.
Fixes#4170