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
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
- Added a public `submit()` method to the `Editor` class to trigger form submission.
- Updated the `EditorComponent` interface to expose an optional `submit()` method.
- 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.
Detected slash skill lookup after existing prompt text and filtered the popup to skill commands only.
Replaced the whole draft when a mid-prompt skill completion is accepted so the submitted prompt invokes /skill:<name>.
Fixes#3654
- Added support for `<code>`, `<hr>`, and `<blockquote>` tags to the markdown renderer.
- Improved HTML entity decoding and tag handling logic for consistent terminal output.
- Refactored internal block parsing methods to facilitate specialized styling for new HTML elements.
- Verified new rendering behaviors and entity handling through updated test suites.
When the end-of-line cursor glyph lands past a wide trailing grapheme
(CJK comma ',', emoji, etc.), the inverse-video cursor extends past
'lineContentWidth' by the full overflow. Prior cursorInPadding logic
dropped exactly one space of right padding but always re-emitted '─'
before '╯', so 'paddingX=1' overflow=1 stayed 1 cell too wide and
'paddingX=2' overflow=2 stayed 2 cells too wide. In the user's
terminal the '╯' wrapped to a new row, looking like the editor box
collapsed onto two lines.
Replaced the boolean with the actual overflow cell count and recomputed
the right chrome budget (padding + optional '─' + corner) from
'paddingX + 1 - overflow', floored at 1 so the corner/vertical bar
survives. The else branch (non-last lines) follows the same budget for
symmetry.
Fixes#3431
Add routeSgrMouseInput() and routeSelectListMouse() to centralize the
repeated SGR parse + SelectList hit-test/hover/wheel/click patterns, and
make SelectList implement MouseRoutable via routeMouse(). The structural
SelectListMouseTarget interface keeps mouse.ts free of component imports.
Extend the emergency-restore tests to assert mouse-off sequences
(1006l/1003l/1000l) are emitted while keeping the alt-screen 1049 gating.
- Implement `handleWheelAt` to restrict wheel scrolling to the settings pane.
- Update selection movement to support clamping when scrolling at boundaries.
- Add tests to verify boundary behavior and spatial constraints.
- Stripped `<span>` and `<text>` tags from rendered markdown while preserving their contents.
- Updated HTML entity normalization to handle common special characters and numeric codes within the TUI.
- Ensured whitespace surrounding stripped inline tags is preserved, preventing content merging.
Preserved the leading whitespace in the suggestion prefix and routed both Enter paths in editor.ts through findLeadingSlashCommandStart so the popup apply and sync fallback no longer bail out on ' /sk'.
Fixes#3095
- Added `BoxBorder` interface and optional border support to the `Box` component.
- Implemented `setBorder()` to allow dynamic toggling of box outlines.
- Added intelligent border dropping logic to prevent overflows in narrow containers.
- Added comprehensive unit tests for rendering, styling, and boundary conditions.
- Implemented `customHr` tokenizer to support varied Unicode horizontal rule characters.
- Added logic to intelligently map divider characters to theme-appropriate symbols.
- Updated theme object ID tracking to use `WeakMap` for better handle integrity.
Pretty-printed HTML lists (e.g. <ul>\n <li>...) leaked source
indentation before bullets and inserted blank rows between items
because whitespace-only text between block-level tags was appended
literally. Skip insignificant formatting whitespace and pin it with
a regression test.
- Centralized decoding of tmux's `extended-keys` control sequences (`csi-u` and `xterm` formats) into a shared utility.
- Updated the multi-line editor and single-line input to use this decoder, preventing escape tails (e.g., `[27;5;106~`) from leaking into the text buffer.
- Added parity to the single-line input by explicitly stripping any remaining control characters after decoding to ensure data integrity.
- Added new `tui.tight` setting with default `false` and documented behavior.
- Added global tight-mode APIs and `setIgnoreTight` propagation across Box, Text, and Markdown.
- Handled `tui.tight` updates to refresh UI borders and trigger rerendering.
- Updated key message components to ignore tight mode selectively and preserve intended spacing.
- Adjusted image budget IDs to use a shared 24-bit seed and wrap safely so new budgets no longer collide.
- Added transmit-state reset and changed image output ordering so full paints can replay image data after terminal clears.
- Updated image and protocol tests to match the new wrapped-id behavior and cursor-save/restore-wrapped transmit sequences.
- Added cursor save/restore escape handling around direct image emission.
- Replaced the previous up-and-down cursor movement sequence with restore logic to keep renderer output positioned correctly.
- Ensured direct image replay path no longer depends on moving back down after placement to align the cursor.
- Added Matplotlib figure PNG rendering and display tracking in Python runner to emit PNG output immediately when figures are displayed via display(fig).
- Extended session persistence to externalize oversized image payloads in both content and details.images, enabling tool result images to survive session reload.
- Enhanced session loader to resolve image data payloads and blob references across content and details.images during session reconstruction.
- Added image cache invalidation in TUI image component when image protocol, cell dimensions, or Kitty Unicode placeholder mode changes.
- Added comprehensive test coverage for Matplotlib display, image persistence across reload, and TUI image rendering with protocol and dimension changes.
Restored the cursor below direct inline-image placements after emitting the graphics sequence so following chat rows cannot overwrite the reserved image area. Added coverage for the placement row terminator.\n\nFixes #2863
- Added latexToBlock module that renders stacked fraction math with baseline-aware rows.
- Updated markdown rendering to route display math through latexToBlock with sole-display handling.
- Exported latexToBlock from the package entrypoint and updated changelog docs accordingly.
- Added advisor resume and suppression repro tests in coding-agent for interrupted turn recovery.
- Added own-line display-math tokenizers for `$$...$$` and `\[...]` markdown blocks.
- Added delimiter-free `\begin...\end` math parsing in text conversion with multiline row preservation.
- Added ANSI rendering for `\textcolor`, `\color`, `\colorbox`, and `\fcolorbox` with scoped color resets.
- Added tests for LaTeX color and math parsing edge cases, including inline code and nested scopes.
- Added LaTeX math and Mermaid allowances in terminal and final-chat prompts.
- Added inline math tokenization in TUI for $, $$, \(\), and \[\].
- Added LaTeX-to-Unicode conversion helpers and exports for math rendering.
- Fixed inline math detection to skip escaped dollars and currency-like spans.
- Removed render_mermaid from tool discovery, task definitions, and registries.
- Removed renderMermaid setting and prompt/docs references tied to the deleted tool.
- Added maxWidth and theme color options to Mermaid ASCII resolution in markdown flow.
- Re-rendered Mermaid ASCII in both directions and clipped output to available width.
Throttled animated loader message-only repaints when synchronized output is unavailable so unsupported terminals only repaint on spinner frame changes.
Fixes#2771
- Added unified `omp setup speech` flow with JSON/check modes and model picker.
- Added local STT pipeline with sherpa workers, recorder/download flow, and streaming inference.
- Added local TTS pipeline with `omp say`, backend selection, and streaming vocalization.
- Replaced legacy speech settings with unified `speech`/`speechgen` configuration keys.
- Added a space-hold gesture state machine in CustomEditor, tracking repeated spaces, detecting holds beyond SPACE_HOLD_THRESHOLD, and firing start/end callbacks via a release timer.
- Hooked editor space-hold callbacks in InputController so STT toggles on hold start and again on release when STT is enabled.
- Added tests for space-hold start/stop behavior and updated keybinding docs to describe the hold-to-record STT workflow.
- Added `paste.largeMenuThreshold` setting with default 100 and values 0/100/250/500/1000.
- Added `Editor.onLargePaste` and `Editor.insertPaste` to intercept oversized pastes and expand markers.
- Added threshold-based large-paste routing to show the menu and wrap oversized content for code/XML or attachment fallback.
- Added tests for large-paste interception, fallback behavior, short-paste handling, and marker expansion.
Always-on LoopWatchdog (armed in TUI.start/stop) logs ui.loop-blocked with blockedMs and the current loop phase on the rising edge of a late probe tick. New pushLoopPhase/popLoopPhase/currentLoopPhase stack in pi-utils feeds it; breadcrumbs at in-process subagent dispatch (subagent:<id>) and the SelectList fuzzy filter (ui.select-filter) attribute residual main-thread stalls.
The whole-line editor decorate ran on `displayText` after the editor appended
the zero-width CURSOR_MARKER and cursor glyph; both start with ESC, so the
magic-keyword regex's right-boundary `(?!\S)` rejected `ultrathink` glued to
the marker and dropped the gradient until a trailing character was typed.
- `Editor.#decorate` (pi-tui) now splits around CURSOR_MARKER and decorates
each user-text segment independently, so word-boundary lookarounds resolve
correctly on both sides; the matching comment block is corrected.
- `KeywordHighlighter` / `highlightMagicKeywords` gain an optional `phase` in
[0, 1) that cyclically rotates the gradient stops. `0` (default) yields
the static palette, so sent-bubble rendering is unaffected.
- `CustomEditor.decorateText` derives `phase` from `Date.now()` and chains
`setTimeout(SHIMMER_FRAME_MS)` ticks while focused, the buffer holds a
magic keyword, and `magicKeywords.enabled` is on — the render itself
schedules the next frame, so losing focus, deleting the keyword, or
flipping the setting stops the animation on its own. `interactive-mode`
wires the repaint hook to `requestComponentRender(editor)` on construction
and after `setEditorComponent`.
- Adds `hasMagicKeyword(text)` (cheap prose-aware probe) and tests for the
seam fix, the phase cycle, the gating, and timer cleanup.
Fixes#2475
- ImageBudget gained a stable partial-pass mode that replayed the last committed suppressed-image id set during beginPass(true), avoiding reordered call-order decisions.
- TUI resize viewport painting switched to beginPass(true) and stopped calling endPass so tail-only drag repaints no longer mutated image demotion state.
- An ImageBudget test now verifies stable partial passes preserved live/text decisions by id and did not trigger purge or reset in the next full pass.
- Recomputed `tools.discoveryMode: "auto"` in the deferred MCP closure in `sdk.ts` once the real tool count is known: a toolset crossing the threshold now flips discovery on, registers and activates `search_tool_bm25`, and skips `activateAll` instead of force-activating every MCP tool.
- Guarded the deferred MCP task against disposed sessions: added `AgentSession.isDisposed` and `enableMCPDiscovery()`, and the late connect now calls `disconnectAll()` instead of refreshing tools onto a dead session.
- Cleared `#fastPathKey`/`#fastPathItems` in `AssistantMessageComponent.invalidate()` so theme/symbol changes rebuild reused Markdown children instead of keeping stale captured themes.
- Memoized unusable read summaries as a `false` sentinel in `read.ts` so the per-session LRU no longer retains full sources of unsummarizable files.
- Broadened `HAS_REF_DEF` in `markdown.ts` to match backslash-escaped reference labels (`[a\]b]: x`) and cleared frozen stream-lex state on blank `setText()`.
- Added regression tests: deferred auto-discovery flip and mid-connect dispose (`sdk-mcp-auto-discovery.test.ts` + `many-tools-mcp.ts` fixture), fast-path child rebuild on invalidate, and escaped-ref-def incremental-lex equivalence.
- Replaced global search query mutation logic with a dedicated `Input` instance so typing now supports cursor movement, word deletion, and other editor hotkeys.
- Updated search banner rendering to display the embedded input line with live cursor while preserving match-count formatting and prompt width handling.
- Extended `Input` with a configurable prompt and end-of-value cursor placement, then updated memory-search tests for the new hotkey-driven editor behavior.
- Added `SettingsList` section-focus state with `sectionFocused`, `hasSectionFocusTargets`, and `toggleSectionFocus()`.
- Implemented section-heading navigation with Tab/Shift+Tab, Up/Down focus jumps, and Enter/Escape exits.
- Updated `/settings` input hints and fallback routing to tab-bar keys when section focus is unavailable.
- Added tests for section-focus navigation, mouse parsing, and tab-bar behavior coverage.
- Added mouse hover, wheel, and click routing for `/settings` rows and tabs.
- Changed `/settings` to open as a fullscreen alternate-screen overlay.
- Added hover, hit-testing, and wheel APIs across TabBar, SelectList, and SettingsList.
- Added SGR mouse parsing plus `routeMouse` dispatch and exported it from the `tui` entrypoint.
- Added `SettingsListOptions` to `SettingsList` with a `layout` override ("auto" sidebar vs "flat" inline headings) and a `typeToSearch: false` switch for hosts that own filtering globally.
- Added `getSelectedItem()` and an `onSelectionChange` callback to `SettingsList`, replaced index-based submenu selection restore with id-based item resolution, and exported `getSettingItemFilterText` for external filtering.
- Padded short section panes to the full viewport in `SettingsList` so the panel height stays constant across sections.
- Added `muted` tabs to `TabBar` (skipped during keyboard navigation, styled via the new `TabBarTheme.mutedTab`), a `setTabs()` replace API that preserves the active tab by id without firing `onTabChange`, a `showHint` toggle for the trailing "(tab to cycle)" hint, and omission of the label prefix when the label is empty.