- Migrated 288 lines of scattered error classification logic from `utils/error-id.ts` into a cohesive `packages/ai/src/error/` module with 13 specialized submodules covering flags, classes, OAuth, providers, rate-limiting, and finalization.
- Replaced 100+ generic `Error` throws across 60+ provider and registry files with semantic `AIError.*` classes (e.g., `AIError.MissingApiKeyError`, `AIError.OAuthError`, `AIError.ProviderResponseError`), improving error diagnostics and retry logic.
- Consolidated error utility imports from `pi-utils` and scattered classification functions into a single `AIError` namespace, reducing coupling and simplifying error handling across all packages.
- Removed "running" status and hub hint details from the subagent badge text.
- Updated relevant status line tests to expect the simplified badge format.
Address PR #3602 review feedback from chatgpt-codex-connector:
when a stdin read carries the empty bracketed paste followed by
a trailing keystroke (a user pressing Enter right after Cmd+V),
the pre-fix paste path was fire-and-forget. The trailing byte
processed synchronously while the clipboard image read was still
pending, so submit ran against an empty pendingImages and the
image landed on the next draft instead.
CustomEditor now tracks in-flight pastes with #pasteInFlight and
buffers subsequent input into #pendingInput. #trackAsyncPaste
increments the counter, awaits the paste promise, decrements, and
drains the queue through handleInput (so requeueing still works
if a drained chunk triggers another async paste).
For an assembled paste whose remaining bytes are present in the
same call, those bytes are pushed onto #pendingInput before the
async paste starts, so they always run AFTER it settles. The
text-paste branch stays sync and drains its own queue inline.
New repro test asserts the call ordering: paste:start fires, the
queued Enter does NOT, and only after the paste promise settles
does Enter dispatch.
Address PR #3602 review feedback from chatgpt-codex-connector:
when the terminal fragments a bracketed paste across stdin chunks
(\x1b[200~ in one read, \x1b[201~ in the next — Windows Terminal
under load, certain SSH muxes, tmux extended-keys passthrough),
the previous single-chunk `isEmptyBracketedPaste` /
`extractBracketedImagePastePaths` guards never saw both markers
in the same `handleInput` call. The inherited
`BracketedPasteHandler` then buffered the run as a zero-length
text paste and Cmd+V still disappeared.
CustomEditor now owns its own BracketedPasteHandler that runs
ahead of `super.handleInput`, so split bracketed pastes resolve
to a single assembled payload before any routing decision:
- empty payload -> onPasteImage (Cmd+V macOS image-only screenshot)
- image-file paths -> onPasteImagePath (#3506 also gains split-chunk
coverage as a bonus)
- everything else -> base editor's public `pasteText` so the
`[Paste #N]` markers, autocomplete, and undo state stay intact
Removed the now-redundant single-chunk `isEmptyBracketedPaste`
helper. New repro tests cover the split-chunk empty paste, the
split-chunk image-file path, and a split-chunk text paste
forwarding exactly once to the base editor.
Address PR #3602 review feedback from chatgpt-codex-connector:
a whitespace-only bracketed paste carries real user content
(indentation, blank-line padding) and must reach the editor as
literal whitespace. The prior 'trim().length === 0' guard treated
whitespace pastes the same as empty pastes and routed them to the
clipboard-image reader, which in SSH/headless sessions silently
replaced the whitespace with a 'Clipboard is empty' diagnostic.
isEmptyBracketedPaste now matches only strict zero-length payloads
('\x1b[200~\x1b[201~'). Whitespace pastes flow through to the
normal text-paste path; empty pastes still route to onPasteImage so
Cmd+V on an image-only macOS clipboard keeps working.
The whitespace-only test case is flipped to assert the preservation
contract instead of the hijack path.
macOS terminals (iTerm2, Terminal.app, Warp, Ghostty without OSC 5522,
…) intercept Cmd+V and read NSPasteboardTypeString first. For an
image-only clipboard (Cmd+Shift+5 screenshot saved to clipboard, Chrome
image copy, …) that read returns empty, so the terminal forwards a
complete-but-empty bracketed paste — '\x1b[200~\x1b[201~' — to the app.
CustomEditor.handleInput inserted that empty payload and the keystroke
disappeared, forcing users back to Ctrl+V (which is never intercepted
and already routes through handleImagePaste).
Add isEmptyBracketedPaste and route a complete, empty-or-whitespace-only
bracketed paste through the same onPasteImage smart reader the
app.clipboard.pasteImage keybind uses, so Cmd+V attaches the clipboard
image (or falls back to the #1628 smart text paste / 'clipboard is
empty' diagnostic) instead of silent nothing. Bracketed pastes carrying
any text (including the explicit image-file path branch from #3506)
keep their existing routing.
Fixes#3601
- Renamed the `find` and `search` tools to `glob` and `grep` respectively across the codebase to improve command clarity.
- Implemented full-stack support for the renamed tools, including CLI arguments, system prompts, SDK exports, and tool registration.
- Added automated migration logic in `settings` to transform legacy `find` and `search` configuration keys to their new equivalents.
- Updated the `collab-web` renderer registry to ensure backwards compatibility with legacy tool outputs.
Dropped eager todo snapshot displacement from tool_execution_start, streaming message_update, and the rebuild assistant-iteration step. Displacement now runs only when the next todo's successful result lands, so a failed follow-up leaves the last-good todo panel on screen.
Added regression coverage for the failed follow-up case and updated the streamed-second-todo test to drive displacement from the success result.
Fixes#3516
Resolved any tracked todo snapshot before storing a fresh one in the rebuild paths so an assistant message replaying multiple todo tool calls collapses to the final snapshot.
Added a renderSessionContext regression test for two todo tool calls in one rebuilt assistant message.
Fixes#3516
Kept successful todo result blocks live until a later todo update replaces them or the turn ends.
Added regression coverage for same-turn todo snapshot replacement after intervening tool output.
Fixes#3516
Reviewer caught: `extractImagePathFromText` reused the bracketed-paste
splitter, which treats unescaped spaces as separators. macOS screenshot
filenames default to names like
`/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png` — the
splitter shredded those into 5 segments, the second segment failed the
explicit-path check, and the helper returned undefined, so the keybind
fallback pasted the path verbatim instead of attaching the image.
Add a whole-text-as-path stage gated on a new ABSOLUTE_PATH_PREFIX_REGEX
(matches `/`, `~/`, `file://`, `\\`, or a drive letter), used
only when the splitter found nothing (otherwise multi-path text like
`/tmp/a.png /tmp/b.png` would be mis-joined). Prose containing a
path-shaped fragment ("see /tmp/x.png") fails both passes and still
pastes as text.
Tests cover (a) macOS screenshot names with spaces, (b) ~/Pictures and
Windows paths with spaces, (c) anchored prose fragments not hijacking
the fallback, and (d) end-to-end real-file integration via
handleImagePaste.
Refs #3506
When the macOS pasteboard's text representation forwards a
`file:///Users/.../img.png` URL (Ghostty/iTerm2/etc. forwarding the
`public.file-url` representation after a Finder copy), the smart
bracketed-paste / keybind fallback recognized it as a path but
`loadImageInput` then tried to read a literal `file://` path and
failed. `normalizePastedPath` now decodes `file://` URLs via
`node:url.fileURLToPath` before the explicit-path check, mirroring
Codex's `normalize_pasted_path` in
`codex-rs/tui/src/clipboard_paste.rs`. Both the bracketed-paste path
and the new `extractImagePathFromText` keybind path benefit.
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
- 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.
Migrate fullscreen overlay selectors onto routeSgrMouseInput() and
routeSelectListMouse(), removing duplicated SGR parsing and SelectList
hit-test boilerplate. Behavior-preserving: wheel step sizes, footer/row
offset guards, and consumption semantics are unchanged.
Make the inline-picker wrappers (theme/thinking/queue-mode/show-images/
plugin) MouseRoutable, each subtracting their single top-border row before
delegating to SelectList.routeMouse(). Name previously magic coordinate
offsets (spacerRowsAfterTabs, contentColInset).
- Implemented SGR mouse event routing for dashboard interaction, including tab selection and pane scrolling.
- Added mouse-driven list manipulation in the extension viewer with selection highlighting, click toggling, and wheel navigation.
- Enabled fullscreen alternate-screen behavior and host terminal mouse tracking for the dashboard overlay.
- Integrated hit-testing and row selection logic into the extension list to support unified mouse and keyboard inputs.
Replaces the old /move (which relocated the current session file) with a
new flow that starts a fresh empty session in the target directory, leaving
the previous session resumable via /resume. With no argument, /move opens
a path autocomplete overlay (type to filter, Tab to accept, Enter to
confirm). If the target directory does not exist, a confirmation prompt
offers to create it. Empty move sessions are cleaned up on shutdown.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The append fast-path opened the session file for sentinel comparison and
recompute without a guard, so a file unlinked/rotated between #refresh's
statSync and the sentinel read threw out of the 250ms poll timer (no catch),
risking a TUI crash. Treat sentinel read/recompute failures as non-appendable
and fall back to the guarded full reload. Adds a fake-timer regression.
Converted bracketed non-image filesystem path pastes into session-local attachment references while preserving the existing image path flow.
Added regression coverage for editor routing and controller local file attachment behavior.
Fixes#3360
The ask tool's "Other (type your own)" free-text input is a prompt-style
HookEditorComponent. The hook-style branch already called matchesAppFollowUp
(Ctrl+Q / Ctrl+Enter) so Windows Terminal users — which can't deliver a
distinct Ctrl+Enter (#1903 / fixed by #1905) — had a working chord on the
main editor, hook editors, and the agent dashboard. The prompt-style branch
did not, so Ctrl+Q was unbound and Ctrl+Enter fell through to Editor as a
newline that WT silently swallowed: pressing Ctrl+Enter did nothing.
#handlePromptStyleInput now checks matchesAppFollowUp before the rest of
the dispatch (mirroring #handleHookStyleInput), so plain Enter remains the
primary submit and the chord is a secondary submit for cross-terminal
muscle memory. The prompt-style hint now reads "enter or ctrl+q submit"
so the fallback is discoverable.
Fixes#3353
The welcome 'Tip:' line hardcoded #b48cff (label) and #9ccfff (body)
with an additional \x1b[2m dim on the body, ignoring the active theme.
On any light theme this dropped the body to ~1.5:1 contrast on a white
background (WCAG AA needs >=4.5:1), making the line effectively
invisible. Switching between light variants did not help because the
colors were not theme-derived.
renderWelcomeTip in packages/coding-agent/src/modes/components/welcome.ts
now paints the label through theme.fg('customMessageLabel', ...) and the
body through theme.fg('muted', ...), drops the manual dim, and wraps the
whole line with theme.italic(...). Both tokens are tuned per theme, so
the line stays vivid on dark backgrounds and readable on light ones
(e.g. light theme's customMessageLabel #7e57c2 = 5.21:1 on white).
A regression test pins the contract: dark/light themes must produce
different bytes for the same tip, and no manual \x1b[2m may remain.
Fixes#3337
The welcome panel advertised '? for keyboard shortcuts' even though
the ? shortcut was deliberately removed in commit dcf482c4c
('fix(editor): removed ? shortcut that opened hotkeys when input was
empty'). Users typing ? on an empty prompt got a literal question
mark submitted to the model instead of the help panel they expected.
Replace the stale claim with '/hotkeys for keyboard shortcuts',
matching the actual command that opens the shortcut reference.
Fixes#1614
Resolve the session-selector.ts conflict by integrating the delete-dialog
content-slot fix (#3283) on top of the fullscreen mouse-picker refactor.
The branch swapped the delete-confirmation dialog INTO a single content
slot (replacing the SessionList) so the picker is always
`chrome + max(list, dialog) + chrome` and never overflows the viewport.
Adjustments baked into this merge:
- Wrap the SessionList in `#contentSlot` and keep the dialog swapping into
that slot, but preserve the new fullscreen path: mouse hit-testing,
the pinned footer (`#footerLines`/`#footerStart`), and fill-height
trimming all still work because the render offset now tracks
`#contentSlot` (the list lives one level down).
- Keep both CHANGELOG entries (picker mouse/fullscreen + #3283 fix) and
the ported scroll-stability regression test.
- Enabled fullscreen overlay rendering for the terminal session picker.
- Implemented full mouse support including wheel-based scrolling and click-to-select functionality.
- Anchored the session picker footer to the bottom of the viewport to correct UI flickering.
- Added comprehensive unit tests for mouse interaction and layout constancy during resizing.
Earlier rounds shrank the SessionList by the dialog's row count to keep
the picker inside the viewport, but the SessionList could only claw back
whole session rows and bottomed out at zero entries. On a narrow
terminal with a long session title the dialog still wrapped past what
the SessionList could free, the picker overflowed the viewport, and the
TUI committed the header into native scrollback.
The picker now hosts the SessionList inside a single contentSlot
Container. Opening the delete confirmation swaps the dialog INTO that
slot (replacing the SessionList); closing it swaps the SessionList back.
The dialog therefore competes only with the SessionList's rendered
budget, not with the SessionList AND the picker chrome, so the picker
frame stays bounded by terminalRows even when the dialog wraps to many
rows. SessionList's external-reserve plumbing is no longer needed and is
removed.
Addresses PR #3285 second-round review feedback.
The first round of the issue #3283 fix reserved a fixed 12 SessionList
rows for the delete confirmation dialog. On a narrow terminal or against
a long session name, HookSelectorComponent's Markdown title and help
text wrap past 12 rows; the picker would still overflow even after the
SessionList shrank to zero entries, and the TUI committed the picker
header into native scrollback again.
SessionSelectorComponent now overrides render() to measure the dialog's
actual rendered height at the live width before super.render() walks
the children, and pushes that as the SessionList's external-row reserve.
The dialog's own Container memoization makes the extra pre-render
essentially free.
Addresses PR #3285 review feedback.
The delete-confirmation dialog mounted as a sibling below the picker's
bottom border briefly grew the picker past the terminal height. The TUI's
append-only renderer committed the picker's top rows (header + first
sessions) into native scrollback to fit the dialog within the viewport.
When the dialog closed and the picker re-rendered shorter, `windowTop`
stayed pinned at `#committedRows`, leaving the picker stranded below the
committed prefix — the user saw the header scrolled off the top.
SessionList now exposes `setExternalReserveRows`; SessionSelectorComponent
reserves the dialog's worst-case height while the dialog is mounted so
the picker's total rendered output stays within the terminal viewport
and the TUI never commits its rows.
Fixes#3283