- Implemented clipboard register management, parsing, and execution rules for CUT, COPY, and PASTE operations in the hashline engine.
- Added session-persistent clipboard state and integration across agent session execution, diff previews, and streaming tools.
- Added comprehensive validation, error messages, recovery handling, and test coverage for clipboard and block operations.
handleEvent has no blanket pre-render (removed for issue #4353), so
each handler must explicitly schedule one when it changes something
visible -- every other statusLine.invalidate() call site in this file
pairs it with ui.requestRender(). The new model_changed handler only
invalidated the cache, so an internal model switch (prewalk hand-off,
retry-fallback) while the TUI was otherwise idle left the status line
showing the stale model until an unrelated event happened to render.
Flagged by @chatgpt-codex-connector on PR #6908.
(cherry picked from commit f565e3a4956bda467b6679a3c3f1e48379395a78)
AgentSession#setModelWithProviderSessionReset is the single choke point
every model mutation runs through (explicit /model, prewalk hand-offs,
retry-fallback, model cycling). It previously changed agent.state.model
silently — no session event told subscribers (ACP, RPC, TUI) that the
active model moved.
Emit a new model_changed AgentSessionEvent from that choke point
whenever the model actually changes, and wire it into every consumer
that must exhaustively handle AgentSessionEvent: the TUI event
controller (invalidates the status line, same as thinking_level_changed)
and the RPC client's forwarded-event allowlist.
(cherry picked from commit f76325de2c7821dd7046ddb67546577c3575a263)
When Cursor packs toolCallStarted and toolCallCompleted into one HTTP/2
chunk, the bridge tool_execution_end (synchronous callback, fired
mid-parse) reaches the interactive controller before the streamed
toolcall_start (queued on AssistantMessageEventStream, delivered a
microtask later). The controller found no pendingTools entry, dropped
the completion, and the card created afterwards animated forever.
Two halves, each necessary:
- Hold an early todo completion in #orphanedToolCompletions and replay
it when the streamed block creates its component.
- Guard card creation from cumulative message_update frames with the
turn-scoped #toolTimelineComponents map. Without this, the update
after the replay re-lists the same toolCall block, finds pendingTools
empty again, and spawns a second, permanently pending card. This is
also why emitting a synthetic tool_execution_start from the bridge
(previous attempt, reverted) could not work.
Both maps are cleared together at the existing transcript-anchor reset
sites. The normal ordering (start first) is covered by a control test.
Two review findings on the native todo sync.
- TodoItem.dependencies is a graph the local model cannot store: rows
are keyed by content, carry no id, and hold no edges. An imported
dependent row files as plain pending and nextActionableTask then
offers work the server considers blocked. Refuse snapshots with an
edge pointing at an unfinished row; edges whose blockers already
finished constrain nothing and still mirror.
- The todo failure warning interpolated the provider error verbatim.
Collapse and truncate it at the render boundary.
Also documents two known, unfixed defects: an async cursorOnToolResult
transformer resolving after the buffer drain, and the todo card
lifecycle race. Emitting a synthetic tool_execution_start for the
latter was measured and rejected -- the completion deletes the entry it
creates, so the late streamed block adds a second card.
Moved vocalizer interruption from agent_start to user message_start so agentLoopContinue preserves queued speech.
Expanded regression coverage for continuation starts and user prompt boundaries.
Fixes#6375
Moved vocalizer clearing from each assistant/tool continuation boundary to the start of a new agent run.
Added regression coverage for uninterrupted internal turns and new-run interruption.
Fixes#6375
Kept the bare 'π' brand per owner direction: the separator between the
brand and the session label now carries the state — '>' when it's the
user's turn (idle), animated spinner frames while working, '!' when the
agent is blocked on the user. Disabled ('tui.titleState' off) renders the
pre-state 'π: label' layout. Updated the state/runtime tests to the new
contract.
Preserved settled user and assistant component instances during compaction-only transcript rebuilds so warmed Markdown and layout caches remain valid.
Covered both manual and automatic compaction paths with focused regression tests.
Fixes#6033
- 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.