- Added the `xd://` virtual device protocol (`internal-urls/xd-protocol.ts`, `tools/xdev.ts`): tools declaring `loadMode: "discoverable"` are unmounted from the request tools array and driven via `read xd://` (list/docs+schema) and `write xd://<tool>` (execute), gated by the `tools.xdev` setting (default on) and inlined into the system prompt.
- Merged the `irc`, `job`, and `launch` tools into a single `hub` tool (`tools/hub/`, `async/job-manager.ts`): messaging keeps `send`/`inbox`/`list`, job control maps to `wait`/`cancel`/`jobs`, process supervision keeps `start`/`logs`/`stop`/`restart`/`describe` with `ps`, and the unified `wait` races background jobs against peer messages; SDK `IrcTool`/`JobTool`/`LaunchTool` are replaced by `HubTool`.
- Removed the hidden `resolve` tool in favor of the `xd://resolve`/`xd://reject`/`xd://propose` resolution devices, auto-including `write` whenever a deferrable tool or plan mode is present.
- Removed the BM25 tool-discovery system: the `search_tool_bm25` tool, the `tool-discovery` module, the `tools.discoveryMode`/`mcp.discoveryMode`/`mcp.discoveryDefaultServers`/`tools.essentialOverride` settings, per-tool MCP selection, and the `mcp_tool_selection` message type.
- Unified tool presentation on `ToolLoadMode` (`essential`|`discoverable`), replacing the custom-tool `xdev?: boolean` opt-out; custom, extension, MCP, RPC host, image-generation, and TTS tools now default to `discoverable`, and added a `satisfies` predicate to `SoftToolRequirement`.
- Removed the standalone `ssh` command tool and `ssh/ssh-executor` (the `ssh://` read/write/search protocol stays), and made `--tools` address hidden built-ins.
- Updated collab-web to render `xd://` dispatches and `hub` op families, dropped the `search_tool_bm25`/`ssh`/`report-finding` renderers, refreshed tool docs and prompts, and migrated the affected tests and changelogs.
- Introduced conditional scrollback clearing during UI renders when transcript compaction is enabled.
- Updated `CommandController` and `EventController` to respect the `display.collapseCompacted` setting.
- Configured `SelectorController` to trigger a chat rebuild and UI reset when the compaction setting changes.
- Updated `InteractiveMode` to dynamically toggle between collapsed and full inline history based on user settings.
- Stop propagating real-time updates for backgrounded Bash jobs to avoid UI flickering once a job enters the background.
- Refine background task tracking in `EventController` to distinguish between persistent background tasks and transient backgrounded Bash commands.
- Update UI rendering to display cleaner background job metadata in the footer instead of inline text notices.
AgentSession defers and coalesces the wire-level agent_end while a
prompt is in flight (#emitSessionEvent), so a multi-attempt retry saga
often surfaces only ONE agent_end to EventController — which can be
the final settle, not an intermediate attempt. Consuming #retryPending
against whichever agent_end arrived first (previous commit) could
therefore discard the real final failure notification.
Switch to gating purely on the retry lifecycle: #retryPending is set
by auto_retry_start and cleared only by auto_retry_end (both
outcomes), never consumed by sendErrorNotification itself. Those
lifecycle events are never deferred, so they reliably bracket the
window a retry is actually outstanding regardless of how agent_end
coalescing lands.
Close the residual gap this creates: #handleRetryableError's
classifier-refusal and Fireworks-fallback-ineligible branches could
short-circuit a saga that already announced auto_retry_start without
ever emitting auto_retry_end, latching #retryPending open forever.
Both branches now emit a final auto_retry_end(false) when a prior
attempt already started the saga. #handleAgentStart also clears
#retryPending defensively so a saga that still somehow never resolves
cannot suppress a later, unrelated turn's notification.
A retryable error's agent_end fires with the failed assistant message
(stopReason === 'error') the instant #handleRetryableError schedules a
retry (auto_retry_start), before the retry has a chance to recover.
sendErrorNotification read that transient agent_end the same as a real
final settle, so error.notify=on raised a 'Stopped with error' toast
even for turns that went on to succeed on retry.
Track a #retryPending flag set on auto_retry_start and consumed
(check-then-clear) by sendErrorNotification, so exactly one mid-retry
agent_end is suppressed per attempt. #handleAutoRetryEnd also clears it
directly on both success and failure so a recovered retry never leaves
it stuck; consuming it on read (rather than only via auto_retry_end)
keeps it self-healing for the classifier-refusal short-circuit path,
which can return from #handleRetryableError without ever emitting a
fresh auto_retry_end.
- Enabled granular task execution by allowing batches to interleave blocking items with non-blocking async background spawns.
- Updated task orchestration to support simultaneous inline result collection and persistent background job tracking.
- Improved agent visibility in the job tool by reporting running subagents even when not explicitly linked to a backing job ID.
- Enhanced terminal state handling to prevent premature tool block closures while async background operations remain active.
- Split mixed assistant text into per-tool segments instead of one post-tool tail.
- Insert each segment immediately after its preceding tool component across live and rebuilt transcripts.
- Extended the regression test to cover two tool calls with middle and final assistant text.
Fixes#4871
- Split mixed assistant messages so pre-tool text stays before tool panels while trailing text renders after the tool timeline.
- Applied the split to live streaming, transcript rebuilds, and file-backed transcript rendering.
- Added a focused EventController regression test for text/toolCall/text Cursor-shaped turns.
Fixes#4871
The token-usage row shown under assistant messages (display.showTokenUsage) now leads with the turn's local wall-clock time down to the second (YYYY-MM-DD HH:mm:ss), sourced from the assistant message's persisted timestamp so live, rebuilt, and restored transcripts all show the turn time rather than the view time.
createUsageRowBlock takes timestamp as an optional trailing argument, preserving its (usage, durationMs, ttftMs) public call contract on the package's ./modes/components/* export surface.
Reset the run-state title to idle when focusing an idle session (was inheriting the previous session's stuck spinner); drive the title to attention while a tool blocks on an approval prompt (not just ask), returning to working at its end; let an extension setTitle() own the terminal verbatim so neither the run-state prefix nor the spinner tick clobbers it, cleared when the app sets an authoritative session title; use NodeJS.Timeout for the spinner timer field.
The terminal title (OSC 0) now carries a run-state prefix: an animated spinner while the agent is working and a steady dot when idle, so a backgrounded tab/pane shows which session is busy vs done. `setTerminalTitleState` also exposes an `attention` ([!]) state for callers; rendering is gated by `tui.titleState` (default on), dedups writes, and is TTY-guarded.
Refs can1357/oh-my-pi#3587
Stopped new-session and session-switch UI paths from detaching active loader/render components without running their disposal hooks.
Added container and loader coverage for disposing children before destructive transcript/status replacement.
Fixes#4686
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.
Replace the visible-anchor suppression with a billed-usage predicate so live and resume paths agree on rendering the badge whenever the turn actually consumed tokens. Only genuinely free turns (no input, output, cache, or premium requests) drop the row, so hidden automated turns keep cost transparency.
Fixes#4532
Suppress token-usage rows for assistant turns that have no visible text, tool call, or terminal error anchor. Share the same decision across live rendering and transcript rebuilds so resume matches live output.
Fixes#4532
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.
- 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.
- 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.
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
- Added an enhanced speech pipeline that utilizes small models to rewrite text for natural language synthesis.
- Implemented `SpeechEnhancer` and `BlockAccumulator` to manage fence-aware text streaming and paragraph splitting.
- Configured a new `speech.enhanced` setting to toggle between mechanical and enhanced vocalization modes.
- Resolved `EPIPE` rejections during speech playback by ensuring stream flushes and suppressing stop-related errors.
- 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.
EventController.handleEvent rebuilt the editor's status-line top border
synchronously on every session event via updateEditorTopBorder(). During
a long-running eval that fires 5-10 events/s, each rebuild ran
StatusLine.getTopBorder → #buildSegmentContext → getCachedContextBreakdown
→ session.getContextUsage → estimateTokens (with JSON.stringify per
toolCall block) — the render pipeline is throttled to ~30 fps, so most
rebuilds were dropped before painting. Combined with a scheduler that
collapsed cadenceDelay to zero whenever a frame overran the 33ms budget,
the TUI busy-looped at ~40-50% CPU.
Fix:
- Editor gains setTopBorderProvider(): a lazy builder invoked once per
editor render. InteractiveMode installs it in the constructor and on
setEditorComponent, so the rebuild coalesces to the render tempo
regardless of event rate.
- Delete updateEditorTopBorder wrapper (now equivalent to
ui.requestRender) and inline every call site.
- Add adaptive render backpressure: a frame that exceeds
MIN_RENDER_INTERVAL_MS inflates the next scheduling delay to
2 * last_frame_cost, capped at 200 ms, targeting a 50% render duty
cycle instead of pinning the CPU at t=0.
New regression tests:
- editor-top-border-provider.test.ts: provider fires exactly once per
render, wins over eager setTopBorder, falls back when cleared, gets
the correct availableWidth.
- adaptive-render-backpressure.test.ts: cheap frames keep the 33 ms
cadence, a slow frame idles proportionally, pathological frames are
capped at 200 ms.
Verified with bun test packages/tui/test (all 246 relevant tests pass)
and bun test packages/coding-agent/test/modes (455 tests pass). Three
pre-existing agent-session-handoff snapcompact failures on main are
unrelated (snapcompactSupportedChars binding).
Fixes#4145
Registered `edit.input` and `eval.code` alongside `write.content` so the reveal controller decodes those top-level string arguments incrementally between throttled full-JSON parses.
Added a regression test covering multi-key extraction so the wire-up survives future additions.
Refs #4043
Added incremental decoding for streamed write content so preview args update below the full JSON parse throttle.
Added a regression test covering sub-throttle content growth in ToolArgsRevealController.
Fixes#4043
- Improved the leaked-thinking stream projector to clone and sync native tool-call blocks directly.
- Eliminated the need for placeholder IDs and complex rekeying logic in the event controller and argument reveal module.
- Simplified native tool-call validation in owned-stream processing by requiring only a non-empty name.
- Added comprehensive unit tests to ensure tool-call IDs and partial JSON parameters remain intact during healing.
- Added `#streamTurnNonce` to prevent aborted streaming turns from corrupting content indexes of subsequent messages.
- Implemented temporary stream-key generation using content position and turn nonces for previewing tool calls without native IDs.
- Added migration logic to key pending tool previews by their real ID and rekey `ToolArgsRevealController` once the real ID is parsed.
- Anchors the incomplete-todo reminder block inside the scrollback transcript instead of a floating live container.
- Eliminates duplicate reminder copies piling up in terminal scrollback during terminal reflows.
- Removes the dedicated `todoReminderContainer` and simplifies state synchronization on todo reload.
- Updates tests to verify sequential reminders commit as separate blocks and are left intact when tools succeed.
- Skipped processing tool calls in the event controller streaming message when the tool ID is missing.
- Prevented creating orphaned empty placeholder cards caused by empty IDs during early Anthropic and OpenAI tool block streaming.