The new global smart-paste listener reused handleImagePaste(), whose
image branches insert [Image #N] into the hidden main editor when a
login/API-key prompt is focused. Mirror the enhanced-paste behavior:
show 'Image paste is not supported in this prompt' and skip image-path
detection so only clipboard text reaches the focused prompt.
- Classified registered extension commands before automatic title generation while preserving normal and prompt-producing slash command titles.
- Added regression coverage for one-shot local handling, unnamed-session preservation, and forwarded prompt eligibility.
Fixes#6061
- Intercepted the configured smart-paste shortcut while a paste-capable modal owns focus.
- Added regression coverage for Ctrl+V API-key entry and documented the fix.
Fixes#6057
Cherry-picked 69c9fe8d4; resolved terminal.ts against the newer
onPrivateModeReport signature and unioned appearance tests with the
Windows Terminal polling regression.
- Routed automatic first-input and replan title requests through AgentSession lifecycle cancellation.
- Propagated disposal aborts to online provider and local tiny-model title generation.
- Added a regression test proving an in-flight title request settles when disposal begins.
Fixes#5666
The empty-editor left-left gesture opens the Agent Hub whenever persisted
or parked subagents exist (intended since f3e372e7b), but the hub's own
close detector starts fresh at 0 with no handoff from the editor's
double-tap detector. The two taps that opened the hub were consumed by the
editor, so a single subsequent left did nothing and the user had to press
left-left again to escape while input and hotkeys stayed disabled.
Thread an armCloseTap option from the gesture through showAgentHub to the
new AgentHubOverlayComponent.armCloseTap(), which seeds the table's
#lastLeftTap so one more left (within the tap window) dismisses the hub.
Fixes#4780
- Implemented `/queue` command and `->`/`=>` shorthands to support deferred, sequential message processing.
- Added a robust parsing utility to handle various list-based queue inputs and automate yield management.
- Integrated visual decorations and state tracking to provide real-time feedback on queueing status.
- Enabled non-cursor line text decoration in the TUI to support dynamic queue header rendering and list numbering.
- Preserved the session websocket preference for ephemeral /btw side-channel turns so Codex websocket-only models do not fall back to SSE.
- Prioritized active /btw and /omfg panels in Esc handling before loop, maintenance, and main-turn interrupts.
- Added regression coverage for side-channel websocket options and /btw Escape priority.
Fixes#5213
Left Darwin stdio MCP server launches in the inherited session so macOS TCC can prompt for Apple Events permissions used by xcrun mcpbridge.
Added resolver coverage for Darwin while preserving Linux detach and Windows console behavior.
Fixes#4987
Esc during an active streaming turn required a second press within 2s
(two-step arm from #3493). In the no-input-waiter submit path the turn
starts with isStreaming=true but no working loader, so Esc fell into
the two-step branch and the agent_start subscription then wiped the
arm — repeated presses kept re-arming and never aborted. The loader-up
path already aborted on a single press, so the confirmation guarded no
coherent state. First Esc now aborts the streaming turn directly.
Adopted from PR #4938 (test + input-controller + changelog hunks only;
unrelated workflow-notice.md churn dropped).
Fixes#4921
- Detected copied shell-prompt transcripts before the Python shortcut router.
- Forwarded OMP terminal chrome pastes through normal prompt submission.
- Added regression coverage for the #4678 transcript shape.
Fixes#4678
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
- 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.
The Esc/Ctrl+C teardown path had three duplicated try { abortX() } catch {} blocks (compaction/handoff/retry) that silently swallowed any abort error. Replace them with a shared safeAbort helper that logs the failure at debug, so a failing abort stays diagnosable while teardown ordering and the aborted flag are unchanged.
InputController.handleFollowUp read raw editor text via getText(),
bypassing the paste-store expansion the Enter path applies through
Editor.getExpandedText(). A large paste collapsed into a [Paste #N, +X
lines] marker was therefore sent verbatim to the model when queued with
Ctrl+Q / Ctrl+Enter, silently dropping the pasted content.
Switch the follow-up path to getExpandedText() so queued submissions
match the Enter path. Image markers are untouched; pendingImages
forwarding is unchanged.
Updated existing input-controller stubs (skill-queue, followup-image,
keybindings) to implement getExpandedText, matching the production
CustomEditor surface.
Fixes#3737
The replan-driven title refresh (title.refreshOnReplan, fired after a
`todo init`) called `generateSessionTitle()` without the user's
`TITLE_SYSTEM.md` override, silently falling back to the bundled
`prompts/system/title-system.md` and overwriting auto titles with the
default policy. The override was only ever discovered by main.ts and
passed into the first-input title path on InteractiveMode, never into
`AgentSession.#refreshTitleAfterReplan`. Most visible in Plan Mode,
which initializes todos early.
`AgentSession` now owns the resolved title prompt:
- New `CreateAgentSessionOptions.titleSystemPrompt` threaded by
`createAgentSession()` into the constructor.
- New `AgentSessionConfig.titleSystemPrompt` stored on
`#titleSystemPrompt` with a `get titleSystemPrompt` /
`setTitleSystemPrompt(...)` pair.
- `#refreshTitleAfterReplan` passes `#titleSystemPrompt` as
`customSystemPrompt` to `generateSessionTitle()`.
- `input-controller.ts` reads from `session.titleSystemPrompt`, and
the duplicate `InteractiveMode.titleSystemPrompt` field /
constructor arg / `InteractiveModeContext` field / `runInteractiveMode`
parameter are removed. `InteractiveMode.refreshTitleSystemPrompt`
now calls `session.setTitleSystemPrompt(...)` so a `/move`-style cwd
change keeps the override in sync.
Regression test asserts the prompt handed to `completeSimple()` from
`#refreshTitleAfterReplan` is the configured override, not the bundled
`title-system.md`.
Fixes#3734
- Updated invokeSkillCommand to accept image and link attachments.
- Added draft restoration logic to preserve input state if command dispatch fails.
- Modified invocation path to pass image data to invokeSkillCommandFromText for processing.
- Refactored command dispatch flow to handle errors by restoring the user's composer draft.
Tracked received thinking content per interactive session so OpenAI-compatible providers that omit reasoning metadata can still reveal streamed reasoning blocks. Added a Ctrl+T regression covering the unlocked visibility path.
Fixes#3669
- Centralized JSON parsing and stream processing logic by moving utilities from `packages/ai` to the shared `@oh-my-pi/pi-utils` package.
- Standardized import paths for JSON parsing and streaming across the agent, ai, and coding-agent packages.
- Refactored SSE stream handling to use consolidated `parseStreamingJson` logic and introduced robust error recovery for malformed container-shaped tail events.
- Cleaned up legacy bundled registry references and updated related module exports and tests to reflect the new utility structure.
Reviewer caught: the macOS file-URL loop returned after the first
image-shaped path, silently dropping the rest of a multi-image
selection. The bracketed-paste handler in `CustomEditor.handleInput`
already iterates every extracted image path; the keybind path now does
the same. Mixed selections (one .pdf + two images) still attach all
images and skip the non-images.
Tests cover (a) multi-image selection (3 attached), (b) mixed selection
with the file-URL fallback owning the outcome (text fallback MUST NOT
run when at least one file URL was an image).
Refs #3506
Reviewer caught (correctly) that the #3506 text fallback relied on
`clipboard.readText()`, which on Darwin shells out to `pbpaste(1)` —
pbpaste only surfaces plain text / RTF / EPS, so a Finder Cmd+C
pasteboard (`public.file-url` only, no plain text, no raw image bytes)
made readText() return empty and the new path-detection never ran.
Add a Darwin-only `readMacFileUrlsFromClipboard` helper that pipes a
small AppleScript through `osascript -` to coerce the pasteboard via
`«class furl»` and emit POSIX paths one per line. Wire it into
`InputController.handleImagePaste` between the readImage and readText
calls; the first image-shaped path routes through
`handleImagePathPaste`, non-image file URLs (e.g. a copied .pdf) fall
through to the existing text fallback. The clipboard interface field is
optional so existing test fixtures keep working without changes.
Tests: covers (a) Darwin file-URL pasteboard with empty pbpaste,
(b) non-image file URLs falling through to text, (c) the helper itself
on darwin/non-darwin and when osascript fails.
Refs #3506
When the clipboard exposes only a file URL for an image (e.g. Finder
`Cmd+C` on a `.png`, certain screenshot tools), arboard's
`get_image()` returns `ContentNotAvailable`. `handleImagePaste` then
fell through to the #1628 smart-paste text fallback and pasted the path
verbatim, while the terminal-mediated paste round-tripped through
bracketed-paste's `extractBracketedImagePastePaths` and attached the
image — producing the asymmetric "for image I need control+v which is
very odd" symptom on macOS.
Refactor `custom-editor.ts` to share the bracketed-paste path-detection
logic via a new `extractImagePathFromText` export, then route the text
fallback through `handleImagePathPaste` whenever the clipboard text is
exactly one explicit image file path. Both keybind- and terminal-mediated
paste now agree.
Fixes#3506
Replaced the streamingComponent/session token fallback with a per-turn sentinel that is reset on every agent_start/agent_end. The arm now survives the pre-message_start to post-message_start transition (and any later message_update churn) within a single turn instead of re-arming when streamingComponent first appears.
EventController replaces ctx.streamingMessage with a fresh immutable snapshot on every message_update, so keying the Esc arm on that reference invalidated it between presses and re-armed instead of cancelling. The arm now keys on ctx.streamingComponent — created once per message_start and stable across deltas — with ctx.session as the pre-message_start fallback, still cleared on agent_start/agent_end.
Subscribed the input controller to session lifecycle events so a fallback streaming Esc arm taken pre-message_start cannot carry across an agent_end/agent_start pair and abort a fresh turn within the 2s window.
Added a two-step Esc guard for active streaming responses and deferred ordinary render scheduling behind queued input so Esc delivery stays responsive under streaming load.
Fixes#3493
Wrap the streaming Enter steer dispatch in try/catch so prompt failures restore text plus pendingImages / pendingImageLinks / imageLinks and surface showError instead of losing an image-only draft.\n\nAlso remove a forbidden ReturnType<> from the follow-up image regression helper.\n\nFixes #3467
Snapshot pendingImageLinks alongside pendingImages at the top of handleFollowUp and wrap both the streaming and idle session.prompt dispatches in try/catch that restores text + pendingImages + pendingImageLinks + imageLinks and surfaces showError, mirroring the main submit and focused submit error paths so an image-only or text+image Ctrl+Enter draft survives dispatch rejection.\n\nFixes #3467
Snapshot pendingImageLinks and re-seed editor.pendingImages / pendingImageLinks / imageLinks in the focused-session submit catch block so an image-only or text+image draft survives a viewSession.prompt rejection, mirroring the main controller error path.\n\nFixes #3467
Treat pending pasted images as submit content in the main and focused input controller paths so image-only Enter/Ctrl+Enter submissions queue instead of dropping or aborting.\n\nFixes #3467
brush-core's Process::wait calls tokio::signal::unix::signal(SIGTSTP) to
detect when its children get stopped. Per tokio's documented contract,
the first call for a SignalKind permanently replaces the kernel-default
handler for the lifetime of the process. So once omp has executed any
bash tool call — even /usr/bin/true — SIGTSTP's default "stop" action
is gone, and InputController.handleCtrlZ's process.kill(0, "SIGTSTP")
gets swallowed by tokio. The TUI tore down via ui.stop() but the process
kept running in Sl+ state, leaving the user with a dead terminal that
only kill -9 could recover.
Send SIGSTOP to our own PID instead. SIGSTOP can't be caught, blocked,
or ignored — it stops the process at the kernel regardless of installed
handlers. Targeting self (not pgid=0) also leaves long-lived children
(MCP stdio servers, the persistent brush native shell) running across
the suspend, so they no longer freeze mid-IPC during a quick fg/bg
detour.
Fixes#3461
- Removed `onPasteFilePath` handler to prevent automatic background file attachment when pasting paths.
- Updated `CustomEditor` to treat non-image paths as literal text input.
- Cleaned up unused file system utilities and paste path resolution logic.