Measured each animated Loader paint and recursively scheduled the next frame with a 10% duty-cycle target, capped at 200ms to retain responsiveness.
Added a regression covering a slow synchronized-output direct-write path and documented the fix.
Fixes#7290
- Add the `app.live.toggle` keybinding defaulted to `Ctrl+L` to start or stop live voice mode.
- Remap the default display-reset action (`app.display.reset`) from `Ctrl+L` to `Alt+L`.
- Update the live visualizer to listen for stop keys so the toggle chord terminates active sessions.
Cached one stable layout representative per frame display width so same-width animation retains the fast path while width transitions rewrap before painting. Added coverage for wider custom frames remaining within terminal bounds.
(cherry picked from commit 3aa173a574538d754e68f8d10f7b88d437709884)
The Loader baked the advancing braille glyph into the underlying Text
via setText, so Text.render's wrap cache (keyed on text) missed every
80ms tick and re-ran wrapTextWithAnsi plus per-line width measurement
over the whole message even though only a 1-cell glyph changed. On a
single-subagent hub wait the direct-write render loop consumed ~48% of
a saturated core.
The wrapped text now carries a stable sentinel glyph (frames[0]); the
spinner advance only bumps the frame index and requests a repaint, and
render() swaps the visible glyph over the cached layout. wrapTextWithAnsi
and stringWidth now run only when the message actually changes.
Fixes#6940
(cherry picked from commit 6ec38053ebc1e33859358df3b66f71fea27532c7)
Skipped stdout draining after ProcessTerminal observes a native Windows terminal disconnect, while preserving normal postmortem cleanup and drain behavior for every other shutdown path.
Fixes#6917
(cherry picked from commit b45fc00ab4b5075ccb9650584947dde061beea20)
ProcessTerminal.#safeWrite wrote every frame with process.stdout.write()
and dropped the backpressure return value. A stalled-but-alive PTY reader
never throws, so the write path had no error to catch: the writable buffer
grew without bound as cosmetic hub-wait spinner and 500ms progress frames
piled up, reaching ~48 GiB RSS and wedging the host in one incident.
Add OutputBacklogGuard to sum the bytes queued since backpressure began and
report when the backlog crosses a 64 MiB cap. #safeWrite then treats a
consumer that will not drain past the cap as a disconnect, taking the same
clean, supervisor-resumable SIGHUP exit a dead terminal already takes
(#5837) instead of unbounded growth.
Fixes#6854
- Render device doc parameter schemas as TypeScript types instead of raw JSON schema dumps.
- Optimize marked streaming block rules and add pre-gates to reduce CPU overhead.
- Increase the markdown render cache entry budget from 32 KiB to 256 KiB.
- Popped the alternate-screen keyboard frame before restoring the main screen.
- Reset the main-screen keyboard state in live and blind emergency cleanup.
- Added regression coverage for both restore paths.
Fixes#6810
- WT_SESSION leaks into tmux/screen/Zellij panes, so the automatic raw
0x08 -> ctrl+backspace override deleted a word on plain Backspace.
- The heuristic now requires isWindowsTerminalSession() and not
isInsideTerminalMultiplexer(); PI_TUI_RAW_BACKSPACE_IS_CTRL=1 stays an
unconditional opt-in.
- Review follow-up for PR #6784.
Gates guard 1's suppression on #dead: the disconnected path stays
best-effort, but a failed restore during a normal stop() throws as it
did before the guard, instead of silently leaving stdin in raw mode.
Claude-Session: https://claude.ai/code/session_01LTC1HNAntXEMnTYmuUpGHz
Windows Terminal identity is commonly lost across SSH and container hops,
leaving only the ambiguous raw 0x08 byte. Added the conservative
PI_TUI_RAW_BACKSPACE_IS_CTRL=1 opt-in so those sessions can map it to
ctrl+backspace without changing the default for terminals where 0x08 means
plain Backspace.
Restored the public isWindowsTerminalSession and matchesRawBackspace exports
and route the parser wrappers through matchesRawBackspace. Documented the
runtime flag and covered local WT, remote opt-in, SSH, and 0x7f behavior.
Fixes#6782
The multi-line prompt editor matched word/line delete and yank with
hardcoded chords via matchesKey() instead of the keybindings registry,
unlike cursor motion and the single-line Input component. As a result
ctrl+backspace (a declared default of tui.editor.deleteWordBackward)
never fired and keybindings.yml remaps of deleteWordBackward,
deleteWordForward, deleteToLineStart, deleteToLineEnd, yank, and yankPop
were silently ignored in the main prompt.
Route those six actions through kb.matches(). Also re-wire the Windows
Terminal raw 0x08 -> ctrl+backspace disambiguation into the TS
matchesKey/parseKey seam (where WT_SESSION is observable), replacing the
dead matchesRawBackspace helper that no longer had any call sites.
Fixes#6782
A recycled terminal pane revokes the pty. stdin EOFs, the disconnect path
runs, and stop() tries to restore raw mode on an fd that is no longer a
tty - Bun's node:tty shim throws ENOENT. That throw escaped stop() and
#markTerminalDisconnected(), preempting its own process.kill(SIGHUP), so
the process died with an uncaught exception instead of exiting 129.
Restoring raw mode on a dead fd is best-effort, matching what
emergencyTerminalRestore() already does, and the disconnect handler is
now wrapped so no teardown failure can preempt the exit.
Under WSL+tmux, resetDisplay() emitted an in-place update rather than a full
paint, so the one-shot unbounded ConPTY intent was never cleared. A later
/resume or handoff inherited it and replayed a multi-megabyte transcript
without the #2115 bound.
Consume the intent on the next authoritative normal-screen render regardless
of whether it emits an update or full paint. Cover resetDisplay() followed by
a bulk replacement under WSL+tmux.
Fixes#4863
The #4863 fix gated the ConPTY frame-truncation on firstPaint, which also
disabled it for post-startup bulk replacements (/resume, handoff) that enter
the fullPaint path with firstPaint=false — reintroducing the #2115 stall for
those flows.
Key the bound on paint intent instead: only a user-driven resetDisplay()
(Ctrl+O expand, thinking/setting toggles, display reset) opts out via
#unboundedConptyPaintRequested. Bulk transcript-replacement paints — first
paint, /resume, handoff, and resize geometry rebuilds — stay bounded.
Fixes#4863
The ConPTY frame-truncation added for large session resumes (#2115) fired on
every full paint on ConPTY hosts (native Windows + WSL), including the
user-driven resetDisplay() that Ctrl+O (app.tools.expand) triggers. Expanding
to review the whole session dropped every row above the retained tail behind an
"older lines hidden" marker.
Bound only the initial resume paint via a boundConptyPaint flag (= firstPaint);
user-driven redraws (Ctrl+O expand, resize geometry rebuilds) replay the whole
transcript untruncated.
Fixes#4863
Gate the runtime alt-toggle resize latch when PI_TUI_RESIZE_IN_PLACE explicitly forces the fast path off, while preserving multiplexer handling.
Add regression coverage that auto-detects the alt-toggle echo, then verifies a later width resize still borrows the alternate screen and performs the authoritative ED3 rewrap.
Terminals that re-report their size whenever the alternate screen buffer toggles turned every fullscreen-overlay exit (/settings, models page) and resize into a spurious height-only SIGWINCH. That armed the resize drag/settle behind the overlay and drove a destructive ED3 clear-scrollback full repaint on exit -- visible as a flicker on terminals without DEC 2026 synchronized output -- with the size revert flashing another.
A SIGWINCH while a fullscreen overlay covers the transcript now repaints the overlay in place instead of arming the drag/settle, and the in-place resize path is latched once the height-only alt-toggle echo is observed, so this was previously handled only for Warp and is now auto-detected for any such terminal.
Fixes#6511
- Replaced resolveHangulCompatibilityJamoWidthFromTerminalIdentity with a
static hangulJamoWidth field on TerminalInfo (ghostty 2, warp 1,
platform otherwise); startup now reads TERMINAL.hangulJamoWidth.
- Reused terminal identity detection to select Warp narrow and Ghostty wide profiles.
- Covered Warp resolution while preserving platform defaults for other terminals.
Fixes#6461
Status presenters (showWarning/showError/showStatus and extension/tool
error presenters) baked theme.fg() into Text at construction, so a
warning shown while the auto-theme default guess was dark kept the
dark-mode color after async appearance detection switched the active
theme to light — dark-catppuccin Mocha yellow on the light Latte
background (1.12:1 contrast, effectively invisible).
Add Text.setStyleFn(), a foreground styler evaluated at render time, and
route the transient status presenters through it. Because the coding
agent invalidates status components on onThemeChange, a theme swap now
re-shapes them against the live theme instead of replaying the palette
active when they were constructed.
Fixes#6337