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.
- 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.
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.
renderUsageReports (command-controller) carried the #3268 dedup contract
with no regression test; the PR's added CLI test asserts the opposite
(per-limit CLI rendering shows the note twice). Export renderUsageReports
and add a real regression through it: two accounts sharing one window group
render a provider-wide UsageReport.note once and an identical per-limit note
once. Verified failing on the pre-fix flatMap form (0 and 2 occurrences) and
passing on head (1 and 1).
When /settings (or the Extensions/Agents dashboard) is open and a tool
approval prompt fires, ExtensionUiController.showHookSelector swaps the
editor out of editorContainer for the HookSelectorComponent. On exit,
the overlay's done() called overlayHandle.hide() + setFocus(editor),
both pointing at the editor captured as preFocus when the overlay
opened — now no longer mounted. The visible approval prompt then sat
unreachable: Up/Down/Enter/Esc routed to the unmounted editor and only
Ctrl+C escaped (issue #3349).
SelectorController now exposes focusActiveEditorArea(), which restores
focus to editorContainer.children[0] (the live slot owner) or falls
back to the editor. Wired into showSettingsSelector, showExtensionsDashboard,
and showAgentsDashboard close paths after overlay.hide().
Tests: unit test verifying focusActiveEditorArea picks the live slot
owner; TUI overlay-focus regression pinning the post-fix contract plus
a 'pre-fix snapshot' test pinning the broken pre-fix behavior so the
restore-from-preFocus assumption can't silently change.
Fixes#3349
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
Address review feedback: provider notes could contain tabs, embedded
newlines, or control characters that break TUI rendering. Both note
rendering sites (provider-wide and per-group) now wrap the joined text
through sanitizeText → truncateToWidth → replaceTabs per AGENTS.md
TUI Sanitization rules.
Provider-wide disclaimers (e.g. OpenCode Go's "OMP-observed spend
only") were duplicated onto every UsageLimit, then repeated N times
in the TUI aggregate renderer (once per account × window). With
2 accounts × 3 windows, the same disclaimer appeared 6 times
bullet-joined.
Structural fix:
- Add notes?: string[] to UsageReport (interface + both schema
copies: usage.ts and auth-broker/wire-schemas.ts) so the field
survives the broker client's "+": "reject" deserialization gate.
- Move opencode-go's disclaimer from per-limit notes to
provider-level notes.
Defensive fix:
- Dedup identical per-limit notes in the TUI aggregate renderer
(command-controller.ts) via [...new Set(...)].
- Render provider-level notes once above per-account sections in
all three rendering paths: TUI (command-controller), CLI
(usage-cli), and ACP (usage-report helper).
Regression tests:
- usage-cli.test.ts: provider-level notes render once, not
duplicated per account or limit; positioned above per-account rows.
- usage-report-notes-schema.test.ts: wire-schema round-trip proving
notes survives usageResponseSchema validation.
Fixes#3268
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
Address P2 review: when executeBuiltinSlashCommand returns a string
(e.g. /loop 10 fix bug → 'fix bug'), the original slash command text
was not recorded to history — only the extracted prompt was. Now the
original text is added to history before reassigning, so Up Arrow
recalls '/loop 10 fix bug' rather than just 'fix bug'.
Applied to both Enter and Ctrl+Enter submit paths.
Address three P1 code review comments on PR #3352:
1. Colon-separator bypass: parseSlashCommand() treats ':' as an argument
separator, but shouldSkipHistory only split on whitespace. So
/login:?code=abc&state=xyz bypassed the filter. Now uses the same
earliest-whitespace-or-colon splitting as parseSlashCommand.
2. /join <link> secret: the collab join link carries a 32-byte room key
and optional write token. Add /join to the denylist — skip any /join
with arguments.
3. Added regression tests for colon-separator forms and /join denylist.
parseCallbackInput() accepts three forms: redirect URLs, query strings
(?code=...), and raw auth codes — all carry OAuth secrets. The previous
filter only skipped URL-like inputs, leaking query strings and raw codes.
Skip ALL /login commands with any argument. The minor convenience loss
(can't recall /login <provider>) is far less important than the risk of
persisting OAuth authorization codes.
Previously only 4 commands (/plan, /goal, /mcp, /ssh) stored their text
in history via per-handler addToHistory calls. All other built-in slash
commands were silently skipped because executeBuiltinSlashCommand returned
true before the input controller's addToHistory was reached.
- Centralize history recording in the input controller after successful
slash command dispatch, for both Enter and Ctrl+Enter submit paths.
- Remove all 10 per-command addToHistory calls from slash command handlers
to prevent duplicates.
- Add shouldSkipHistory() security filter to exclude commands that may
carry secrets: /login <url> (OAuth callback with code=/state= params)
and /mcp add --token <token> (bearer token).
- Add regression tests for the security filter (8 cases).
- Update 7 existing test files to remove handler-level addToHistory
assertions (now the input controller's responsibility).
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
- Transitioned the eval tool from batch multi-cell execution to a single-step input structure with flat parameters.
- Updated core agent logic, UI components, and documentation to support state persistence across incremental eval calls.
- Restricted bash tool capabilities by requiring explicit use of `read` or `find` instead of `ls` or `find`.
- Added support for Ruby and Julia language runtimes to the eval tool and associated web renderers.
- Update input handler to recognize CSI-u escape sequences using `matchesKey`.
- Ensure legacy bare escape sequences remain supported in environments without the protocol.
- Add test coverage for both CSI-u and legacy escape inputs.
- Introduced `generateHandoffFromContext` to enable provider-aware oneshot generation and improved cache hit rates via the live-turn pipeline.
- Updated `buildSideRequestContext` to support pinning custom system prompts, preventing per-turn hook leakage during handoff.
- Added concurrency guards across CLI and RPC modes to block manual `/handoff` requests while a session is actively streaming.
- Standardized handoff execution to force `toolChoice: "none"` and enforce consistent cache-routing behavior.
- Added a `share.store` configuration option (`blob` | `gist`) that allows users to choose between the default share server or a GitHub gist for storing exported session data.
- Changed the default upload target from secret GitHub gists to the share server to avoid GitHub API rate limits for shared sessions.
- Enabled fallback to the share server when a gist upload fails or the GitHub CLI is unavailable.
When a full rebuild caught the session file mid-append, #loadLocalFull cleared the pending buffer but advanced offset past the headless trailing bytes. The next poll then read only the completion bytes, parsed a headless fragment, and silently dropped the completed entry. Carry the bytes after the last newline as pending so the completion concatenates with them on the next #appendLocal pass.
Fixes#3258
The append-based collab-guest path only reset the byte cursor on host transcript rotation/truncation, so the refetched bytes stacked on top of the stale pre-rotation rows still in the ChatTranscriptBuilder. Drop the builder and model state before refetching from byte 0 so rotated host transcripts replace stale history instead of duplicating it.
Fixes#3258
If the session file grew between the earlier statSync and readFileSync inside #loadLocalFull, the rebuild rendered the appended bytes but the tail state recorded the pre-race stat.size. The next poll's #appendLocal would then read from that stale offset and re-render the bytes already in the rebuild, duplicating rows for agents that append during a viewer open or full rebuild. Anchor size/offset to data.byteLength and re-stat for mtime/identity so the post-read clock matches what's on disk.
Fixes#3258
Tail appended transcript JSONL instead of rebuilding rendered history on every poll, collapse compacted history for live chat rendering, and replace synchronous session rewrites so tailers detect historical changes.
Fixes#3258
Require bracketed image-path paste detection to see an explicit local path separator or file URI before routing .png-like text to image attachment handling.\n\nFixes #3253