- Propagated builtin allowArgs metadata into TUI autocomplete entries.
- Let no-arg slash-looking prompts fall through to prompt-composer completions while keeping argument-capable commands scoped.
- Added regressions for /settings @ and /settings #copy.
- Stopped prompt-composer # actions and @ file references from claiming submitted slash-command argument text without explicit argument completions.
- Added provider regression tests for /rename title arguments and explicit command argument completions.
Fixes#4600
- Migrated native scrollback logic to a visual-record model with a three-zone row verification system.
- Replaced heuristic-based audit boundaries with exactness-boundary tracking for improved reliability.
- Resolved issues with live tool preview duplication and content vanishing during scroll.
- Added adaptive index re-basing for frame shrinks and audit exemptions for frozen visual snapshots.
- Removed complex snapshot caching and volatile/stable state tracking logic.
- Replaced multi-zone audit logic with streamlined tail-sample checks.
- Simplified render boundaries by deriving a single final boundary from the live region.
- Eliminated redundant audit state management and auxiliary safe-end interfaces.
sendNotification only appended a BEL inside tmux, so under Zellij an OSC
9/99 notification was dropped (Zellij doesn't forward it and has no DCS
passthrough) and a backgrounded pane got no attention indicator. Added
isInsideZellij() and a Zellij branch that follows the OSC with a bare BEL,
raising Zellij's [!] flag like tmux's monitor-bell.
Refs can1357/oh-my-pi#3583
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
Merged PR #4330 and fixed the remaining offered-boundary regression by stopping offer promotion at intervening live blocks.
Verified with targeted transcript/native scrollback regressions: 40 pass.
Extended the audited-offer classification to cover rows already committed as forced-overflow when the offer boundary rises to include them, and widened the committed-prefix hard scan to the offer boundary so a single-row shift inside the offered zone catches instead of being tolerated by the tail sample.
Added regression asserting the offer boundary appearing after prior overflow still repairs the shifted tail on the next live growth.
Addresses codex-connector review on #4330.
Separated audited offerable transcript rows from durable snapshot rows so lower finalized content below a live block can be repaired instead of duplicated when the live block grows.
Added transcript and virtual-terminal regressions for the lower finalized tail case.
Fixes#4326
Skipped the xterm modifyOtherKeys fallback for unknown SSH terminals so iOS SSH clients do not lose Shift input while preserving the fallback for known/local terminals.
Fixes#4325
- Route #deleteToStartOfLine, #deleteToEndOfLine, #deleteWordBackwards,
#deleteWordForwards, and #insertTextAtCursor through
#retriggerAutocompleteAtCursor so text-mutating keys (Ctrl+W, Ctrl+U,
Ctrl+K, Alt+Backspace, Alt+D) and paste/yank refresh the popup like
#handleBackspace / #handleForwardDelete already do.
- Add the staleness guard to the Tab and Enter+file-path acceptance
paths in Editor.handleInput; the Enter+slash path already had it.
- Tighten #autocompletePrefixMatchesCursorText to mirror
CombinedAutocompleteProvider.applyCompletion's re-anchor branches
(exact / path-suffix / clean leading-slash / @-token) so genuine
"extended prefix" cases stay valid while destructive edits and
paste-over-prefix scenarios are correctly rejected.
Adds regression tests exercising every destructive keybinding, yank
over the prefix, and the Enter+file-path fall-through to submission.
Fixes#4295
ImageBudget#acquireId stored every non-empty imageKey in #keyToId (packages/tui/src/components/image.ts:134) but never removed entries, so the map grew unbounded for the lifetime of the TUI session. endPass now deletes the #keyToId entry for each id as it is demoted/purged from live graphics (packages/tui/src/components/image.ts:188-193), and takeAllTransmittedIds clears the map on the full transmitted-reset path (packages/tui/src/components/image.ts:223). Added unit tests covering demotion eviction and full reset. Verification (bun run check:types and bun test test/image-budget.test.ts) was not executed per campaign constraints.
Closes#4250
- Bypasses frame array allocation and merging during render except when hosted under ConPTY.
- Reuses the active window array slice for DECCARA calculations to avoid redundant array copies.
- Simplifies the `stdin-buffer` CSI sequence scanner by removing unnecessary resume search logic and redundant SGR mouse validation regexes.
- Ensures in-memory overlay edits are durably written to the plan file before proceeding with approval.
- Avoids asynchronous write races by awaiting the final plan file serialization.
- Aligns synthetic approved-plan prompts with reference-only expectations.
- Rewrites viewport in-place rather than shifting native history when window position moves while overlays are visible.
- Prevents scrolling native terminal history during frozen commit tape states.
- Re-baselines terminal cursor safely to the top row using viewport-clamped cursor up sequences during virtual scroll rewrites.
- Syncs the internal window tracking row state and commits the redrawn frame on virtual scroll rewrites.
- Replaced unbounded `String#indexOf` lookups with structured byte loops to enforce `MAX_STRING_SEQ_BYTES` cap.
- Stopped unterminated OSC, DCS, and APC sequences from scanning past their limits and blocking the event loop.
- Maintained a one-byte overlap when checking chunk boundaries to ensure split escape terminators are correctly reassembled.
- Added comprehensive unit tests covering split boundary terminators and oversized payload scenarios.
Fixes issue #2115: ConPTY hosts truncated a >1MB single full paint at a logical-line boundary, corrupting large-session resume. Bounds the bytes #emitFullPaint emits (retain tail) while keeping full logical state for render planning/commit; guarded to ConPTY hosts only. Includes the issue-2115 repro test. The PR head's original pre-planning truncation was obsolete against the refactored render pipeline, so the evaluator-approved approach (fix 44b88a2f5) was ported onto current #emitFullPaint.
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
Three failure modes shared the same root cause on the StdinBuffer.process
hot path — unbounded synchronous work in a single call.
- `StdinBuffer.extractCompleteSequences` grew each escape candidate one code
unit at a time and re-tested every prefix, so a malformed CSI/OSC/DCS/APC
streamed across chunks re-inspected the accumulated buffer on every
`process()` call (issue A). Replaced the grow-and-recheck loop with a
single linear scan bounded by a per-type cap (CSI 4 KiB, OSC/DCS/APC 16
MiB) and a resume-search offset carried on `StdinBuffer` so a legit
chunked OSC 5522 image paste stays O(total) instead of O(total²).
- `BracketedPasteHandler` had no cap or timeout: a caller that bypassed
StdinBuffer (fed the start marker with no end marker) grew `#buffer`
forever (issue B, defense in depth). Added an optional `byteLimit` option
(default 64 MiB) that aborts paste mode and delivers accumulated bytes as
`pasteContent`, mirroring `StdinBuffer#abortPaste`.
- The `ProcessTerminal` data handler ran six always-executed escape-probe
regex tests per `data` event; a 100 KB non-bracketed paste (terminal
without DEC 2004 support) turned into ~600K regex executions (issue C).
Added a fast path that forwards non-ESC sequences straight to the input
handler when no reassembly buffer is holding state.
Verified with new stdin-buffer + bracketed-paste tests plus empirical
repros: a streamed 1 MiB malformed CSI drops from ~4300 ms to ~50 ms of
synchronous work, and a 1 MiB chunked OSC 5522 payload drops from ~6800 ms
to ~25 ms.
Fixes#4073
- Preserved Unicode letters, marks, and numbers during fuzzy-search normalization so CJK and other non-ASCII queries no longer collapse to match-all searches.
- Added regression coverage for Chinese fuzzy filtering against unrelated ASCII items.
Fixes#4114
Cleared frozen-prefix render state after full-lex renders cannot establish a current appendable prefix, preventing truncated transient text from reusing spacer lines produced with a former tail.
Added regression coverage for truncating back to a previously frozen prefix.
Fixes#3975
The mid-prompt slash skill autocomplete added in #3654 replaced the
entire editor draft with /skill:<name> on accept so the dispatcher
(which only matched leading /skill:) would still fire. That wiped
every keystroke the user had typed before reaching for the skill.
Insert the /skill:<name> token at the cursor in the TUI editor —
replacing only the partial /sk slash token, leaving prose before and
after intact — and extend the skill-command parser so a /skill:<name>
token surrounded by whitespace is recognized as an invocation too,
with the surrounding prose threaded through to the skill as args.
The parser change is shared across all three dispatch sites
(interactive TUI, ACP, RPC) via a new parseSkillInvocation helper
in extensibility/skills, so the three Map<string,string> /
session.skills lookups stay aligned on the same parse.
Fixes#3913
- Added a public `submit()` method to the `Editor` class to trigger form submission.
- Updated the `EditorComponent` interface to expose an optional `submit()` method.
StdinBuffer held a bare `\x1b\x1b` chunk and timer-flushed it as one
sequence. `parseKey("\x1b\x1b")` returns undefined, so CustomEditor
fell through to the base editor and never fired the configured `onEscape` —
the double-escape gesture and the second-press single-Esc handler both went
dead whenever the terminal batched the two presses into one stdin read.
Split an exact bare `\x1b\x1b` into two ESC events only after the
flush window proves no follower arrived. If a follower does arrive, emit the
first ESC and restart parsing at the second ESC so legacy Alt chords
(`\x1bd`, `\x1b\x7f`) remain one downstream keypress. Meta-CSI/SS3
chords (`\x1b\x1b[A`, `\x1b\x1bO…`) still emit as one combined
sequence.
EventController.tool_execution_update re-armed the working loader when a
transient overlay (auto-compaction / auto-retry / handoff) had torn it down
mid-tool; tool_execution_end did not. A subagent (`task`) call only fires
_end, so a task result landing after such an overlay left the UI looking
idle even though the session was still streaming. Mirror the reconciler
call in #handleToolExecutionEnd.
Fixes#3857
Four non-overlapping algorithmic complexity reductions in hot paths.
(Streaming-reveal throughput is owned separately by #3843.)
1. session/session-manager.ts pathTo: O(n^2) branch.unshift() leaf->root
walk -> O(n) push + single reverse(). Hot path (5-10x/turn via getBranch).
2. edit/streaming.ts extractAddedLines: O(n^2) progressive string
concat per streaming tick -> array push + single join.
3. edit/modes/patch.ts: collapseConsecutiveSharedLines O(n*m) filter
+ includes -> Set (O(n+m)); collapseRepeatedBlocks O(n^3) with
per-iteration slice allocations + every() -> index arithmetic with
a single shared.has() guard. Semantics preserved.
Honorable: tui/src/utils.ts replaceTabs reallocated
" ".repeat(DEFAULT_TAB_WIDTH) every call -> hoisted TAB_SPACES const.
StdinBuffer held a bare `\x1b\x1b` chunk (or emitted it as one when followed by
a non-CSI byte). `parseKey("\x1b\x1b")` returns undefined, so CustomEditor
fell through to the base editor and never fired the configured `onEscape` —
the double-escape gesture and the second-press single-Esc handler both went
dead whenever the terminal batched the two presses into one stdin read.
Split a bare `\x1b\x1b` into two ESC events at the buffer layer, mirroring
the existing split for ESC + SGR mouse report. Meta-CSI/SS3 chords
(`\x1b\x1b[A`, `\x1b\x1bO…`) still emit as one combined sequence.
EventController.tool_execution_update re-armed the working loader when a
transient overlay (auto-compaction / auto-retry / handoff) had torn it down
mid-tool; tool_execution_end did not. A subagent (`task`) call only fires
_end, so a task result landing after such an overlay left the UI looking
idle even though the session was still streaming. Mirror the reconciler
call in #handleToolExecutionEnd.
Fixes#3857
Selectors (model, settings, file/tree, slash, OAuth) re-filter the same
stable candidate list on every keystroke. fuzzyRank was re-normalizing the
query once per candidate (~400x/keystroke) and rebuilding each candidate's
search index from scratch on every keystroke.
- prepareQuery/fuzzyMatchCore: normalize + split the query a single time in
fuzzyRank instead of per candidate. Pure win on warm and cold paths.
- indexCache: memoize the per-text SearchIndex across keystrokes, with
conservative admission -- texts >4 KiB bypass the Map entirely (no memory
bloat from pasted prompts/transcripts searched via the message selector),
and admission stops at the cap instead of evicting (a stream of unique
one-off texts can't churn the map).
Incremental typing over a 400-item list drops ~58% (6.7ms -> 2.8ms for an
8-keystroke session); first-keystroke (cold) latency is unchanged (~0.5ms),
as the query-prep savings offset the cache overhead. Match ranking is
byte-identical, enforced by a golden-checksum guard in the new benchmark and
contract tests.
Adds packages/tui/bench/fuzzy.bench.ts (deterministic corpus, cold-start
session measurement, ranking-drift guard) and fuzzy contract tests for cache
transparency and the empty-normalized-query callback behavior.
- Allow mid-prompt slash tokens (e.g., `/tmp`) to fall back to file-path completion when no matching skill is found.
- Update notification eligibility logic to allow D-Bus delivery for in-band terminals while excluding Bell-only protocols.
- Add test case verifying slash-command fallback behavior for file paths.
- Removed the hardcoded terminal whitelist for native notifications in favor of protocol-based detection.
- Updated Warp terminal configuration to prefer `NotifyProtocol.Osc9` over `NotifyProtocol.Bell`.
- Simplified notification delivery logic by delegating D-Bus toast filtering to terminal protocol capabilities.
- Added `getForceFileSuggestions` and `shouldTriggerFileCompletion` to `AutocompleteProvider` interface.
- Updated `Editor` to invoke file completion logic dynamically when providers support it.
- Corrected log viewer scroll offset calculation to account for viewport height.
- Added comprehensive unit test for log row selection and expansion.
Renamed the buffer-switch helper to keyboardEnhancementEnterSequence and added a sibling keyboardEnhancementExitSequence so callers can balance push/pop generically instead of hard-coding the kitty pop.
ProcessTerminal returns the matching kitty pop on exit when kitty is active, and null when only the xterm modifyOtherKeys fallback is active: modifyOtherKeys is a global terminal flag with no per-screen stack, so emitting >4;0m on overlay exit would clear it on the normal screen and break composer typing between overlays. terminal.stop() and the emergency-restore path still disable it globally on graceful teardown.
TUI alt-screen entry/exit and resize alt-screen helpers now route through the new pair with fallbacks to kittyEnableSequence for custom Terminal implementations predating the new properties.
Fixes#3705