Stopped foreground streaming from treating an unknown native viewport as permission for destructive history rebuilds, so offscreen growth stays incremental after the viewport fills.\n\nFixes #2154
On kitty (Linux/Wayland) and any terminal supporting OSC 5522 enhanced
paste, the /login API-key prompt for OpenCode Zen — and any other modal
Input prompt (Perplexity OTP, GitHub Enterprise URL, manual OAuth
redirect URL, …) — silently dropped pasted content. The user could not
paste their API key; on Enter or Esc the key surfaced in the main
prompt.
Cause: InputController.#setupEnhancedPaste enables kitty's enhanced
clipboard protocol on TUI start and consumes the resulting OSC 5522
packets in an addInputListener that runs *before* focus dispatch in
tui.#handleInput. The controller's pasteText callback then routed
unconditionally to this.ctx.editor.pasteText(text) — the main
CustomEditor — even when selector-controller had cleared editorContainer
and focused a temporary Input. The text accumulated in the detached
editor and only resurfaced when the modal was dismissed.
Fix:
- TUI.getFocused() exposes the currently focused component.
- Input.pasteText(text) mirrors Editor.pasteText so any Input can absorb
a payload from a non-bracketed transport.
- InputController's enhanced-paste callback consults getFocused() and
routes text to the focused component when it exposes pasteText,
falling back to the editor only when no modal target is in focus.
- Image pastes refuse with a status message when a modal Input is
focused, instead of stuffing a binary blob into the hidden editor.
Regression tests in packages/tui/test/input.test.ts and
packages/coding-agent/test/issue-2127-repro.test.ts pin both the new
TUI/Input contract and the InputController routing source.
Fixes#2127
Inside tmux/screen/zellij, rewinding/branching after a streamed reply had
grown past the viewport permanently anchored the input box to the pane
top: every subsequent frame repainted the visible viewport blank and
parked the cursor at screen row 0.
`requestRender(true, { clearScrollback: true })` dispatches the
`sessionReplace` intent, which re-emits the entire transcript through
`#emitFullPaint` with `options.clearScrollback === false` inside
multiplexers (ED 3 is a no-op in pane history). `#emitFullPaint` only
reset `#scrollbackHighWater` inside the `clearScrollback` branch and
otherwise raised it monotonically (`if (pushedNow > #scrollbackHighWater)`),
so the high-water from the pre-rewind streaming peak survived the
shrink. On the next frame `#planLiveRegionPinnedRender` saw
`#scrollbackHighWater` far above the new natural viewport top, computed
`renderViewportTop` past the actual content, and `#emitLiveRegionPinnedRepaint`
clamped `viewportTop` to `lines.length` — writing eight empty viewport
rows and parking the cursor at screen row 0.
A full repaint with `clearViewport: true` physically re-emits the entire
transcript from row 0, so the rows committed to native scrollback by that
paint are exactly `max(0, lines.length - height)` regardless of whether
ED 3 cleared pre-paint scrollback. Mirror the existing `#maxLinesRendered`
pattern and assign instead of monotonic-max so the renderer's bookkeeping
matches the freshly painted transcript in both mux and non-mux modes.
Fixes#2130
- Added atomicTokenPattern support to treat image and paste markers as indivisible tokens, enabling atomic deletion on backspace and forward-delete instead of character-by-character removal.
- Extended image marker format to include pixel dimensions [Image #N, WxH] and updated paste marker format to [Paste #N, +X lines] or [Paste #N, Y chars] with consistent comma-separated metadata.
- Implemented accent styling for paste markers in editor rendering while preserving hyperlinks for image markers, improving visual distinction between marker types.
- Refactored image reference handling to unified placeholder system supporting both image and paste markers with kind-aware rendering callbacks and regex patterns.
- Reworked `StdinBuffer` to collect bracketed-paste chunks incrementally and flush them once the end marker is found, while carrying an overlap tail so split markers are still detected.
- Simplified pasted-text sanitization in `Editor` to use a single regex pass that strips control characters while preserving newlines.
- Added tests covering split-marker boundaries and large chunked pastes to verify the new paste assembly behavior.
Codex review on #2101 caught that `ImageBudget.endPass()` (and any other
mid-composition caller) fires `requestRender(false)` synchronously from
*inside* the in-flight paint composition — before `#armPostFullPaintSettle()`
runs at the tail of the intent dispatch. The request slipped past the new
settle gate, set `#renderRequested = true`, and queued a `scheduleImmediate`
that landed in `#scheduleRender` after the paint returned. `#scheduleRender`
then created a `#renderTimer` on the standard 30 Hz throttle (~33 ms), which
fired well inside the 150 ms quiet window the settle was meant to enforce —
defeating the coalescing and reintroducing the cascade.
`#armPostFullPaintSettle` now reclaims the in-flight request: it records
`hadPendingRender = #renderRequested || #renderTimer`, clears both, then —
if a request was reclaimed — eagerly arms the settle's trailing timer at the
full window length so the absorbed request still rides one coalesced render
at the end of the window. Subsequent `requestRender(false)` calls during
the settle see the timer and fold into it via the existing gate.
Added a regression test that drives a sessionReplace through a component
whose `render()` calls `tui.requestRender()` synchronously (mirroring
`endPass()`'s exact shape). The test uses an injected `RenderScheduler`
that records every `scheduleRender(cb, delayMs)` call, then asserts no
short-delay (< 100 ms) timer is queued after the arm — only the settle's
trailing timer at ~150 ms. Verified the test fails without the fix
(records a 32 ms throttled timer) and passes with it (only the settle
trailing timer).
Codex review on #2101 caught that `chunkForConPTY` compared the code-unit
length of the JS string to the cap, while `process.stdout.write(string)`
UTF-8-encodes before `WriteFile`. A CJK transcript row encodes to ~3 bytes
per BMP code unit, so the 16 KiB code-unit cap raised in #2101 could land at
~48 KiB of actual `WriteFile` traffic and reintroduce the #2034 parked-
viewport bug for non-ASCII content.
Renamed the constant to `MAX_CONPTY_WRITE_CHUNK_BYTES`, rewrote the chunker
to walk code units while summing each one's UTF-8 width (1 byte for <0x80,
2 for <0x800, 4 across surrogate-pair pairs, 3 for other BMP and unpaired
surrogates), and switched `#safeWrite`'s gate to `Buffer.byteLength(data,
"utf8")`. Surrogate pairs are kept together so the chunker never produces
an unpaired surrogate that would round-trip as U+FFFD.
Added regression coverage:
- `chunkForConPTY` directly: a CJK payload whose byte length is 4× the cap
splits into chunks each ≤ cap bytes; a non-BMP emoji-only payload never
splits a surrogate pair.
- `ProcessTerminal` end-to-end on win32: a 30-row CJK payload whose code-unit
length fits within 16 KiB but whose UTF-8 length doesn't is still chunked
(the old code-unit gate would have let this through as one oversized
`WriteFile`).
Resuming a session on Windows ConPTY paints the entire transcript through
#emitFullPaint so the historical rows scroll into native scrollback. WT's
viewport-follow logic gets lossy during that burst: spinner/blink-driven
requestRender(false) calls firing at 30 Hz immediately afterwards each emit
another viewport repaint, the host can't keep up, and the visible tail ends up
parked a few rows above the actual last row until any focus event (Alt+Tab)
forces a host repaint. The drift is non-deterministic (1-15 rows per resume).
Fix: after sessionReplace/historyRebuild/overlayRebuild paints whose lines
overflow the viewport, arm a 150 ms ConPTY settle window keyed on
isConPTYHosted(). Inside the window, non-forced requestRender(false) calls are
coalesced into a single trailing render that fires when the window expires —
the host fully drains the big paint before any new bytes hit the buffer.
Forced renders preempt the settle so user-driven Ctrl+L stays immediate, and
stop() cancels the trailing timer. The first-ever initial paint is exempt:
nothing has been on screen yet so no drift can have accumulated.
Also bumped MAX_CONPTY_WRITE_CHUNK from 8 KiB to 16 KiB. A 3 MB session-resume
paint emits ~192 WriteFile calls instead of ~384, halving the surface area
where WT's viewport tracker can lose the cursor mid-burst, and still well
under the ~32 KiB threshold from #2034 that the original 8 KiB cap defends
against.
Inlined the (now single-caller) isWindowsSubsystemForLinux helper and exported
the new isConPTYHosted predicate that both #safeWrite and the renderer's
#armPostFullPaintSettle hang off, so the WSL/win32 detection stays in one
place.
Fixes#2095
The earlier guard only suppressed throttled `requestRender(false)`; forced repaints (e.g. `#finishSixelProbe`, image-budget eviction, `resetDisplay`) bypassed it through `requestRender(true)` or the direct `#doRender` path and could still paint into a still-reflowing pane inside the 50 ms settle window — recreating the flash race.
Extracted `#armMultiplexerResizeTimer(clearScrollback)` as the single arm-or-extend point used by the SIGWINCH callback, the deferral branch in `requestRender(true)`, and `resetDisplay()`. Each call cancels any queued throttled render, ORs the callers `clearScrollback` intent into `#deferredForcedClearScrollback`, and re-arms the timer. When the timer fires it consumes the flag and re-enters `requestRender(true, { clearScrollback })`; clearing `#multiplexerResizeTimer` before re-entry lets the deferred call pass straight to `#prepareForcedRender`. `stop()` clears the flag.
Added regression tests for forced `requestRender(true)` and `resetDisplay()` landing inside the debounce window.
A streamed-token tick that landed `requestRender(false)` in the same 30fps frame as the SIGWINCH left `#renderTimer` armed, so the throttled render could fire inside the 50 ms settle window and paint at the new geometry while tmux was still reflowing — the same race the debounce is meant to avoid. The immediate-force path used to clear that timer through `#prepareForcedRender`; the debounce path now does the same when arming, and `#scheduleRender` short-circuits while the multiplexer timer is active so any follow-on `requestRender(false)` is held off until the eventual forced render. Added a regression test covering a queued render + SIGWINCH in the same frame.
- Handled SGR mouse motion reports in `PlanReviewOverlay` to set hovered options from hit-test rows and clear highlights outside option rows.
- Updated option rendering to paint a background band on hovered, non-disabled options while preserving keyboard selection behavior.
- Enabled any-motion mouse tracking for overlays and expanded regression coverage to verify hover tracking setup and teardown.
Tmux/screen/zellij send SIGWINCH while the pane is still mid-reflow and fire several events during drag-resize or pane-close animations. Forcing an immediate render on each event raced those mid-reflow paints — the multiplexer overwrote the TUI output and the user saw the viewport flash blank before the next throttled frame.
The SIGWINCH callback now debounces inside multiplexer sessions through a 50 ms render-scheduler timer; subsequent events cancel and re-arm it, so a single forced render fires at the final geometry once the pane is quiet. `#resizeEventPending` is still set on every event so the eventual render classifies as a resize, and stop() cancels the pending timer.
Fixes#2088
- Added a reassembly buffer for DEC 2048 in-band resize reports split across stdin reads.
- Dropped invalid or stale partial resize sequences to prevent leaked trailing bytes from entering terminal input.
- Added tests that verify split resize reports are handled and split kitty key sequences are forwarded correctly.
- Tracked focus transitions in TUI and flagged renders after focus changes to allow unknown viewport mutations.
- Used the new focus-change flag when deciding explicit viewport mutation so subsequent frames repaint even when the terminal lacks a viewport-oracle position.
- Added a regression test covering menu teardown focus changes on unknown-viewport terminals with eager scrollback risk.
- Built canonical and alias key maps for action/custom handlers and rebuilt them on changes.
- Preserved the first handler when custom key alias collisions occurred during rebuilds.
- Exported canonicalKeyId and addKeyAliases from keybindings for external use.
- Added hardware cursor state caching to reuse last known position and visibility.
- Updated cursor flow to preserve target row/column/visibility across rerenders.
- Reworked cursor output emission to skip unchanged cursor moves and hide/show writes.
- Replaced the TUI row truncation path with per-character and ANSI-sequence iteration that tracks visible cells before clipping output.
- Added ANSI helpers to parse sequence boundaries and preserve OSC66 visible payloads while enforcing max source length during truncation.
- Added natives regression coverage for Ghostty super+alt backspace key matching and parsing.
- Switched long-row truncation to visible-cell width, preserving suffixes after zero-width prefixes.
- Added issue-2045 regression test in issue-2045-repro.test.ts for combining-prefix truncation.
Ghostty on macOS reports Option+Backspace as kitty modifier 11
(wire) = 10 (mask) = super(8) | alt(2). The native key matcher only
recognised shift/ctrl/alt, so:
- format_kitty_key in crates/pi-natives/src/keys.rs rejected any
sequence whose effective modifier had bits beyond shift/ctrl/alt
set; parseKey returned undefined for \x1b[127;11u.
- parse_key_id recognised only those three modifiers, so even an
explicit 'super+alt+backspace' degraded to 'alt+backspace' and
could not match an actual super+alt encoded press.
- The editor's matchesKey(data, 'alt+backspace') branch was never
reached and Option+Backspace either no-op'd or fell through to
single-character delete.
Added a MOD_SUPER constant, taught parse_key_id, format_with_mods,
and format_kitty_key about the super bit, added super+alt+backspace
(and super+alt+delete / super+alt+d) to tui.editor.deleteWord*
defaults, and broadened the editor's direct matchesKey calls to
accept the super+alt variants. Existing alt+backspace / alt+d /
alt+delete paths are untouched, so the legacy ESC+DEL workaround
keeps working too.
Added Rust unit tests for the super modifier (positive and
negative for hyper/meta sequences we still drop) and TS
regressions in keys.test.ts and editor.test.ts that exercise
\x1b[127;11u end-to-end.
Fixes#2064
Detected OSC 66 text-sizing sequences in the bounded line fitter and included them whole so payloads (e.g. Markdown headings) are not skipped as zero-width.
Added a regression assertion that OSC 66 visible cells survive the bound when the row begins with the sized span.
Scanned oversized render rows by ANSI sequence so bounded line fitting skips leading zero-width control spans without dropping subsequent visible cells.
Extended issue #2045 regression coverage to assert visible text survives long SGR and OSC hyperlink prefixes.
Skipped repainting tmux panes when an offscreen shrink leaves the visible tail unchanged, and added regression coverage for the no-content-bytes contract.
Fixes#2046
Bounded the raw row span passed into TUI line fitting so ANSI-heavy subagent output cannot grow terminal write buffers independently of viewport width.
Added issue #2045 regression coverage for pathological zero-width ANSI rows.
Fixes#2045
The kitty keyboard progressive-enhancement probe sends `CSI ? u CSI c`
and used the DA1 reply as a fallback sentinel: if no kitty reply arrived
the modifyOtherKeys fallback was engaged. The kitty handler then refused
to enable kitty whenever modifyOtherKeysActive was already true, which
silently dropped the reply on terminals that answer DA1 before CSI ? u
(Superset / xterm-on-Electron). The protocol never enabled, so
Shift+Enter was delivered as a bare \r and the input box submitted.
Make the kitty reply authoritative regardless of ordering: drop the
`!modifyOtherKeysActive` guard, and if the premature DA1 already engaged
the modifyOtherKeys fallback, send `CSI > 4 ; 0 m` to disable it before
pushing the kitty stack frame. Kitty is strictly preferred (per the
existing #3259-fallback comment), so this is a clean cutover.
Add a regression test that drives a real ProcessTerminal through the
existing render harness across three orderings: kitty-then-DA1 (control),
DA1-then-kitty (#2042 regression), and DA1-only (modifyOtherKeys
fallback stays).
Fixes#2042
WSL processes report process.platform === 'linux', but their stdout still
funnels through wslhost into the same Windows ConPTY pipe, so the >32 KB
viewport-tracking regression reproduces there as well. The gate now fires
whenever process.platform === 'win32' OR isWindowsSubsystemForLinux()
detects WSL_DISTRO_NAME/WSL_INTEROP. Genuine POSIX terminals keep the
single-write fast path.
Added a WSL-specific test alongside the existing win32 / clean-linux
cases to lock the broader gate.
Fixes#2034
Windows ConPTY ties viewport tracking to per-WriteFile boundaries: when a
single process.stdout.write exceeds ~32-64 KB, the pseudo-console stops
following the cursor and the host UI's scroll position stays parked at
wherever the write began. Full paints (resume, history rebuild, large
permission dialogs) showed only the first screenful until Alt+Tab forced
the host to re-query the cursor; the bytes always landed in scrollback.
ProcessTerminal#safeWrite now routes win32 writes through a new
chunkForConPTY helper that splits oversized buffers into <=8 KiB pieces
at \n boundaries, keeping every WriteFile well below the threshold and
escape sequences intact (TUI paints separate rows with \r\n, so a line
break almost always exists in range). Non-win32 platforms keep the
single-write fast path.
Fixes#2034
- Constrained the no-append repaint optimization to execute only when the logical viewport position was unchanged.
- Documented the live-region pinned repaint cursor re-anchoring fix in the TUI changelog entry.
- Updated key canonicalization in keybinding parsing to parse modifiers case-insensitively and infer `shift` from printable uppercase keys.
- Added regression tests for `shift+a` matching uppercase `A` and for cursor anchoring during no-append live repaint scenarios.
- Recorded the uppercase Shift keybinding fix in the package changelog entry.
- Added animated message-color gating so loader timers run at 30fps only when needed.
- Added no-op checks in loader and text updates to skip work for unchanged values.
- Optimized key matching by canonicalizing key IDs and precomputing alias sets.
- Added prepared-line caching and bounded terminal/width scans to reduce repaint overhead.
- Updated the initial render intent to carry a `clearScrollback` flag.
- Adjusted first-frame rendering logic to preserve existing scrollback by default and clear only when requested.
- Added `resolver` stubs to coding-agent and web-search test registries for interface compatibility.
- Added a clear-initial-terminal-history flag to interactive mode initialization and passed it from cold-launch startup.
- Extended TUI.start to accept clearScrollback and clear native scrollback on initial full paint for non-multiplexer sessions.
- Preserved ED3 streaming scrollback accounting during capped streaming frames and added tests to prevent committed prefix duplication across terminal history rewrites.
- Replaced the repaint viewport clamp with strict bottom anchoring at lines.length - height.
- Removed the anti-duplication clamping path that could persist off-tail #viewportTopRow values between frames.
- Kept native scrollback cleanup to the existing dirty checkpoint mechanism for stale committed prefixes.
- Marked Markdown cached lines as readonly snapshots and returned copied arrays from `render()`.
- Stored fresh snapshot arrays in both L1 and L2 caches so callers appending rows cannot mutate shared cached Markdown output.
- Added an AskTool test that renders calls and results twice to verify repeated renders remain stable and do not duplicate option labels.
- Updated InputController streaming submit handling to interrupt and resume when queued steering exists, refreshing the pending-message UI and render.
- Added AgentSession.interruptAndFlushQueuedMessages to abort active work and continue processing queued messages immediately.
- Added tests for queued steering interruption in both agent-session concurrency and input-controller keybinding flows.
- Added OSC5522 parsing and a payload controller that prefers image MIME and reassembles chunks.
- Added bracketed image-path detection and `onPasteImagePath` callback wiring in the editor.
- Added local image-path paste insertion with fallback to text paste on image-size errors.
- Added `Editor.pasteText()` and OSC5522 disable sequencing on terminal cleanup.
- Removed the per-frame `#fitLineCache` in `TUI` rendering to avoid rope-string hashing during repaints.
- Deleted `packages/tui/bench/width.ts`, retiring an obsolete visibleWidth benchmark script.
- Fixed `visibleWidth` parity with native width using `Bun.stringWidth` ANSI stripping.
- Fixed tab and OSC66 width parsing with `s=`/`w=` metadata handling.
- Added ASCII and long-string fast paths in `visibleWidth` for faster width checks.
- Consolidated visible-width regression coverage by replacing removed IME/Arabic/Jamo tests with `visible-width.test.ts`.
- Clamped repaint anchor to min(firstChanged, scrollbackHighWater) so byte-identical committed rows are not re-painted into the active grid.
- Added submitPinsViewportToTail capability to reconcile native scrollback at submit on unprobeable local terminals.
- Renamed allowUnknownViewportMutation to allowUnknownViewportTransientRepaint to clarify it never authorizes a settled transcript commit.
- Made `Component.invalidate` optional in `packages/tui/src/tui.ts` and callsites.
- Added `iconOverride` support to status-line rendering in `status-line.ts`.
- Added `borderColor` and `framedBlock()` primitives in `output-block.ts` for framed output styling.
- Adjusted `renderOutputBlock()` to draw a continuous border when no header text is present.
- Added terminal checkpoint-reconciliation tests in `submit-checkpoint-reconcile.test.ts` for pinned-tail behavior.
- Added terminal detection for submit-key actions that pin the viewport to tail, and added a test override for that capability.
- Converted TERMINAL into a clone-based RuntimeTerminal so mutable capability fields can be updated directly by runtime setters.
- Updated checkpoint replay gating to allow ED3 rebuild when viewport position is unprobeable but submit-pin behavior is reported true.
- Added SGR mouse input handling for wheel and click actions, mapped to option, ToC, and body targets.
- Added deleted-heading state to undo snapshots so removed titles persist and restore correctly.
- Updated the plan-mode active prompt to require decision-complete, implementation-ready plans.
- Enabled mouse tracking for fullscreen overlays and asserted mouse-on/off sequences in tests.
- Added section-based plan parsing with per-section delete, undo, and annotate.
- Added Tab/Shift+Tab focus regions and per-line scroll navigation.
- Added Refine feedback loop emitting annotations back to the model.
- Added `OverlayOptions.fullscreen` borrowing the terminal's alt screen buffer.