- 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.
- 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.
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
@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
MCP servers such as JIT gateways advertise required scopes via the RFC 6750 `WWW-Authenticate` challenge (`scope="..."`) and via RFC 9728 protected-resource metadata (`scopes_supported` / `scopes` / `scope`), then reject follow-up requests with `insufficient_scope` when a bearer token was issued without them. OMP's discovery only picked up `scopes_supported` from the auth-server metadata document, so `/mcp reauth`, `/mcp add`, and the MCP add wizard silently minted scope-less tokens.
- Extract `scope`/`scopes` from the WWW-Authenticate challenge into a new `AuthDetectionResult.scopes` field via `extractOAuthChallengeScopes`.
- Thread a `protectedScopes` option through `discoverOAuthEndpoints` and its recursion; capture `scopes_supported`/`scopes`/`scope` off resource-metadata documents; use those scopes when the auth-server metadata omits them.
- Pass `authResult.scopes` from `analyzeAuthError` into every discovery call site (`/mcp reauth`, `/mcp add`, MCP add wizard).
- Add regression tests for insufficient_scope + resource_metadata, resource-metadata `scopes_supported` passthrough, and challenge-scope threading.
Fixes#4467
Under jj the git branch label is unhelpful - a colocated repo parks git
HEAD detached, and a secondary jj workspace has no git at all. The `git`
segment now overlays the jj working-copy label (bookmarks + short
change-id) in both cases, via a read-only `jj log --ignore-working-copy`
query that is throttled and cached (and force-refreshed on a git-HEAD
change). Plain git repos and non-jj dirs are unchanged. Extracted a
testable `jj-info` module (root detection, query, format).
Refs can1357/oh-my-pi#3582
A `blocked` status (plus `block`/`unblock` ops and an optional blocker
note) for tasks that are open but waiting on something the agent can't act
on — a user decision, another agent, or an external action. Blocked tasks
stay in the tracker but are excluded from the stop-time incomplete-todo
reminder (via the existing pending/in_progress allowlist) and from
auto-promotion. Syncs the mode-layer TodoStatus, the ACP status map/guard,
the markdown markers, and the renderers.
Refs can1357/oh-my-pi#3581
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.
Codex review flagged two P2s the reporter (@DylanBohlender) confirmed:
1. OAuthCallbackFlow#handleCallback checked LAUNCH_PATH BEFORE the
`pathname !== this.callbackPath` guard, so an OMP config that pinned
the provider callback at `/launch` (via `oauth.callbackPath` or a
matching `oauth.redirectUri`) had the launch route eat its
`/launch?code=...&state=...` redirect and 302 it back to the
authorization URL instead of resolving the callback. Reorder so
`callbackPath` resolution wins the collision, and suppress `launchUrl`
in that case (also when `redirectUri`'s pathname resolves to `/launch`
even without an explicit `callbackPath` override) so UIs never
advertise a self-redirecting copy target.
2. RpcClient.login's `open_url` listener called
`onOpenUrl(req.url, req.instructions)` and dropped `launchUrl`, so SDK
hosts built on the public helper couldn't surface the truncation-safe
copy target. Extend the callback signature to
`(url, instructions?, launchUrl?)` and forward the field — backward
compatible for existing 1-2 arg consumers.
Regression tests in packages/ai/test/callback-server-launch-route.test.ts:
- callbackPath = /launch: launchUrl is undefined AND a
`/launch?code=...&state=...` request resolves via the callback template
(200/HTML), never 302s to the authorize URL.
- redirectUri pathname = /launch (callbackPath default): launchUrl still
suppressed defensively by the parsed-pathname guard so the base class
never advertises a colliding launch route even when callers skip the
MCPOAuthFlow path-derivation.
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
- Omit Next from the guest multi-select ui-request options until at least
one option is checked or a custom answer exists, mirroring the local
dialog's disabled-Next gating. The remote select has no disabled-row
concept, so Next is omitted rather than dimmed (PRRT_kwDOQxs0bc6OFbDW).
- Add the bottomBorder(1) term to the ask dialog's fixed-row budget so the
rendered dialog no longer overflows the viewport by one row
(PRRT_kwDOQxs0bc6OFbDY).
- Add focused tests: guest wire-level Next gating round trip, and dialog
height <= viewport assertion.
- Replace #requestGuestUiString's string|"unavailable"|undefined channel
with a tagged GuestUiResult ({answered}|{cancelled}|{unavailable}) so a
guest answer literally equal to "unavailable" no longer collides with
the transport-unavailable sentinel (PRRT_kwDOQxs0bc6OE3gN).
- Cap in-body question header rendering to MAX_HEADER_ROWS with ellipsis
truncation so long/multiline questions cannot push options off-screen
(PRRT_kwDOQxs0bc6OE3gS).
- Guest Other editor cancellation now continues the loop (multi) / re-shows
the select (single) instead of cancelling the whole ask
(PRRT_kwDOQxs0bc6OE3gU).
- Disable the Next row on a single-question multi-select until at least one
option or custom input is chosen, preventing empty-result submission
(PRRT_kwDOQxs0bc6OE3gY).
Op: correct
Restores: review:4375
- Widen ExtensionAskDialogResult to a union with { kind: "chat" } variant
so AskTool can distinguish chat handoff from cancel (undefined).
- AskDialogComponent.#finishChat passes { kind: "chat" } via onChat.
- Controller settles { kind: "chat" } locally and propagates a "chat"
sentinel through the guest/collab path instead of returning undefined.
- AskTool returns a chat-redirect AgentToolResult (chatRedirect details)
instead of aborting with ToolAbortError.
- #promptForNote prefills with the existing note only when editing the
same row (noteRowKey === rowItem.key), preventing cross-row note leaks.
- Add ask-dialog and ask tool tests for chat redirect and row-specific
note prefill.
- Reset the inactivity countdown in handleInput after the closed/prompt
guard, matching HookSelector/HookInput semantics so a user actively
navigating options/tabs is not auto-submitted by an absolute deadline.
- Add boundPromptTitle helper that flattens whitespace, wraps to the
terminal content width, and caps at 3 rows with ellipsis truncation;
apply it to custom-input and note prompt titles in the rich dialog
and the guest-UI editor path so long/multi-line questions stay usable.
- Drop totalRows from both ScrollView calls so the full allLines array
is sliced via scrollOffset + row; previously totalRows caused render
to read lines[row] instead of lines[scrollOffset + row], leaving the
viewport stuck at the top while the scrollbar thumb moved.
- Add tests for inactivity reset, bounded prompt titles, and scrolling.
Restores: review:4375
Op: correct
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
Plan-approval's 'Approve and compact context' used to pass the rendered
plan-mode-compact-instructions prompt as the first positional argument
to handleCompactCommand -> session.compact(), which landed on the
session_before_compact extension hook as customInstructions. Extensions
treating that field as user focus (e.g. to bias a query-focused summary)
would then see plan-mode boilerplate instead of operator intent and
produce query-biased compactions.
Add CompactOptions.internalGuidance: a private summarizer-only channel.
session.compact() reads it into the fallback-model summarizer while the
session_before_compact hook payload still only carries the public
customInstructions arg (undefined for the plan-compact path). The
snapcompact-disable predicate and the /compact rejectsFocus guard cover
both fields so a directed summary is never silently downgraded.
Extend the interactive-mode handleCompactCommand facade + command
controller with a fourth internalGuidance parameter, and switch the
plan-approval callsite in interactive-mode.ts to route the plan prompt
through it.
Fixes#4359
Selecting "Approve and compact context" while a user turn was typed during
compaction surfaced `Failed to finalize approved plan: Agent is already
processing` and silently discarded the operator's queued turn.
`flushCompactionQueue` fires the queued user turn (fire-and-forget) before
`handleCompactCommand` returns, so by the time `#approvePlan` resumed, the
session was streaming. The previous shape aborted the queued turn and still
raced into `AgentBusyError` when `session.prompt()` ran before abort settled.
The finalize path now queues the plan-approved directive as a synthetic
follow-up when the session is streaming, and catches a racing `AgentBusyError`
from `prompt()` with the same fallback. `AgentSession.followUp()` gained a
`{ synthetic, expandPromptTemplates, attribution }` option so the hidden
execution directive lands as an agent-attributed developer message on the
follow-up queue, without flipping advisor auto-resume the user-follow-up path
does.
Fixes#4358
Adds the rich TUI ask dialog, additive schema fields, ask.enabled tool gating, note/preview/header support, chat redirect, and timeout behavior that defers while nested prompts are active instead of discarding user input.
Op: extend
- Prevent premature failure during process startup by waiting for stderr to drain before throwing exit errors.
- Resolve race conditions between stdout closure, process exit, and readiness timeouts by using a local child process reference.
- Ensure proper cleanup of abandoned processes during startup failures to prevent leaks.
- Introduced a dedicated advisor rail symbol to differentiate note cards from thinking output.
- Applied bold custom header tags and severity-tinted rails to distinguish advisor notes from surrounding text.
- Shifted note body text to the default content color to prevent blending with dimmed thinking-output styles.
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
Merged PR #4330 and fixed the remaining offered-boundary regression by stopping offer promotion at intervening live blocks.
Verified with targeted transcript/native scrollback regressions: 40 pass.
- Added `normalizeSingleStringField` to dynamically map misplaced string inputs to required schema fields for single-argument tools.
- Integrated argument normalization into `validateToolArguments` to handle model-specific variations in JSON payloads during validation passes.
- Updated `coding-agent` streaming and rendering components to recognize `_input` as a legacy alias for `input` across various UI paths and logic flows.
- Refactored `hashlineEditParamsSchema` to strictly enforce the `input` field while maintaining support for legacy aliases via runtime coercion rather than schema definition.
- Corrected unit tests to reflect that `_input` is rejected by the strict schema but handled gracefully by the validation layer.
Normalized extension custom-message payloads before session state or persistence, including bare string sendMessage shorthands. Skipped legacy bare custom_message entries during context rebuilds and dropped malformed custom/hook messages before LLM conversion. Added regression coverage for the poisoned-session resume crash.\n\nFixes #4345
Separated audited offerable transcript rows from durable snapshot rows so lower finalized content below a live block can be repaired instead of duplicated when the live block grows.
Added transcript and virtual-terminal regressions for the lower finalized tail case.
Fixes#4326
- Tracked placeholder/partial-result paints via render() override so an update landing before the shape reaches the terminal skips resetDisplay().\n- Added negative-case unit tests proving no reset fires when the intermediate shape was never painted.\n\nFixes #4314
- Added renderer hooks for first-result placeholder replacement and partial-result settle repaints.\n- Enabled the hooks for SSH and covered the streamed-placeholder and settle seams.\n\nFixes #4314
The status-line renderer's #lookupPr method called `gh pr view` through
Bun's raw `$` shell with no signal, no timeout, and no non-interactive
environment. A stalled `gh` process (keychain prompt, network hang, auth
deadlock) wedged the await forever; because #prLookupInFlight was set
before the call and never reset, subsequent renders skipped the lookup
and the child leaked indefinitely.
Route the lookup through the existing `git.github.run` helper with
`AbortSignal.timeout(git.GIT_COMMAND_TIMEOUT_MS)` so the child inherits
GH_NON_INTERACTIVE_ENV (disabling terminal and keychain prompts) and
receives SIGTERM on the standard 5-minute deadline. Non-zero exit still
falls through to the null cache, preserving the failure-tolerant
behavior.
Fixes#4234