packages/tui/src/terminal.tsaccepts a host-owned stderr subscription and releases it on stop.packages/tui/src/stderr-observer.tsretains direct-stream observation for ordinary terminals without replacing a later writer during cleanup.
- A host can redirect stderr to a diagnostic log. Observing calls above that redirect falsely reports visible output and duplicates the working frame.
- Mouse geometry is invalidated inside the terminal, below extension components.
- Terminal construction, external-write observation and stop cleanup. Visible stdout/stderr must continue invalidating stale hit targets.
packages/tui/src/tui.ts: new module-scopeddefaultDiagnosticLogDirectory()andrenderErrorLogDirectory.TuiBase's constructor publishes its resolvedlogDirectoryinto that module scope, andlogRenderErrorOnce()writes to it instead of re-derivingos.homedir()/.senpi/agent. The default when no host directory is supplied is unchanged.packages/tui/test/render-contract.test.ts: a throwing child rendered by a TUI constructed with an explicit log directory writessenpi-debug.loginto that directory and not into aHOME-derived path.
logRenderErrorOnce()is reached fromContainer.render(), which has no TUI instance, so the path was hardcoded fromos.homedir(). Hosts already pass a resolved agent directory (logDirectory), andpi-debug.logalready honours it; only the render-error diagnostic did not.- Under a non-default brand the resolved agent directory is not
~/.senpi/agent, so the only record of a component that throws every frame landed in a directory the operator never reads. - Suites that quarantine the agent directory but not
HOMEappend to the developer's real~/.senpi/agent/senpi-debug.log; a single run of the coding-agent progressive-transcript suite was measured growing that file from 19,650 to 19,744 bytes. That leak is what led here, but it is not fixed by this change: that suite renders a container directly and never constructs aTuiBase, so no host directory is published and the fallback still resolves fromHOME. Re-measured with this change applied, the same run still grew the file (19,744 -> 19,838). Closing it belongs to the coding-agent test setup, which must quarantineHOMEthe way it already quarantines the agent directory.
- Render containment and its diagnostic live inside
packages/tui's render path; an extension cannot reachContainer.render()'s catch branch or the module-scoped logger.
packages/tui/src/tui.ts: the module-scoped diagnostic state block nearDIAGNOSTIC_LOG_MODE, thelogRenderErrorOnce()body, and theTuiBaseconstructor'slogDirectoryassignment.
packages/tui/src/tui.ts: newcanReceiveKeys()export;resolveMouseFocusTarget()returnsComponent | nulland resolves a clicked component that cannot receive keys to the deepest mounted ancestor that can, or tonull; new privatefindKeyFocusOwner().packages/tui/src/tui-main-screen.ts:applyMouseResultskips a null focus owner, and the release branch only re-applies the click target's focus when the click handler left focus untouched.packages/tui/src/tui-alt-screen.ts: same null handling inapplyMouseDispatchResult(newapplyFocusparameter) and the same click-handler precedence inhandleMouseEvent.packages/tui/test/tui-alt-screen.test.ts: the mouse-aware control keeps capture and drag routing, and the keyboard owner keeps focus. The previous expectation parked focus on a control with nohandleInput, which is the defect this entry fixes.
- A clickable row (
MouseRegion) or tab strip has nohandleInput. Focusing it madehandleTerminalInputdrop every later keystroke, so answering an ask-user question with the mouse silently killed typing while output kept flowing. - The release branch applied the click target's focus after the click handler ran, so the composer focus restored by an ask-user submit was immediately overwritten.
- Resolving at the renderer keeps every clickable surface correct without each call site opting in; a per-component opt-in missed the tab strip, which does not use
MouseRegion.
- Mouse focus ownership is renderer state inside
packages/tui; an extension cannot reorder focus application around click dispatch.
packages/tui/src/tui.ts:resolveMouseFocusTargetsignature and return type.packages/tui/src/tui-main-screen.tsandpackages/tui/src/tui-alt-screen.ts: the click branches of their mouse handlers.
packages/tui/src/native-module-path.ts: resolve the installed TUI entry withimport.meta.resolve, fall back tomoduleRequire.resolve, and accept the package-anchored candidate only when the entry is absolute.
packages/tui/src/native-module-path.ts: Bun can return the bare package specifier fromrequire.resolveinside an esbuild chunk. The resulting relative candidate cannot load the native helper, so Ctrl+V silently reads an empty clipboard.
packages/tui/src/native-module-path.ts: native helper discovery belongs to the TUI package, below extension clipboard handling.
packages/tui/src/native-module-path.ts: package resolution and its first candidate. The module-directory and executable-directory fallbacks retain their order.
packages/tui/src/dollar-invocation-autocomplete.ts:getDollarInvocationContextcompletes any$queryat a whitespace boundary regardless of earlier$tokens; slash commands are offered only while the token is the first thing in the prompt; an exact known-skill token closes the popup. NewfindDollarSkillMentions(line, knownSkills)andknownSkillNames(commands).packages/tui/src/autocomplete.ts:AutocompleteProvider.getMentionRanges?(line)andMentionRange;CombinedAutocompleteProviderimplements it from itsskill:commands.packages/tui/src/components/editor.ts:LayoutLinecarrieslogicalLine/startIndex;EditorTheme.mention?styles resolved mention ranges. Row composition moved topackages/tui/src/components/editor-line-render.ts(renderEditorLine), which styles the cursor grapheme and each mention fragment separately so the cursor's SGR reset cannot bleed into a mention.
- senpi#1778: after one
$skillthe popup no longer opened for a later$, and a resolved mention was indistinguishable from prose.
- The editor owns row composition and the popup trigger policy.
- MEDIUM:
editor.tsrender()cursor branch (replaced byrenderEditorLine) and thelayoutTextpushes; LOW:autocomplete.tsinterface.
packages/tui/src/terminal.tsselects an injectable tmux CLI cursor source when TMUX_PANE is set.packages/tui/src/tmux-cursor-query.tsaccepts only two matching pane-relative numeric readings at least 10 ms apart within the existing 750 ms total budget. The private query is still written; its replies cannot override the tmux source.
packages/tui/src/terminal.ts: tmux 3.7b swallows private DECXCPR, leaving fresh short frames unclickable. Errors, malformed output, movement and timeout still leave placement unknown; timeout remains restart-only recovery.
packages/tui/src/terminal.tsowns the cursor broker and renderer calibration lifecycle, below extension input handling. Bare CPR remains forbidden.
packages/tui/src/terminal.ts: constructor options, pending query state, issue/settlement and private-response interception. Outside tmux the private protocol bytes are unchanged. The existing large terminal module is not refactored; the new source is below 250 pure LOC.
packages/tui/src/terminal.ts: adds the private DECXCPR broker, two/three-parameter response interception, shared in-flight promises, bounded timeout, late-fragment discard, and non-suppressing external-write observation. A timed-out broker stays fail-closed until restart because CPR has no request identifiers.packages/tui/src/tui.ts: calibrates short frames against a matching committed placement/cursor snapshot and invalidates placement on external stdout/stderr writes. The pending-wrap CPR column just beyond the right margin is accepted.packages/tui/src/tui-main-screen.ts: after external output, appends a fresh working frame before recalibration rather than guessing the old frame position from a moved cursor. Existing output and scrollback are not cleared.packages/tui/src/index.ts: exports the cursor-position result type; custom terminals may omit the optional query/observation methods.
packages/tui/src/terminal.tsandpackages/tui/src/index.ts: private replies avoid collisions with modified function keys, while custom terminal implementations remain usable without CPR support.packages/tui/src/tui.tsandpackages/tui/src/tui-main-screen.ts: real-PTY QA showed that recalibrating an unchanged old frame after a stderr newline mapped a blank row onto an option. A fresh committed frame is necessary before its cursor can identify its origin.
packages/tui/src/terminal.ts,packages/tui/src/tui.ts,packages/tui/src/tui-main-screen.ts, andpackages/tui/src/index.ts: terminal negotiation, write ownership, hardware cursor snapshots, and committed renderer geometry are below extension APIs.
packages/tui/src/terminal.ts: keyboard-negotiation interception, stdout guard, stderr observer, and lifecycle cleanup.packages/tui/src/tui.ts: additive calibration members and stop cleanup; no terminal-input handler changes.packages/tui/src/tui-main-screen.ts: fork-owned post-output append recovery and committed-frame calibration.packages/tui/src/index.ts: terminal result-type exports.
packages/tui/src/tui-main-screen.ts: consumes mouse input before extension listeners, enables click-only tracking for leases on supported terminals, and dispatches same-cell clicks against committed component and overlay geometry.packages/tui/src/tui.ts: exposes mounted mouse-layout roots to the fork-owned main-screen renderer. No terminal input handler changes.
packages/tui/src/tui-main-screen.ts: stale mouse reports must never reach the editor or extension input listeners; layout changes must cancel gestures rather than activate a replaced control.packages/tui/src/tui.ts: overlay identity participates in the same committed-layout check as root component identity.
packages/tui/src/tui-main-screen.tsandpackages/tui/src/tui.ts: the renderer owns terminal capture, input ordering, overlays, and committed hit geometry.
packages/tui/src/tui-main-screen.ts: fork-owned dispatch and tracking lifecycle.packages/tui/src/tui.ts: additive protected mouse-layout root accessor only.
packages/tui/src/tui.ts: adds idempotent capture leases, lifecycle blockers, placement epochs, and committed-frame anchors; unknown, stale, resized, and image-bearing frames cannot resolve mouse rows.
packages/tui/src/tui.ts: inline clicks need reliable frame placement without changing renderer defaults or taking permanent terminal ownership.
packages/tui/src/tui.ts: committed frame geometry, hardware cursor placement, and renderer lifecycle are private renderer state.
packages/tui/src/tui.ts: one fullRender hook, resize/replay/insert-scroll/multiplexer epoch increments, and stop bookkeeping. Terminal input routing is untouched.
packages/tui/src/tui-alt-screen.ts: four helper bodies delegate tomouse-input.ts; fullscreen selection, scrolling, search, and tracking bytes remain unchanged.packages/tui/src/index.ts: exports the shared parser, protocol constants, and click synthesizer.packages/tui/src/stdin-buffer.ts: retains incomplete owned SGR reports for at most 750 ms and 64 characters, discarding expired tails through a CSI terminator rather than leaking them into keyboard handling. A new escape boundary resynchronizes without stripping the next report's CSI prefix (pinned by an additional assertion-based RED/GREEN during final review).
packages/tui/src/tui-alt-screen.tsandpackages/tui/src/index.ts: regular-mode consumers need the same zero-based mouse protocol contract without duplicating private parsing.packages/tui/src/stdin-buffer.ts: timeout-flushed mouse fragments previously exposed protocol tails as typed text.
packages/tui/src/tui-alt-screen.ts,packages/tui/src/index.ts, andpackages/tui/src/stdin-buffer.ts: protocol framing and renderer-private helper ownership precede extension input dispatch.
packages/tui/src/tui-alt-screen.ts: helper delegations and one import; selection, scrollbar, and search logic are untouched.packages/tui/src/index.ts: mouse exports.packages/tui/src/stdin-buffer.ts: owned-fragment buffering and timeout flush.
packages/tui/src/latex.tsstays deleted; upstream f0592205f's seven relational-algebra join symbols now live inpackages/tui/src/components/latex.ts, pinned bytest/components-latex-relations.test.ts.packages/tui/src/tui.tskeepsPI_DEBUG_REDRAWand thepi-debug.logfilename instead of upstream c505f4c19'sPI_TUI_DEBUG_REDRAW/pi-tui-debug.logrename.packages/tui/src/tui.ts(TuiBase.logDirectory) keeps its~/.senpi/agentdefault, so over-wide crash dumps stay at<home>/.senpi/agent/senpi-crash.log; upstream'sos.tmpdir()fallback when no log directory is supplied is not adopted.packages/tui/src/components/loader.tsgains upstream's protectedgetRenderedIndicator()hook on top of the fork message/indicator formatters.
- The fork's debug and crash artifacts are documented under the
senpinames and read by the QA harness; renaming them would break existing evidence tooling for no user benefit. The LaTeX symbol table belongs with the living component module.
- Renderer logging paths, crash dumps, and component internals are not reachable from extensions.
src/tui.tsenv-var and log-filename constants,components/latex.tssymbol tables, andcomponents/loader.tsrender hooks.
packages/tui/src/dollar-invocation-autocomplete.ts: dollar skill lookup no longer rejects nonzero logical editor lines, so multiline drafts and queued message composition can request the same filtered skill suggestions.packages/tui/test/autocomplete-dollar.test.tsandpackages/tui/test/editor-dollar-autocomplete.test.ts: cover later-line provider lookup and paste/follow-up typing through the real Editor surface.
- The editor remains active while a response is streaming and while follow-up text is queued. A pasted or multiline draft can place the cursor on a later logical line, and the previous line-zero-only guard silently suppressed the skill picker there.
- Logical cursor routing and autocomplete request admission are owned by the standalone TUI Editor/provider path below the interactive extension API.
- LOW:
packages/tui/src/dollar-invocation-autocomplete.tscontext gate and the focused dollar/editor tests.
packages/tui/src/dollar-invocation-autocomplete.ts: dollar invocation lookup now resolves the token at the cursor after ordinary prompt text, while rejecting common shell variables and positional parameters before consulting the skill catalog. Existing leading skill chaining and trailing-space insertion remain unchanged.packages/tui/test/autocomplete.test.ts: adds provider coverage for the mid-line skill hint and canonical$skillinsertion.
- The Codex-style skill picker should appear when a user types
$in a valid prompt token, not only when the line consists entirely of a leading dollar invocation. Shell-like forms such as$HOMEand$1must remain literal.
- Dollar token extraction and completion arbitration run inside the standalone TUI autocomplete provider before interactive-mode extensions receive the editor event.
- LOW:
packages/tui/src/dollar-invocation-autocomplete.tsaround token extraction and shell-variable classification.
- packages/tui/package.json: build uses tsgo for the emitted workspace build.
- The native compiler reduces omob build time without changing runtime JavaScript.
- The package build manifest owns the compiler used by the fork's release pipeline.
- The
buildscript in packages/tui/package.json.
packages/tui/src/terminal-image.ts: adds an environment-based capability detection path (detectCapabilitiesFromEnvironmentwith a tmux client-termfeatures hyperlink probe, per-terminal classification including Zed, and conservative defaults for unknown terminals) used when the tmux probes are unavailable, honors thePI_HYPERLINKS,PI_IMAGE_PROTOCOL, andPI_TRUE_COLORoverrides, and merges stored overrides ingetCapabilities;setCapabilityOverridesreplaces the overrides and resets the cache (upstream e86823096 #8665 and 649214477 #8828).packages/tui/src/index.ts: exportssetCapabilityOverrides.
- Auto-detection misfires on unknown, multiplexed, or non-queried terminals; explicit overrides give users and branded distributions (the fork bridges the
SENPI_*names through settings) a deterministic way to force hyperlink, image-protocol, and truecolor behavior.
- Capability detection and caching run inside the TUI package before components render; no extension seam sits between environment detection and the cached capabilities.
- MEDIUM:
packages/tui/src/terminal-image.tsdetection, environment fallback, and override merging; LOW:packages/tui/src/index.tsexport list.
packages/tui/src/terminal.ts:refreshTerminalDimensionswraps the self-directed SIGWINCH in a try/catch that skips the refresh on failure, andProcessTerminal.startuses it, so environments whose seccomp or LSM policies denykillfor the process no longer crash startup (upstream 605a1b038, #8898).
- The dimensions refresh after suspend/resume is best-effort; a policy-restricted signal threw during terminal start and aborted the whole TUI for a purely cosmetic refresh.
- The signal is sent from
ProcessTerminalconstruction, below every extension hook.
- LOW:
packages/tui/src/terminal.tsrefreshTerminalDimensionsand its call site inProcessTerminal.start.
packages/tui/src/autocomplete.ts: fuzzy file completion merges a depth-1 listing of the base directory ahead of the recursive fd results (deduplicated),walkDirectoryWithFdaccepts amaxDepth, and equal-score results tie-break by shallower depth, then shorter path, then locale order (upstream b37ebb7f2, #8669).
- Nested results previously interleaved unpredictably when scores tied; shallow matches first mirrors shell completion expectations and makes the ordering deterministic.
- The fd-backed provider internals score and order suggestions inside the TUI package.
- LOW:
packages/tui/src/autocomplete.tssuggestion merge and sort comparators.
packages/tui/src/tui-alt-screen.ts: adds acopyOnSelectoption (default true) with getter and setter, gates mouse-release auto-copy on it, joins slash and hyphen word segments during selection so paths and kebab-case tokens stay whole, and factors outgetActiveSelectionText,copyActiveSelectionToClipboard, andhasActiveSelectionfor keyboard-driven copy (upstream 4e4949299 #8731 and 1ac6128e6 #8676).
- Fullscreen mode owns mouse selection, so it must mirror terminal word-selection behavior itself, and hosting modes need a seam to disable auto-copy and drive copying from a keybinding instead.
- Alt-screen viewport mouse handling and clipboard writes are TUI-internal.
- MEDIUM:
packages/tui/src/tui-alt-screen.tsword-selection joining and the mouse-release copy path.
packages/tui/src/components/editor.tsandpackages/tui/src/terminal.tsretain the current upstream terminal input behavior after synchronizing main.
- The PR merge must preserve both the upstream terminal changes and the callback lifecycle repair.
- Terminal input normalization and editor dispatch are owned by the TUI runtime.
packages/tui/src/components/editor.tsandpackages/tui/src/terminal.ts.
packages/tui/src/components/editor.tsapplies the existing CSI-u Shift+Enter sequence to a standalone LF only while the multiline editor handles it.ProcessTerminalforwards raw input, so single-line inputs and selectors keep their existing Enter behavior. The conversion is limited to Linux sessions where both Warp and non-empty WSL markers are present; plain CR Enter, non-Warp terminals, non-WSL sessions, SSH/multiplexer sessions, and bracketed paste payloads keep their existing input bytes.packages/tui/src/mux.ts:isMultiplexerSession()accepts an optional environment so terminal normalization reuses the shared tmux, GNU Screen, and Zellij detection without process-global test setup.packages/tui/test/terminal.test.ts: focused coverage proves both supported Warp/WSL environment markers, hardened platform/marker boundaries, and raw forwarding for non-editor consumers.
- Warp documents that its terminal sends Shift+Enter as LF (
0x0a). In Senpi's legacy keyboard path, that byte must also remain recognizable as Enter for terminals that send LF for plain Enter, so the editor's submit binding wins before the Ctrl+J/newline binding. Normalizing only direct local Warp-on-WSL sessions restores an unambiguous Shift+Enter identity while Warp's plain CR Enter continues to submit. SSH and multiplexer sessions are excluded because their active client terminal can differ from the inherited process environment. The editor-only boundary prevents this compatibility workaround from changing submission semantics for other focused TUI components. - See Warp #13782 for the terminal byte behavior.
- Coding-agent extensions can transform raw input through
onTerminalInput, but that hook cannot correct the sharedProcessTerminalsemantics for other TUI consumers or guarantee the default behavior without optional extension loading. The terminal layer is the single cross-consumer seam.
- LOW:
packages/tui/src/terminal.tsatforwardInputSequence()and its normalization helpers,packages/tui/src/mux.tsat shared multiplexer detection, andpackages/tui/test/terminal.test.tsbeside the existing native Shift+Enter normalization coverage.
packages/tui/src/tui.ts: Windows full redraws continue to clear and repaint the visible screen, but no longer emitESC[3J, which deletes the user's terminal scrollback buffer on ConPTY. Non-Windows non-multiplexer redraws retain their existing scrollback-clearing behavior.
- Windows Terminal's ConPTY resize and focus transitions can trigger a full redraw outside a multiplexer. Clearing scrollback is destructive and makes prior session output unrecoverable when the user returns to the terminal window.
- The platform-specific redraw guard belongs in the TUI renderer's
fullRender()path, where screen clearing and scrollback deletion are emitted together.
- LOW:
packages/tui/src/tui.tsaroundTuiBase.doRender()and thefullRender()scrollback-clear guard. - LOW:
packages/tui/test/mux-scrollback.test.tsaround resize scrollback emission assertions.
packages/tui/src/terminal.ts:isDeadTerminalError()gains a third, last-resort branch that parses a trailingerrno: <n>out of the error message. It fires only when that number is a dead-terminal errno that is stable across darwin and linux —EIO(5) andEPIPE(32).ENOTCONNis deliberately left out of the numeric set because its value differs per platform (57 on darwin, 107 on linux). The existing stringcodeand numericerrnobranches are unchanged and still win first, and any error that matches none of the three branches still propagates out ofProcessTerminal.stop().test/terminal.test.tspins the real Bun shape (a barenew Error("setRawMode failed with errno: 5")with neithercodenorerrno), theerrno: 32message form, and two rethrow fences: an unrelatednew Error("boom")and a live-but-unrelatederrno: 22message.
- Bun 1.4.0's tty shim throws a plain
Errorfor a failedsetRawMode()ioctl:codeanderrnoare both absent and the number survives only in the message text (verified locally:{"isError":true,"hasCode":false,"hasErrno":false,"msg":"setRawMode failed with errno: 5"}). The previous classifier recognized only the two property shapes, so on a dead SSH/PTY peer the exception escapedProcessTerminal.stop()intoTui.stop()andstopInteractiveTui(), aborting shutdown and hanging the session witherror: setRawMode failed with errno: 5.
- Raw-mode ownership and teardown are private
ProcessTerminallifecycle responsibilities running inside the shutdown path. No extension surface sits between the saved raw-mode state and the stdin ioctl, so the classification has to happen where the throw occurs.
- LOW:
packages/tui/src/terminal.tsaround the dead-terminal errno constants and theisDeadTerminalError()body. - LOW:
packages/tui/test/terminal.test.tsaround theProcessTerminal stopsuite.
packages/tui/src/terminal.tsarms aprocess.stdin"error" guard fromProcessTerminal.start()until a 250ms grace window afterstop(): a vanished or re-backgrounded controlling terminal fails the next stdin read with EIO, and without a listener the EventEmitter rethrew it as an uncaught exception that killed the agent process. The classifier owns EIO only — Node'scode: "EIO"and Bun's rawerrno: 5/-5shapes — and every other stdin error keeps its default EventEmitter propagation. EIO is swallowed without pausing the stream, so a pgrp that regains the tty foreground keeps accepting input.
- When omo's launcher chain dies (e.g. external SIGTERM), the orphaned engine's pending stdin read on the now-background tty fails with EIO and crashed the process through
uncaughtException("exiting due to uncaughtException: EIO read"). The same hazard was fixed upstream-style in gajae #3758; this port adapts it to the fork'sProcessTerminaland adds the numeric-errno shape from the shutdown-time classifier.
- The crash topology (launcher chain + orphaned engine) and the Bun runtime shim are fork-owned; the fork's terminal lifecycle differs from upstream's.
ProcessTerminal.start()/stop()inpackages/tui/src/terminal.tsduring upstream syncs.
packages/tui/src/components/markdown.tskeeps the fork LaTeX pipeline (latex_block/latex_inline/latex_literaltoken kinds,latexToUnicode, formula length caps, and word-boundary guards) on top of upstream's renderer.packages/tui/src/terminal.tskeeps dead-terminal detection (EIO/EPIPE/ENOTCONN plus Bun's raw errno-5 macOS tty shim) and thePI_TUI_KEYBOARD_PROTOCOLenhancement gate.
These are fork-owned product surfaces (senpi branding, provider wire behavior, fork runtime features) that upstream does not carry; the sync must re-assert them on top of upstream's tree.
The divergence lives in core wiring, package identity, or build plumbing that executes before any extension loads, so no extension hook can express it.
- The token-scanner section of
packages/tui/src/components/markdown.tsand the raw-mode setup inpackages/tui/src/terminal.ts.
packages/tui/src/tui-alt-screen.ts: re-diverges from upstream59a71b235dby exactly one identifier. The private teardown helper staysdeleteAltScreenKittyImages()(upstream calls itdeleteKittyImages()), and both call sites keep the fork name: thestop()synchronized-output teardown sequence and the full-clear branch that falls back to it when no Kitty placements were uploaded. The emitted escape bytes are byte-identical to upstream in every branch.
- The fork's alt-screen class shares a file-scope namespace with the module-level Kitty helpers
imported from
terminal-image.ts(deleteAllKittyImages,deleteAllKittyPlacements). The alt-screen-scoped name states which of the two deletion semantics the method wraps, so a reader resolving the full-clear branch does not have to check whetherdeleteKittyImagesis the imported protocol helper or the class method that gates it onimageProtocol === "kitty".
TuiAltScreenteardown and its full-clear frame construction are private renderer internals that emit terminal bytes directly; no extension surface exists between the class and the terminal.
- LOW:
packages/tui/src/tui-alt-screen.ts— thestop()teardown write, the private helper declaration, and theclearImagesternary in the full-clear path. Upstream edits to the same three hunks resolve by keeping the fork identifier and taking upstream's byte content.
packages/tui/src/components/editor.ts:insertImageMarker()renumbers the visible markers to canonical 1..k in reading order (viaImageMarkerRegistry.canonicalize, previously dead code) and returns the marker's FINAL canonical id instead of the insertion counter;setText()canonicalizes after pruning so a surviving high id displays as[Image #1];EditorSnapshotcarries an opaqueattachmentStatecaptured through the new owner hooks andundo()restores it BEFORE firing the marker-order notification; cursor position is preserved across the renumbering rewrite.packages/tui/src/editor-component.ts: new optional pairedsnapshotAttachmentState/restoreAttachmentStatecontract next toonImageMarkersChanged, documented together with the tightenedinsertImageMarkerid semantics.- Regression coverage:
test/editor-image-marker.test.tspins out-of-order insert canonicalization, post-prune renumbering, and multi-marker delete+undo payload restoration.
- The insertion counter only produces reading-order numbers when the cursor
sits after every existing marker, so pasting in front of one displayed
[Image #2][Image #1]; the owner's reconcile-by-position then mispaired or destroyed payloads. Undo restored marker text and registry ids but the payloads live with the owner, so a delete+undo permanently lost the deleted marker's image.
- The marker registry, undo stack, and the id semantics of
insertImageMarkerareEditorinternals below the component contract; extensions cannot renumber marker text or hook the undo pop.
- MEDIUM:
insertImageMarker()and the undo snapshot/restore block inpackages/tui/src/components/editor.ts. - LOW: the image-marker section of
packages/tui/src/editor-component.ts.
Repository-wide changes.md audit backfill for renderer, terminal, and component surfaces (2026-08-17)
- Backfill from the repository-wide changes.md audit (pin
914cf147, tag v0.84.2): this entry names every upstream-owned TUI production path that still diverges from the pinned upstream tree, so the next upstream sync can resolve each file's fork intent. Behavioral history for most paths lives in the dated sections of this file; the entries added by this backfill carry the rest. - Renderer core:
packages/tui/src/tui.tsholds the fork's differential renderer inTuiBase— synchronized autowrap-guarded frames, viewport-bounded normalize/diff, scrollback replay, the insert-scroll fast path, the configurable render fps cap, over-wide containment, the componentdispose()contract, and mode-gated tmux focus routing (see the focus-routing entry below plus the 2026-08-14, 2026-07-31, 2026-07-04, 2026-07-03, and 2026-07-02 sections).packages/tui/src/tui-main-screen.tsis reduced to a thin main-screen subclass that owns render-state capture/restore;packages/tui/src/tui-alt-screen.tsdiffers from the pin only by thedeleteAltScreenKittyImages()teardown rename (its focus, clipboard, and mouse-release behavior is upstream v0.84.2 parity, delivered by PR #892). - Terminal I/O:
packages/tui/src/terminal.ts(external stdout guard while started, control-stripped OSC 0 titles, best-effort raw-mode restoration on dead terminals),packages/tui/src/stdin-buffer.ts(stateful UTF-8 reassembly of split multibyte chunks), andpackages/tui/src/terminal-image.ts(Kitty graphics through tmux allow-passthrough, Unicode placeholder placement, tmux-reported cell dimensions). - Components and primitives:
packages/tui/src/components/box.ts(disposal contract),packages/tui/src/components/editor.ts(paste-marker registry with provenance, atomic cursor discipline, autocomplete trigger characters),packages/tui/src/components/image.ts(per-row Kitty placeholder lines),packages/tui/src/components/loader.ts(messageFormatteranimation plusdispose()),packages/tui/src/components/markdown.ts(LaTeX tokenizers and the bounded highlight cache),packages/tui/src/components/select-list.ts(therenderRowtheme composer),packages/tui/src/autocomplete.ts(mixed$//invocation picker and skill-namespace filtering),packages/tui/src/editor-component.ts(the paired paste-state API),packages/tui/src/fuzzy.ts(hot-path scoring and alphanumeric swap variants),packages/tui/src/utils.ts(two-generation width cache, terminal-output normalization, thecoalesceAdjacentSgrutility), andpackages/tui/src/index.ts(the fork export surface: paste markers, select-list row types, tmux helpers, markdown cache controls). packages/tui/src/latex.tsis the upstream LaTeX module path, deleted in this fork: the converter was rewritten dependency-free and relocated topackages/tui/src/components/latex.ts(see the relocation entry below).
- Merges resolve tracker files to
ours, so every divergent upstream-owned path needs an entry in its exact nearest tracker that names it; without this inventory the divergence is invisible to the audit and to the next sync.
- These paths are the renderer, terminal-protocol, and primitive layer itself: frame bytes, stdin framing, capability probes, paste registries, and package exports sit below the extension API that would otherwise carry such behavior.
- HIGH:
packages/tui/src/tui.ts(TuiBaserender paths, scheduler, dispose, focus routing) andpackages/tui/src/tui-main-screen.ts(the thin-subclass split itself). - MEDIUM:
packages/tui/src/components/editor.ts,packages/tui/src/components/markdown.ts,packages/tui/src/terminal-image.ts,packages/tui/src/terminal.ts, andpackages/tui/src/utils.ts. - LOW:
packages/tui/src/components/box.ts,packages/tui/src/components/image.ts,packages/tui/src/components/loader.ts,packages/tui/src/components/select-list.ts,packages/tui/src/autocomplete.ts,packages/tui/src/editor-component.ts,packages/tui/src/fuzzy.ts,packages/tui/src/stdin-buffer.ts,packages/tui/src/tui-alt-screen.ts, and thepackages/tui/src/index.tsexport lists;packages/tui/src/latex.tsis a whole-file deletion to reconcile againstpackages/tui/src/components/latex.ts.
Landed 2026-06-17 (commit 4f6749bb7).
packages/tui/src/tui.ts:Componentdeclares optionaldispose?()andContainerimplements tree-wide disposal —dispose()runs once (guarded by adisposedflag),clear()disposes the children it removes,removeChild()disposes the removed child, anddetachAll()detaches without disposing for callers that reuse components.packages/tui/src/components/box.ts: the same contract locally —clear()andremoveChild()dispose affected children,dispose()is idempotent, anddetachAll()preserves the previous non-disposing clear semantics for cache-preserving reuse.packages/tui/src/components/loader.ts:dispose()stops the animation timer so a disposed loader cannot keep ticking.packages/tui/src/components/markdown.ts: the module-level syntax-highlight cache is bounded with insertion accounting, andclearRenderCache()plus highlight call counters are exported throughpackages/tui/src/index.tsfor teardown and tests.- Coverage:
packages/tui/test/component-dispose.test.tsandpackages/tui/test/markdown-highlight.test.ts.
- Resumed multi-thousand-entry sessions replace whole component subtrees; without a disposal contract, stale animation timers and unbounded module-level highlight caches accumulate for the process lifetime.
- Component lifecycle and module-level caches are TUI internals; extensions compose components but cannot inject tree-wide teardown or clear renderer-owned caches.
- LOW: the disposal methods in
packages/tui/src/components/box.tsandpackages/tui/src/components/loader.ts. - MEDIUM:
packages/tui/src/components/markdown.tscache accounting; LOW: itspackages/tui/src/index.tsre-exports. - LOW: the
Containermethod block inpackages/tui/src/tui.ts.
Landed 2026-07-26 (commit 8abee395c).
packages/tui/src/components/select-list.ts:SelectListThemegains optionalrenderRow, a composer receiving decomposedSelectListRowParts— selection prefix (withselectedPrefixalready applied), truncated primary, column-aligned description, and selection state — and taking over row composition. Without a composer, rendering funnels through one legacy branch that reproduces the previous composition operand-for-operand; the previously deadselectedPrefixcallback is now honored for selected prefixes.packages/tui/src/components/editor.ts: threads the composer through the existing theme plumbing without widening the public editor API.packages/tui/src/index.tsexportsSelectListRenderRowandSelectListRowParts.- Coverage:
packages/tui/test/select-list-render-row.test.ts,packages/tui/test/select-list-characterization.test.ts(byte-identical legacy output including truncation suffixes, column math, CJK widths, and the narrow-width path), andpackages/tui/test/editor-render-row.test.ts.
- Row composition was hard-coded (prefix, primary, and description wrapped in one
selectedText()call), which made it impossible to color a slash-command prefix independently of the selected-row background — the requirement the grok chrome's colored slash menu brought in.
- SelectList is the shared selector primitive consumed by editors and dialogs before any coding-agent extension UI hook runs; only the library can expose row decomposition.
- MEDIUM:
packages/tui/src/components/select-list.tsaroundcomposeRow()and the theme interface. - LOW: the theme plumbing in
packages/tui/src/components/editor.tsand thepackages/tui/src/index.tsexport list.
Landed 2026-06-08 (commit af0ab07a0).
packages/tui/src/fuzzy.ts:fuzzyMatchscoring moved from a per-call closure into a top-levelscoreMatch, and the per-character regex word-boundary test became char-code classification (isWordBoundaryPrefix). The whole-token letter/digit swap regex is generalized intobuildAlphanumericSwapQueries(): every adjacent letter/digit transposition plus whole-token swaps, each scored with the flatALPHANUMERIC_SWAP_PENALTY(5), best matching variant wins — so queries likegpt5amatchgpt-a5.- Exact-match priority and slash-separated filter tokens are upstream v0.84.2 behavior (in the pin) and are not fork deltas.
- Coverage:
packages/tui/test/fuzzy.test.tspins the adjacent-swap case.
- Selector filtering runs on every keystroke against large model registries; the closure allocation and per-character regex dominated the hot path, and single transposed alphanumerics previously failed to match.
fuzzyFilteris the ranking primitive inside the shared autocomplete and selector stack; extensions receive filtered lists and cannot replace the matcher.
- MEDIUM: scoring and swap-variant construction in
packages/tui/src/fuzzy.ts; upstream edits to the same functions will conflict textually.
Landed 2026-07-29 (commit 5655c1cd8).
packages/tui/src/latex.ts— the upstream-owned module path — no longer exists in this fork. The LaTeX converter was rewritten as the dependency-free, budgeted parser described in the 2026-07-29 "Native Unicode LaTeX in Markdown conversations" section and lives atpackages/tui/src/components/latex.ts, beside its only consumer, the Markdown tokenizers inpackages/tui/src/components/markdown.ts.packages/tui/src/index.tsno longer re-exportsrenderLatexfrom the old path; conversion is internal to the Markdown component (the paste-marker exports took that slot).
- The fork's converter is a deliberate rewrite (bounded nesting budgets, balanced parsing, fallback to literal text), not an edit of upstream's module. Keeping it beside its consumer matches the package layout, and recording the deleted upstream path maps the next sync's deletion to this entry instead of resurrecting upstream's module at
packages/tui/src/latex.ts.
- Math tokenization happens inside the Markdown component before extension-facing UI hooks; consistent rendering across every Markdown consumer requires the parser seam.
- The deleted
packages/tui/src/latex.tsis a whole-file divergence: an upstream sync touching it must reconcile againstpackages/tui/src/components/latex.ts. LOW: thepackages/tui/src/index.tsexport slot.
Landed 2026-08-16 (commit 03f46f57e, shipped in PR #892).
packages/tui/src/tui.ts:TuiBase.handleTerminalInput()consumes tmux focus events only whenmode !== "fullscreen". Fullscreen renderers own focus events so they can clear exactly an active drag selection without forcing idle or completed-selection repaints; the main screen still refreshes terminal capabilities when focus returns to a multiplexer pane.- PR #892 (merge/upstream-20260816) delivered upstream v0.84.2, whose focus behaviors — skipping repaints of idle fullscreen sessions on focus loss, giving focused fullscreen overlays wheel and viewport keys, and fullscreen transcript search — previously failed here because the fork's
TuiBasefocus interception forced a redraw before the alt-screen selection logic ran. The routing above is the fork-side repair;b25d5bdebrealigned the upstream assertions with fork branding. - The upstream focus-loss tests carried by that sync (
packages/tui/test/tui-alt-screen.test.ts) now run against the fork renderer.
- Three upstream focus-loss behaviors failed after the v0.84.2 merge until fork-side focus consumption was scoped to the main screen; without this entry the next sync would re-break or silently drop the repair.
- Focus events are consumed inside the renderer's input path before any component or extension sees the bytes.
- MEDIUM: the
handleTerminalInput()focus branch inpackages/tui/src/tui.ts. LOW:packages/tui/src/index.tsimport ordering.
packages/tui/src/tui-alt-screen.tscarries upstream v0.84.2's selection-copy behavior (upstream issue #8110, delivered here by the PR #892 sync): copying an alt-screen selection writes through the host-clipboard seam that interactive mode wires on its side. The fork tree matches the pin for this behavior.- The residual fork delta in this file is the teardown rename
deleteAltScreenKittyImages(), which keeps alt-screen image teardown distinct from the shared kitty deletion helpers.
- Recorded so the next upstream sync treats the clipboard path as upstream-owned parity rather than a fork delta to re-port, and so the audit's divergence for this file is attributed to the rename.
- Selection copy executes inside the fullscreen renderer's mouse/selection handler; no extension seam intercepts terminal mouse bytes.
- LOW: the
deleteAltScreenKittyImages()rename sites; the clipboard path itself is upstream-owned.
packages/tui/src/tui-alt-screen.tscarries upstream v0.84.2's generic SGR mouse-release handling (upstream issue #7963, delivered by the PR #892 sync):handleSelectionMouseEventaccepts release events reporting the no-button code (button === 3) in addition to button 0, so a release that does not name a drag button still completes selection instead of being dropped.
- Recorded for sync parity like the host-clipboard entry: the behavior is upstream-owned and at pin parity here, and the file's only fork divergence remains the teardown rename.
- SGR mouse parsing and selection state are private to the fullscreen renderer's input path.
- LOW: the release guard in
handleSelectionMouseEvent; upstream-owned otherwise.
2026-08-16: add a prompt-leading mixed dollar invocation picker (PR #909)
CombinedAutocompleteProviderrecognizes a prompt-leading$run.- The editor treats
$as a built-in symbol autocomplete trigger, so the mixed picker opens on real keystrokes rather than only through direct provider calls. - The first
$token lists canonical/commandrows before$skillrows and filters both with the same query. - Selecting a command inserts
/name; selecting a skill inserts$name. - A second leading
$token reopens only known skills, while inline or unknown-prefix dollar text stays literal.
- OmO Desktop and Senpi RPC now expose one mixed command/skill surface; the terminal needs the same invocation
affordance without teaching providers a new
$commandexecution syntax. - Canonical insertion keeps existing slash command dispatch and the shared dollar skill parser authoritative.
- MEDIUM:
autocomplete.tstrigger ordering and completion replacement. - LOW:
components/editor.tsdefault autocomplete trigger characters. - LOW: additive
dollar-invocation-autocomplete.tsand its focused test.
- When a frame's content grows above the viewport and a visible row also changes (
viewportTop !== prevViewportTopwithlineCountDelta !== 0), the renderer now falls back to the canonicalrenderScrollbackReplay/ mux dispatch instead of repainting only the visible rows in place.
- The in-place repaint emitted exactly
heightrows and returned, so rows inserted above the viewport (e.g. Ctrl+O expanding several tool blocks in one frame) never reached terminal scrollback even thoughsetPreviousLinesmarked them painted — leaving mismatched headers and truncated results. The replay path re-emits the full canonical transcript.
- LOW:
tui.tstheviewportTop !== prevViewportTopbranch; LOW intui-render.test.ts.
packages/tui/src/terminal.ts:ProcessTerminal.stop()still restores the raw-mode state captured bystart(), but now treatsEIO,EPIPE, andENOTCONNfrom the teardown-timesetRawMode()call as a dead terminal instead of crashing the exiting CLI.- The EIO classifier accepts both Node's string
code: "EIO"shape and Bun's macOS raw positiveerrno: 5shape, using numeric errno only when no string code is available. - The separate coding-agent classifier handles asynchronous stdout/stderr stream
errorevents; this numeric fallback stays scoped to the synchronous stdinsetRawMode()ioctl that produced the observed Bun error shape. - Unexpected raw-mode restoration errors still propagate so shutdown does not hide unrelated defects.
test/terminal.test.tscovers successful restoration, the dead-terminalEIOregression, and unexpected-error propagation.
- An SSH or PTY peer can disappear after input draining but before raw-mode restoration. Node/Bun then throws a
synchronous stdin ioctl error, which bypasses the coding-agent's stdout/stderr error handlers and replaces the
requested exit with an uncaught
setRawMode failed with errno: 5stack.
- Raw-mode ownership and restoration are private
ProcessTerminallifecycle responsibilities. Extensions receive neither the saved raw-mode state nor a teardown hook around the stdin ioctl.
- LOW:
packages/tui/src/terminal.tsaround the terminal error classifier andProcessTerminal.stop()raw-mode restoration. - LOW:
packages/tui/test/terminal.test.tsaround lifecycle coverage.
- Cursor restoration and visibility bytes now stay inside each synchronized
render frame instead of being written after
FRAME_END. - The editor stops drawing its inverse-video fake cursor when the hardware
cursor is visible; it still emits
CURSOR_MARKERfor IME placement. - The renderer also removes a colocated inverse-video cursor after
CURSOR_MARKER, covering focused single-lineInputconsumers and both inverse-off (CSI 27 m) and full-reset (CSI 0 m) terminators without discarding full-reset semantics. - Runtime cursor-mode toggles defer visibility changes to the replacement frame, and shutdown no longer blanks content beneath a hardware cursor.
- With
showHardwareCursor: true, animated Working updates briefly published the real cursor on the loader row before a second write returned it to the editor, producing rapid flicker. - The visible hardware cursor and fake cursor were both drawn at the editor insertion point, making Korean IME composition look duplicated. The same ownership conflict affected search, selector, login, and extension inputs.
- This cannot be implemented as an extension: cursor-marker extraction, synchronized-frame boundaries, and final ANSI cursor writes are renderer invariants below the extension API.
- HIGH:
tui.tssynchronized render exits and cursor positioning. - LOW:
components/editor.tscursor rendering.
tui.tsreusesnormalizeTerminalOutputresults across frames through a per-instance memo keyed by the raw line string. Full normalization passes swap in a fresh map holding only the lines used by the current frame, so the memo never outgrows the transcript it mirrors (whose normalized strings it shares by reference). Unchanged lines now keep their string identity across frames, which also restores O(1) reference-equality diff compares that fresh normalization allocations previously defeated. Image lines keep bypassing normalization unchanged.mux.tsviewportRenderEnabled()now defaults on;PI_TUI_VIEWPORT_RENDER=0opts out of viewport-bounded normalize+diff and1still forces it on. Output byte-equivalence between the bounded and full paths is pinned bytest/viewport-render.test.ts(streaming, offscreen line-count changes, offscreen in-place mutations).scripts/perf-trend-local.shpins the two baseline frame-cost lanes toPI_TUI_VIEWPORT_RENDER=0so their historical meaning (unbounded full pass) survives the default flip.bench/frame-cost.ts300-frame p50 on Apple M5 Max, stable components: 100k-line transcript 16.20ms -> 1.97ms (new default; 8.2x) and 16.20ms -> 12.34ms with bounding opted out (memo only); 30k lines 4.51ms -> 1.66ms; 10k lines 2.23ms -> 1.34ms. Emitted bytes per frame stay identical (131) across all lanes.- Coverage:
test/viewport-render.test.tsproves the unset-flag default bounds normalization, the opted-out full pass renormalizes only new content after the first frame, and byte-identical writes across both paths;test/mux.test.tspins the default-on/opt-out switch semantics.
The normalize/diff pipeline is private render state inside TUI.doRender() (previousLines, previousRawLines,
viewport offsets). No component or extension seam can deduplicate normalization work or change the bounded-path
default without owning that state.
- MEDIUM:
tui.tsnormalizeLine()/applyLineResets()bodies and the render-state field block. - LOW:
mux.tsviewportRenderEnabled(),test/mux.test.ts,test/viewport-render.test.ts,scripts/perf-trend-local.shbench lanes.
- Bare
/no longer lists everyskill:<name>command, and partial/skillinput exposes oneskill:namespace hint instead of flooding the palette with every child skill. /skill:and case variants such as/SKILL:open the full skill namespace, while/followed by a skill's full name or leading letters finds matching child skills directly.
- The shared
skill:prefix flooded the root slash-command overview and obscured the smaller set of general commands, while filtering every child also left/skillas a discoverability dead end.
The shared autocomplete provider owns slash-command filtering before coding-agent extensions receive input, so an extension cannot change which registered skill commands appear for each typed prefix.
- LOW:
slash-command-autocomplete.tsskill filtering and its focused autocomplete regression test.
components/markdown.tsregisters bounded Marked block and inline tokenizers for$...$,$$...$$,\(...\), and\[...\]math. Dollar delimiters require non-word outer boundaries, and bracket/parenthesis candidates stop at inline-code or competing opener boundaries. Currency, shell variables, code spans, and malformed delimiters remain literal, including partial streamed currency/shell pairs and math-like text after an unclosed inline-code opener.- The dependency-free
components/latex.tsconverter uses a balanced parser for nested fractions, roots, text wrappers, symbols, and Unicode sub/superscripts. Formula length and nesting budgets fall back to the original text instead of partially converting or repeatedly rescanning untrusted input. A leading combining mark receives a dotted-circle anchor so terminal cell width agrees with the differential renderer. - TeX epsilon/phi variants and escaped script markers stay distinct, complete command names prevent prefix corruption, and unknown commands remain readable. Display formulas inherit their surrounding style context.
- Coverage:
test/markdown.test.tsproves ordinary-text boundaries, nested/budgeted conversion, streamed partial currency/shell and inline-code frames, CJK wide cells, inherited styles, malformed preservation, and focusedVirtualTerminalcell widths including a column-zero combining mark.
The Markdown component owns tokenization before extension-facing coding-agent UI hooks run. Rendering formulas
consistently in assistant messages, nested Markdown structures, and every direct TUI consumer requires the parser seam.
- MEDIUM:
components/markdown.tsparser construction and custom token branches. - LOW:
components/latex.tssymbol/script conversion tables andtest/markdown.test.tsLaTeX cases.
tui.tsnow formats the full over-wide render diagnostic only when strict mode needs it or before the first release-mode crash dump. Later over-wide release frames still truncate safely, but no longer map every rendered line throughvisibleWidth()after the one-shot dump has already been written.__renderDiagnosticStats()exposes diagnostic line-scan counts only underPI_TUI_TEST_SEAMS=1.test/render-contract.test.tsproves the first over-wide release frame scans diagnostic input and a second frame neither writes nor rescans the transcript.
The existing overWideCrashDumpWritten guard covered only the filesystem write. Building crashData happened before
that guard, so an animated row could rescan a large resumed transcript on every frame even though no second dump was
possible. Before the companion coding-agent throttle, a 34 MB session's 32 ms Working shimmer turned that
diagnostic work into a continuous CPU loop.
- LOW:
tui.tsaround release-mode over-wide truncation and crash diagnostics. - LOW:
test/render-contract.test.tsover-wide release behavior.
components/editor.tssetText()no longer unconditionally clears the large-paste registry. It now prunes only entries whose markers do not appear in the new text (and resets numbering when the registry empties). Markers that survive a programmaticgetText()→setText()round-trip stay live: they remain atomic segments and still expand to the full pasted body on submit and ingetExpandedText().- Pruning matches the exact canonical marker string reconstructed from the stored body via the shared
formatPasteMarker()helper (also used at insert time), so arbitrary new text that merely looks like a live marker ([paste #1 +5 lines]with a mismatched suffix) cannot accidentally revive a registry entry and expand to unrelated content. - Provenance check:
setText()retains an entry only if its canonical marker appears in BOTH the previous and the new text (a genuine carried-over round-trip). Stale registry entries — kill-line/word-delete remove marker text without touching the registry, intentionally, so yank can restore a killed marker — can no longer be revived by replacement text that coincidentally contains their exact marker. Explicit cross-instance transfers usesetPasteState(), which skips the provenance check by design. - New
getPasteState()/setPasteState()onEditorplus optionalgetPasteState?/setPasteState?on theEditorComponentinterface (exportedEditorPasteState): snapshots the registry for transfer between editor instances.setPasteState()raises the paste counter above transferred ids (no collisions) and prunes entries whose markers are absent from the current text. The interface documents the paired contract: implement both together — callers treat an editor withsetPasteStatebut nogetPasteStateas paste-unaware, because it could not re-export collapsed markers on a later hand-off. - The submit/
getExpandedText()expansion logic is extracted as the exportedexpandPasteMarkers(text, state)helper so consumers holding a paste snapshot (e.g. an editor hand-off where the source lacksgetExpandedText) can expand markers without duplicating the marker grammar. - Expansion and atomic segmentation are both canonical-exact and therefore consistent: only the exact marker string produced at paste time expands or merges into an atomic segment. Same-id text with a different suffix (e.g. a literal
[paste #1 +5 lines]while entry #1 stores 12 lines) stays literal and is not treated atomically. Previously expansion was suffix-lenient and segmentation was id-based, so a coincidental same-id literal could be replaced by the stored body at submit. - Previously any
setTextround-trip (dialog save/restore, queued-message restore, editor hand-off) orphaned live markers into dead literal text, so submitting sent the literal[paste #1 +18 lines]placeholder to the model instead of the pasted content. - Tests:
test/editor.test.ts"Paste marker atomic behavior" — round-trip preservation, queued-restore combination, selective/exact pruning, coincidental-marker rejection, cross-instance transfer, counter collision safety, and numbering reset.
The paste registry and marker segmentation are Editor-private state; consumers only see getText()/setText()/getExpandedText() and cannot preserve the registry across a round-trip themselves. Cross-instance transfer needs a first-class snapshot API for the same reason.
- LOW:
components/editor.tssetText(),prunePastes(),formatPasteMarker(),getPasteState()/setPasteState(), and the handlePaste marker-insertion line. - LOW:
editor-component.tsoptional paste-state methods;index.tsEditorPasteStateexport. - LOW:
test/editor.test.tspaste marker suite.
terminal-image.ts:detectCapabilitiesno longer hard-disables images under tmux. It probes the effective#{allow-passthrough}value for the current pane (plus#{client_termname}) viatmux display-message -p; when passthrough ison/alland the outer terminal implements the Kitty graphics protocol (kitty/Ghostty/WezTerm viaclient_termnameor leaked env hints), capabilities becomeimages: "kitty", tmuxPassthrough: true. Both probes are dependency-injectable for tests.terminal-image.ts: new exportedwrapTmuxPassthrough(sequence)wraps a sequence in a tmux DCS envelope (ESC Ptmux; … ESC \with every payload ESC doubled).encodeKittywraps each APC chunk individually anddeleteKittyImage/deleteAllKittyImageswrap their delete commands whentmuxPassthroughis active.terminal-image.ts: Kitty Unicode placeholder placement for split-safe tmux rendering. Direct passthrough placement draws at the outer terminal's cursor and breaks in split panes, so placeholder-capable outer terminals (kitty, Ghostty) getkittyUnicodePlaceholders: true:encodeKittygains avirtualoption (U=1virtual placement),buildKittyPlaceholderRowemits U+10EEEE cells with row/column (and id high-byte) diacritics plus the image id in the 24-bit foreground color, andrenderImagereturns per-rowlines(first line carries the wrapped transmission). Placeholder cells are plain 1-column text, so tmux clips/scrolls/moves them with the pane. WezTerm (no placeholder support) stays on direct placement;PI_TUI_TMUX_KITTY_PLACEMENT=placeholder|directoverrides the heuristic. TheImagecomponent usesresult.lineswhen present instead of one sequence line plus empty padding rows.terminal-image.ts: the tmux probe also reportsclient_cell_width/client_cell_height; when tmux images are enabled the detected cell size is adopted viasetCellDimensionsbecause tmux never answers theCSI 16 tcell-size query (verified against tmux 3.6), keeping image aspect ratios correct.terminal-image.ts/index.ts:outerKittyGraphicsMode(clientTermname)is exported so the coding-agent startup guidance can decide whether recommendingallow-passthroughis useful for the attached terminal.utils.ts:extractAnsiCodelearned DCS sequences (ESC P … ST), skipping doubled-ESC pairs so the escaped inner ST does not terminate the envelope early. Wrapped image lines therefore keepvisibleWidth === 0and stay compatible with the TUI's Kitty image-line bookkeeping (id/row extraction intui.tsusesindexOf("\x1b_G"), which still matches inside the doubled-ESC payload).
Image capability detection and Kitty sequence emission are terminal-image.ts internals consumed by the
Image component and the TUI renderer's image deletion/diff paths; extensions cannot re-wrap sequences the
renderer emits.
- MEDIUM:
terminal-image.tstmux branch ofdetectCapabilities,encodeKittychunk assembly, andrenderImagekitty branches. - LOW:
utils.tsextractAnsiCodeescape-sequence branches;components/image.tskitty line assembly. - LOW:
index.tsterminal-image export list;test/terminal-image.test.tstmux capability tests.
autocomplete.tsreopens slash suggestions for a/skill:token after a completed, known leading skill command and offers only skill commands there. Completion inserts the selected second skill command with its leading slash and trailing space.- Other slash commands remain leading-only, and skill suggestions do not appear after prose or an unknown leading skill. This keeps the autocomplete contract aligned with the executable leading-run parser in coding-agent.
The shared autocomplete provider owns the suggestion and insertion decisions that editor consumers use before the coding-agent session receives a prompt.
- LOW:
autocomplete.tsslash-command suggestion and completion branches. This fork-local diff is deliberately minimal because the file is shared with upstream pi.
packages/tui/src/tui.ts: the static 16ms render throttle (MIN_RENDER_INTERVAL_MS) is now an instance field#minRenderIntervalMs(default 16ms — behavior unchanged for existing callers) plussetMaxRenderFps(fps): fps is clamped to 30-120 and stored asMath.floor(1000 / fps)(120fps ⇒ 8ms interval).packages/tui/src/index.ts: exportsgetGraphemeSegmenterandgetWordSegmenterfromutils.tsso consumers (smooth-streaming reveal in coding-agent) share the singleIntl.Segmenterinstances.- Tests:
packages/tui/test/render-fps-cap.test.ts(mocked-timer throttle-delay assertions) andpackages/tui/test/segmenter-exports.test.ts(root re-export identity).
The render throttle is TUI-private scheduler state; extensions and components can request renders but cannot
safely replace the minimum frame interval. The segmenters already existed as module singletons in utils.ts — only
the package-root export surface was missing.
- LOW:
packages/tui/src/tui.tsaround the scheduler field declarations andscheduleRender(). - LOW:
packages/tui/src/index.tsaround theutils.tsre-export list.
packages/tui/src/terminal.ts(+index.tsexport):ProcessTerminalacceptsonExternalStdoutWrite. While started,process.stdout.writeis patched so writes not issued by the terminal itself are forwarded to the handler instead of reaching the screen; the terminal's own output goes through the captured raw writer. External writes previously interleaved with frames, scrolled the viewport, and permanently desynchronized differential rendering. Passthrough restores onstop(), and a throwing handler falls back to raw stdout so output is never lost.packages/tui/src/terminal.ts:setTitlestrips C0/C1 control characters before emitting OSC 0 — an embedded BEL/ESC in session, tool, or extension titles terminated the sequence early and dumped the remainder as raw output.packages/tui/src/tui.ts:renderRequestedandinputRenderPendingare reset in bothstop()andstart(). A render requested within the pending window (nextTick or the 16ms throttle) or while stopped leftrenderRequestedset, so every plainrequestRender()after restart silently no-oped until a keypress.
- stdout ownership, OSC emission, and render-scheduling flags are
ProcessTerminal/TUIinternals; components and extensions cannot patch process streams or reset private scheduler state safely.
- MEDIUM:
packages/tui/src/terminal.tsaroundstart()/stop()stream handling andsetTitle. - LOW:
packages/tui/src/tui.tsstop()/start()scheduling-state resets. - LOW:
packages/tui/test/external-stdout-guard.test.ts,packages/tui/test/terminal.test.ts.
packages/tui/src/tui.ts: added multiplexer-aware full-render policy, bounded mux viewport repaint, opt-in viewport-bounded normalize/diff, scroll-then-diff for bounded concurrent mutations, cursor visibility write coalescing, SGR reset-after-clear coverage, and release-mode render-failure containment.packages/tui/src/utils.ts: replaced the width cache with a two-generation cache and added the measured SGR coalescing utility/report path; runtime SGR coalescing remains unwired because the measured byte reduction was below the adoption gate.
These behaviors depend on TUI's private render state: previous and raw line snapshots, viewport offsets,
terminal dimensions, cursor bookkeeping, synchronized output framing, mux detection, image-row handling, and
row-clear invariants. Components and extensions can reduce churn or request renders, but they cannot safely
replace the renderer's terminal-byte decisions or update its internal cursor/viewport state.
- HIGH:
packages/tui/src/tui.tsarounddoRender(),fullRender(),renderViewportInsertScroll(),renderScrollbackReplay(),positionHardwareCursor(), and render-error diagnostic handling. - MEDIUM:
packages/tui/src/utils.tsaround width caching, terminal-output normalization, and ANSI parsing helpers. - LOW:
packages/tui/test/tui-render.test.tsflicker-budget and scrollback assertions when upstream changes renderer byte expectations.
- In
packages/tui/src/tui.ts, every frame write is bracketed byTUI.FRAME_BEGIN(DECSET 2026+DECRST 7) andTUI.FRAME_END(DECSET 7+DECRST 2026) instead of bare synchronized-output markers. - New regression:
packages/tui/test/regression-wrap-desync-ghost-line.test.ts.
- Differential rendering tracks the cursor with relative moves only. When the terminal draws a row wider than
visibleWidth()measured (East-Asian-ambiguous glyphs, emoji newer than the terminal's Unicode tables, decomposed Hangul jamo), the row physically wraps, the cursor drifts one row down, and every later single-row diff (e.g. the loader seconds tick) paints one row too low — leaving a stale, partially overwritten ghost line such asWorking (0s • esc to interrupt)above the fresh one. With autowrap off during the frame, over-wide rows clip at the last column and the drift cannot happen. Autowrap is restored at frame end so the shell never observes the disabled state, even after a crash between frames.
- MEDIUM: every
let buffer = "\x1b[?2026h"/buffer += "\x1b[?2026l"site inTUI.doRender(),fullRender(),renderViewportInsertScroll(), andrenderScrollbackReplay()— upstream edits to those literals will conflict with theFRAME_BEGIN/FRAME_ENDconstants.
packages/tui/src/components/loader.tssupportsmessageFormatterwith an independent message animation interval.- Senpi's normal TUI depends on this for
Working (Xs • esc to interrupt)shimmer; a loader that only animates the indicator frame is not compatible with the forked CLI.
The loader is instantiated by InteractiveMode during streaming. Extensions can replace the indicator options, but a
globally installed CLI must ship a TUI runtime whose Loader honors messageFormatter.
- HIGH:
packages/tui/src/components/loader.tsaroundLoaderIndicatorOptions,setIndicator(),restartAnimation(), andupdateDisplay(). - HIGH: package/release wiring that decides whether
@code-yeongyu/senpibundles this forked TUI runtime or installs upstream npm@earendil-works/pi-tui.
- In
packages/tui/src/tui.tsTUI.doRender(), structural changes that begin above the previous viewport now replay the latest canonical transcript from the top of the visible viewport when the visible rows would otherwise be unchanged. - In
packages/tui/test/tui-render.test.ts, the Ctrl+O regression now checks the latest xterm scrollback suffix for multiple offscreen expanded blocks, not only the visible tail viewport.
- Terminal scrollback rows above the visible viewport cannot be rewritten in place. The earlier fork-only differential remap updated
previousLineswithout writing a new canonical transcript, so older collapsed tool/read blocks stayed visually collapsed while the bottom block appeared updated. A full screen clear fixed the stale scrollback but reintroduced visible flicker, so the replay now avoids bothESC[2JandESC[3Jand validates the newest canonical suffix instead of trying to delete historical rows.
- HIGH:
TUI.doRender()around thefirstChanged < prevViewportTopbranch, because this preserves the fork's no-viewport-clear behavior while adding a scrollback-only replay path. - LOW:
packages/tui/test/tui-render.test.tsunderTUI viewport remap for above-viewport growth.
- In
packages/tui/src/tui.tsTUI.doRender(), content shrinkage that starts above the current viewport now remaps the viewport to the new bottom and uses the existing in-place viewport repaint path instead of forcingfullRender(true). - In
packages/tui/test/tui-render.test.ts, regressions now cover a direct above-viewport collapse and repeated Ctrl+O-equivalent expand/collapse toggles.
- Ctrl+O toggles every expandable chat item. When expanded tool output collapses above the visible rows, the old shrink branch cleared the screen and scrollback (
ESC[2J/ESC[3J]), which produced a visible TUI flash even when the final visible tail rows were unchanged.
- MEDIUM:
TUI.doRender()around thefirstChanged < prevViewportTopremap branch, because this fork already carries upstream-divergent differential repaint logic there. - LOW:
packages/tui/test/tui-render.test.tsunderTUI viewport remap for above-viewport growth.
- In
packages/tui/src/tui.tsTUI.doRender(), streaming inserts that move the viewport down while leaving a stable bottom suffix now use a scroll-region update for the changed viewport prefix, then paint only the newly inserted rows. - The fast path skips image rows and overlays, preserving the existing safer repaint paths for cases where terminal-owned image placement or overlay composition makes scroll-region edits risky.
- In
packages/tui/test/tui-render.test.ts, an expanded-output regression now asserts repeated appends avoid viewport/scrollback clears, keep DECSET 2026 balanced, preserve the final viewport, and avoid repainting stable tail rows every tick.
The decision depends on internal renderer state: previous and next viewport slices, line-count delta, stable suffix detection, image-line detection, hardware cursor bookkeeping, and synchronized terminal writes. Components and extensions can reduce churn, but cannot safely emit scroll-region edits or update TUI's private viewport/cursor state.
packages/tui/src/tui.tsnear the viewport remap and differential render branches indoRender().packages/tui/test/tui-render.test.tsinTUI viewport remap for above-viewport growth.
- In
packages/tui/src/tui.tsTUI.doRender(), above-viewport growth that remapsviewportTopnow repaints only the visible viewport rows in place under synchronized output instead of falling back to a post-init full replay path. - The repaint path deletes only kitty images in the previously visible viewport slice before rewriting rows, preserving image cleanup without clearing scrollback.
- In
packages/tui/test/tui-render.test.ts, the above-viewport expansion regression now also asserts no raw\x1b[2J/\x1b[3Jappears and verifies visible expanded rows are repainted while DECSET 2026 remains balanced.
The decision point depends on internal renderer bookkeeping (prevViewportTop, viewportTop, hardwareCursorRow, kitty image ID tracking, and synchronized write boundaries). Extensions/components can trigger renders but cannot replace this internal fallback behavior or safely rewrite only viewport rows at this stage.
packages/tui/src/tui.tsaround thefirstChanged < prevViewportTopbranch insidedoRender()(viewport remap handling and fallback path).packages/tui/test/tui-render.test.tsinTUI viewport remap for above-viewport growthassertions.
- Tighten
TUI.doRender()fallback paths so streaming updates can stay on the differential renderer instead of clearing the full screen when unchanged visible viewport rows are stable. - Keep synchronized output (
DECSET 2026) balanced around every differential write path. - Add flicker-budget regression tests for synthetic streaming workloads in
packages/tui/test/tui-render.test.ts.
The fallback decisions live inside TUI.doRender() and depend on private renderer state: previousLines, viewport offsets, terminal dimensions, cursor row tracking, and the line-diff window. Extension hooks and components can request renders, but they cannot override the internal decision to call fullRender(true) or wrap terminal writes with synchronized output.
Component-level caching is added in coding-agent components because high-frequency assistant/tool updates rebuild render trees during streaming. External extensions can register alternate renderers, but they cannot memoize the built-in assistant and tool execution components without replacing core interactive-mode rendering.
packages/tui/src/tui.ts:TUI.doRender()fallback branches around width/height changes,clearOnShrink, deleted-line handling, viewport-shift handling, and synchronized output writes.packages/tui/src/tui.ts:fullRenderpaths andfullRedrawCountaccounting.packages/coding-agent/src/modes/interactive/components/assistant-message.ts: assistant streaming render cache.packages/coding-agent/src/modes/interactive/components/tool-execution.ts: tool execution streaming render cache.packages/coding-agent/src/modes/interactive/interactive-mode.ts: streaming render request audit comments nearmessage_updateandtool_execution_update.
flicker budget under streaminginpackages/tui/test/tui-render.test.tsverifies:- full clear sequence count stays at the initial render only,
- ANSI escape bytes remain below the content-byte budget,
- every
DECSET 2026begin has a matching end, - no
fullRender(true)equivalent clear occurs after the init phase.
packages/tui/src/image-markers.ts(new):ImageMarkerRegistrytracks the ids of atomic[Image #N]markers living in editor text, storing ids only and never image bytes. It guarantees the visible numbers stay a contiguous1..ksequence (viacanonicalize()), exposesauthorizedMarkers()for markers occurring exactly once (the only ones safe to treat as atomic), and supports single-occurrence removal plusEditorImageStatesnapshots for transfer between editor instances.packages/tui/src/paste-markers.ts: marker segmentation generalized so paste markers and image markers share the same atomic-segment machinery instead of the paste path owning a private tokenizer.packages/tui/src/components/editor.ts: image markers are treated as atomic editor segments.insertImageMarker()inserts the next[Image #N]marker at the cursor and returns its id, backspace/delete removes a marker whole,getImageMarkerState()/setImageMarkerState()export and install registry snapshots, andonImageMarkersChangedreports the ids in text reading order whenever markers are added, removed, pruned, or renumbered.packages/tui/src/editor-component.ts: theEditorComponentinterface gains the optional image-marker API (insertImageMarker,getImageMarkerState,setImageMarkerState,onImageMarkersChanged) with paired-contract docs: an editor exposing insertion without the change callback is treated as image-unaware and receives the plain text path instead.packages/tui/src/index.ts: exports the image-marker surface (ImageMarkerRegistry,EditorImageState,ImageMarkerCanonicalization,ImageMarkerRemoval,IMAGE_MARKER_REGEX,IMAGE_MARKER_SINGLE,formatImageMarker,isImageMarker,imageMarkerId).
- Pasting a clipboard image used to insert the raw temp file path into the composer, leaking local filesystem paths into prompts and transcripts. Atomic markers let the editor display
[Image #1]while the payload lives outside the text, and contiguous renumbering keeps the Nth marker mapped to the Nth submitted image.
- Cursor discipline, segment atomics, and the editor's text model are TUI internals; an extension can compose components but cannot make backspace delete a marker whole or keep registry ids synchronized with visible numbers across editor instances.
- MEDIUM:
packages/tui/src/components/editor.ts(segment handling around cursor movement and deletion) andpackages/tui/src/paste-markers.ts(the generalized segmentation shared with paste markers). - LOW:
packages/tui/src/image-markers.ts(new fork-owned file, no upstream counterpart),packages/tui/src/editor-component.ts(additive optional interface members), and thepackages/tui/src/index.tsexport lists.
packages/tui/src/tui.ts: main-screen render writes are emitted in bounded 1 MiB chunks, including full redraws and differential updates, so large image-heavy frames cannot exceed V8 string limits while preserving the fork's synchronized frames and viewport renderer.
- Upstream #8028 prevents V8 string-length crashes when a main-screen render contains very large terminal-image payloads. The fork renderer lives in
TuiBase, so the bounded write behavior is ported there rather than replacing the fork's thinTuiMainScreensubclass.
TuiBaseowns the fork's main-screen differential renderer, insert-scroll path, scrollback handling, and lifecycle state; no extension boundary can safely split its terminal frame writes.
packages/tui/src/tui.tsaround full-render and differential-render terminal writes;packages/tui/src/tui-main-screen.tsremains a thin state-capture subclass.
packages/tui/src/components/box.ts: forkContainerdisposal semantics (dispose()idempotent via adisposedflag,clear()disposing children,detachAll()detaching without disposing for reuse) alongside upstream's mouse layout cache and child hit-testing.packages/tui/src/components/editor.ts: fork atomic paste and image markers (MarkerKind, marker-aware segmentation,removePasteMarker/removeImageMarkerwith renumbering), undo snapshots carrying attachment payloads,normalizeWarpWslShiftEnterInputseam and the@/#/$autocomplete triggers, on top of upstream's mouse selection.packages/tui/src/components/select-list.ts: forkSelectListRowParts/renderRowrow composer and ranking beside upstream'smousePressedIndex/handleMouse/getVisibleRange.packages/tui/src/index.ts: fork exports (fullscreen transcript search, atomic image markers,expandPasteMarkers,ProcessTerminalOptions,calculateImageRows,sanitizeTerminalLabel/shortenImagePath) with upstream'sMouseRegion/native clipboard exports.packages/tui/src/terminal.ts: fork dead-terminal detection (EIO/EPIPE/ENOTCONNcodes, Bun errno fallbacks), shared stdin error dispatcher, keyboard-enhancement state, Warp/WSL shift+enter normalization,ProcessTerminalOptions.onExternalStdoutWrite, multiplexer detection; upstream'sgetNativePlatformHelper()VT-input path was adopted.packages/tui/src/tui-alt-screen.ts: the fork keeps thedeleteAltScreenKittyImagesteardown name (three call sites) around upstream's mouse/scrollbar/search additions.packages/tui/src/utils.ts: fork two-generation width cache,coalesceAdjacentSgr, DCS/tmux passthrough escaping and grapheme/word helpers, plus upstream'sgetActiveBackgroundAnsi.
- The fork renderer's paste/image provenance, disposal contract, terminal fault tolerance and width caching are product invariants pinned by fork tests; upstream's mouse, scrollbar and native-platform work was layered onto them.
- These are the TUI library primitives every component and the coding agent build on.
- HIGH:
packages/tui/src/components/editor.tsmarker handling and input dispatch;packages/tui/src/terminal.tsProcessTerminalstart/stop. - MEDIUM:
packages/tui/src/index.tsexport list;packages/tui/src/utils.tswidth cache and ANSI helpers;select-list.tsrender path. - LOW:
box.tslifecycle methods;tui-alt-screen.tsteardown call sites.