Skip to content

Latest commit

 

History

History
1313 lines (823 loc) · 89.1 KB

File metadata and controls

1313 lines (823 loc) · 89.1 KB

TUI delta rendering fork changes

2026-09-23 — Let hosts observe the real stderr destination (senpi#1879)

What changed

  • packages/tui/src/terminal.ts accepts a host-owned stderr subscription and releases it on stop. packages/tui/src/stderr-observer.ts retains direct-stream observation for ordinary terminals without replacing a later writer during cleanup.

Why

  • A host can redirect stderr to a diagnostic log. Observing calls above that redirect falsely reports visible output and duplicates the working frame.

Why an extension could not handle it

  • Mouse geometry is invalidated inside the terminal, below extension components.

Expected merge conflict zones

  • Terminal construction, external-write observation and stop cleanup. Visible stdout/stderr must continue invalidating stale hit targets.

2026-09-22 - Render-error diagnostics follow the host log directory (senpi#2000)

What changed

  • packages/tui/src/tui.ts: new module-scoped defaultDiagnosticLogDirectory() and renderErrorLogDirectory. TuiBase's constructor publishes its resolved logDirectory into that module scope, and logRenderErrorOnce() writes to it instead of re-deriving os.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 writes senpi-debug.log into that directory and not into a HOME-derived path.

Why

  • logRenderErrorOnce() is reached from Container.render(), which has no TUI instance, so the path was hardcoded from os.homedir(). Hosts already pass a resolved agent directory (logDirectory), and pi-debug.log already 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 HOME append 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 a TuiBase, so no host directory is published and the fallback still resolves from HOME. 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 quarantine HOME the way it already quarantines the agent directory.

Why an extension could not handle it

  • Render containment and its diagnostic live inside packages/tui's render path; an extension cannot reach Container.render()'s catch branch or the module-scoped logger.

Expected merge conflict zones

  • packages/tui/src/tui.ts: the module-scoped diagnostic state block near DIAGNOSTIC_LOG_MODE, the logRenderErrorOnce() body, and the TuiBase constructor's logDirectory assignment.

2026-09-20 - Keyboard focus requires the ability to receive keys (senpi#1882)

What changed

  • packages/tui/src/tui.ts: new canReceiveKeys() export; resolveMouseFocusTarget() returns Component | null and resolves a clicked component that cannot receive keys to the deepest mounted ancestor that can, or to null; new private findKeyFocusOwner().
  • packages/tui/src/tui-main-screen.ts: applyMouseResult skips 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 in applyMouseDispatchResult (new applyFocus parameter) and the same click-handler precedence in handleMouseEvent.
  • 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 no handleInput, which is the defect this entry fixes.

Why

  • A clickable row (MouseRegion) or tab strip has no handleInput. Focusing it made handleTerminalInput drop 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.

Why an extension could not handle it

  • Mouse focus ownership is renderer state inside packages/tui; an extension cannot reorder focus application around click dispatch.

Expected merge conflict zones

  • packages/tui/src/tui.ts: resolveMouseFocusTarget signature and return type.
  • packages/tui/src/tui-main-screen.ts and packages/tui/src/tui-alt-screen.ts: the click branches of their mouse handlers.

2026-09-20 - Resolve native clipboard helpers in the published bundle (senpi#1848)

What changed

  • packages/tui/src/native-module-path.ts: resolve the installed TUI entry with import.meta.resolve, fall back to moduleRequire.resolve, and accept the package-anchored candidate only when the entry is absolute.

Why

  • packages/tui/src/native-module-path.ts: Bun can return the bare package specifier from require.resolve inside an esbuild chunk. The resulting relative candidate cannot load the native helper, so Ctrl+V silently reads an empty clipboard.

Why an extension could not handle it

  • packages/tui/src/native-module-path.ts: native helper discovery belongs to the TUI package, below extension clipboard handling.

Expected merge conflict zones

  • packages/tui/src/native-module-path.ts: package resolution and its first candidate. The module-directory and executable-directory fallbacks retain their order.

2026-09-17 - Repeated dollar mentions and styled skill tokens (senpi#1778)

What changed

  • packages/tui/src/dollar-invocation-autocomplete.ts: getDollarInvocationContext completes any $query at 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. New findDollarSkillMentions(line, knownSkills) and knownSkillNames(commands).
  • packages/tui/src/autocomplete.ts: AutocompleteProvider.getMentionRanges?(line) and MentionRange; CombinedAutocompleteProvider implements it from its skill: commands.
  • packages/tui/src/components/editor.ts: LayoutLine carries logicalLine/startIndex; EditorTheme.mention? styles resolved mention ranges. Row composition moved to packages/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.

Why

  • senpi#1778: after one $skill the popup no longer opened for a later $, and a resolved mention was indistinguishable from prose.

Why an extension could not handle it

  • The editor owns row composition and the popup trigger policy.

Expected merge conflict zones

  • MEDIUM: editor.ts render() cursor branch (replaced by renderEditorLine) and the layoutText pushes; LOW: autocomplete.ts interface.

2026-09-14 - Out-of-band tmux frame anchors (#1645)

What changed

  • packages/tui/src/terminal.ts selects an injectable tmux CLI cursor source when TMUX_PANE is set. packages/tui/src/tmux-cursor-query.ts accepts 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.

Why

  • 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.

Why an extension could not handle it

  • packages/tui/src/terminal.ts owns the cursor broker and renderer calibration lifecycle, below extension input handling. Bare CPR remains forbidden.

Expected merge conflict zones

  • 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.

2026-09-13 - Private cursor calibration and external-output recovery

What changed

  • 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.

Why

  • packages/tui/src/terminal.ts and packages/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.ts and packages/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.

Why an extension could not handle it

  • packages/tui/src/terminal.ts, packages/tui/src/tui.ts, packages/tui/src/tui-main-screen.ts, and packages/tui/src/index.ts: terminal negotiation, write ownership, hardware cursor snapshots, and committed renderer geometry are below extension APIs.

Expected merge conflict zones

  • 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.

2026-09-13 - Regular-mode scoped click dispatch

What changed

  • 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.

Why

  • 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.

Why an extension could not handle it

  • packages/tui/src/tui-main-screen.ts and packages/tui/src/tui.ts: the renderer owns terminal capture, input ordering, overlays, and committed hit geometry.

Expected merge conflict zones

  • 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.

2026-09-13 - Lease intent and fail-closed mouse geometry

What changed

  • 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.

Why

  • packages/tui/src/tui.ts: inline clicks need reliable frame placement without changing renderer defaults or taking permanent terminal ownership.

Why an extension could not handle it

  • packages/tui/src/tui.ts: committed frame geometry, hardware cursor placement, and renderer lifecycle are private renderer state.

Expected merge conflict zones

  • packages/tui/src/tui.ts: one fullRender hook, resize/replay/insert-scroll/multiplexer epoch increments, and stop bookkeeping. Terminal input routing is untouched.

2026-09-13 - Share mouse protocol parsing and retain owned fragments

What changed

  • packages/tui/src/tui-alt-screen.ts: four helper bodies delegate to mouse-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).

Why

  • packages/tui/src/tui-alt-screen.ts and packages/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.

Why an extension could not handle it

  • packages/tui/src/tui-alt-screen.ts, packages/tui/src/index.ts, and packages/tui/src/stdin-buffer.ts: protocol framing and renderer-private helper ownership precede extension input dispatch.

Expected merge conflict zones

  • 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.

2026-09-12 - Carry-forwards from the upstream v0.85.x sync

What changed

  • packages/tui/src/latex.ts stays deleted; upstream f0592205f's seven relational-algebra join symbols now live in packages/tui/src/components/latex.ts, pinned by test/components-latex-relations.test.ts.
  • packages/tui/src/tui.ts keeps PI_DEBUG_REDRAW and the pi-debug.log filename instead of upstream c505f4c19's PI_TUI_DEBUG_REDRAW / pi-tui-debug.log rename.
  • packages/tui/src/tui.ts (TuiBase.logDirectory) keeps its ~/.senpi/agent default, so over-wide crash dumps stay at <home>/.senpi/agent/senpi-crash.log; upstream's os.tmpdir() fallback when no log directory is supplied is not adopted.
  • packages/tui/src/components/loader.ts gains upstream's protected getRenderedIndicator() hook on top of the fork message/indicator formatters.

Why

  • The fork's debug and crash artifacts are documented under the senpi names 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.

Why an extension could not handle it

  • Renderer logging paths, crash dumps, and component internals are not reachable from extensions.

Expected merge conflict zones

  • src/tui.ts env-var and log-filename constants, components/latex.ts symbol tables, and components/loader.ts render hooks.

2026-09-11 - Keep dollar skill hints active across multiline drafts

What changed

  • 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.ts and packages/tui/test/editor-dollar-autocomplete.test.ts: cover later-line provider lookup and paste/follow-up typing through the real Editor surface.

Why

  • 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.

Why an extension could not handle it

  • Logical cursor routing and autocomplete request admission are owned by the standalone TUI Editor/provider path below the interactive extension API.

Expected merge conflict zones

  • LOW: packages/tui/src/dollar-invocation-autocomplete.ts context gate and the focused dollar/editor tests.

2026-09-11 - Offer skill hints for valid dollar tokens in prompt text

What changed

  • 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 $skill insertion.

Why

  • 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 $HOME and $1 must remain literal.

Why an extension could not handle it

  • Dollar token extraction and completion arbitration run inside the standalone TUI autocomplete provider before interactive-mode extensions receive the editor event.

Expected merge conflict zones

  • LOW: packages/tui/src/dollar-invocation-autocomplete.ts around token extraction and shell-variable classification.

2026-09-10 - Use native TypeScript builds for omob performance

What changed

  • packages/tui/package.json: build uses tsgo for the emitted workspace build.

Why

  • The native compiler reduces omob build time without changing runtime JavaScript.

Why this lives in the fork

  • The package build manifest owns the compiler used by the fork's release pipeline.

Expected merge conflict zones

  • The build script in packages/tui/package.json.

2026-09-04 - Port upstream terminal capability overrides

What changed

  • packages/tui/src/terminal-image.ts: adds an environment-based capability detection path (detectCapabilitiesFromEnvironment with 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 the PI_HYPERLINKS, PI_IMAGE_PROTOCOL, and PI_TRUE_COLOR overrides, and merges stored overrides in getCapabilities; setCapabilityOverrides replaces the overrides and resets the cache (upstream e86823096 #8665 and 649214477 #8828).
  • packages/tui/src/index.ts: exports setCapabilityOverrides.

Why

  • 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.

Why an extension could not handle it

  • Capability detection and caching run inside the TUI package before components render; no extension seam sits between environment detection and the cached capabilities.

Expected merge conflict zones

  • MEDIUM: packages/tui/src/terminal-image.ts detection, environment fallback, and override merging; LOW: packages/tui/src/index.ts export list.

2026-09-04 - Wrap the SIGWINCH self-signal

What changed

  • packages/tui/src/terminal.ts: refreshTerminalDimensions wraps the self-directed SIGWINCH in a try/catch that skips the refresh on failure, and ProcessTerminal.start uses it, so environments whose seccomp or LSM policies deny kill for the process no longer crash startup (upstream 605a1b038, #8898).

Why

  • 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.

Why an extension could not handle it

  • The signal is sent from ProcessTerminal construction, below every extension hook.

Expected merge conflict zones

  • LOW: packages/tui/src/terminal.ts refreshTerminalDimensions and its call site in ProcessTerminal.start.

2026-09-04 - Order nested autocomplete results deterministically

What changed

  • packages/tui/src/autocomplete.ts: fuzzy file completion merges a depth-1 listing of the base directory ahead of the recursive fd results (deduplicated), walkDirectoryWithFd accepts a maxDepth, and equal-score results tie-break by shallower depth, then shorter path, then locale order (upstream b37ebb7f2, #8669).

Why

  • Nested results previously interleaved unpredictably when scores tied; shallow matches first mirrors shell completion expectations and makes the ordering deterministic.

Why an extension could not handle it

  • The fd-backed provider internals score and order suggestions inside the TUI package.

Expected merge conflict zones

  • LOW: packages/tui/src/autocomplete.ts suggestion merge and sort comparators.

2026-09-04 - Fullscreen selection word joining and copy-on-select control

What changed

  • packages/tui/src/tui-alt-screen.ts: adds a copyOnSelect option (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 out getActiveSelectionText, copyActiveSelectionToClipboard, and hasActiveSelection for keyboard-driven copy (upstream 4e4949299 #8731 and 1ac6128e6 #8676).

Why

  • 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.

Why an extension could not handle it

  • Alt-screen viewport mouse handling and clipboard writes are TUI-internal.

Expected merge conflict zones

  • MEDIUM: packages/tui/src/tui-alt-screen.ts word-selection joining and the mouse-release copy path.

2026-08-29 - Preserve upstream terminal input fixes

What changed

  • packages/tui/src/components/editor.ts and packages/tui/src/terminal.ts retain the current upstream terminal input behavior after synchronizing main.

Why

  • The PR merge must preserve both the upstream terminal changes and the callback lifecycle repair.

Why an extension could not handle it

  • Terminal input normalization and editor dispatch are owned by the TUI runtime.

Expected merge conflict zones

  • packages/tui/src/components/editor.ts and packages/tui/src/terminal.ts.

Warp on WSL LF is normalized to Shift+Enter (2026-08-24)

What changed

  • packages/tui/src/components/editor.ts applies the existing CSI-u Shift+Enter sequence to a standalone LF only while the multiline editor handles it. ProcessTerminal forwards 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.

Why

  • 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.

Why an extension could not handle it

  • Coding-agent extensions can transform raw input through onTerminalInput, but that hook cannot correct the shared ProcessTerminal semantics for other TUI consumers or guarantee the default behavior without optional extension loading. The terminal layer is the single cross-consumer seam.

Expected merge conflict zones

  • LOW: packages/tui/src/terminal.ts at forwardInputSequence() and its normalization helpers, packages/tui/src/mux.ts at shared multiplexer detection, and packages/tui/test/terminal.test.ts beside the existing native Shift+Enter normalization coverage.

2026-08-27 - Preserve Windows Terminal scrollback during resize redraws

What changed

  • packages/tui/src/tui.ts: Windows full redraws continue to clear and repaint the visible screen, but no longer emit ESC[3J, which deletes the user's terminal scrollback buffer on ConPTY. Non-Windows non-multiplexer redraws retain their existing scrollback-clearing behavior.

Why

  • 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.

Why this lives in the fork

  • The platform-specific redraw guard belongs in the TUI renderer's fullRender() path, where screen clearing and scrollback deletion are emitted together.

Expected merge conflict zones

  • LOW: packages/tui/src/tui.ts around TuiBase.doRender() and the fullRender() scrollback-clear guard.
  • LOW: packages/tui/test/mux-scrollback.test.ts around resize scrollback emission assertions.

Dead-terminal detection reads Bun's errno-in-message shape (2026-08-26)

What changed

  • packages/tui/src/terminal.ts: isDeadTerminalError() gains a third, last-resort branch that parses a trailing errno: <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) and EPIPE (32). ENOTCONN is deliberately left out of the numeric set because its value differs per platform (57 on darwin, 107 on linux). The existing string code and numeric errno branches are unchanged and still win first, and any error that matches none of the three branches still propagates out of ProcessTerminal.stop().
  • test/terminal.test.ts pins the real Bun shape (a bare new Error("setRawMode failed with errno: 5") with neither code nor errno), the errno: 32 message form, and two rethrow fences: an unrelated new Error("boom") and a live-but-unrelated errno: 22 message.

Why

  • Bun 1.4.0's tty shim throws a plain Error for a failed setRawMode() ioctl: code and errno are 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 escaped ProcessTerminal.stop() into Tui.stop() and stopInteractiveTui(), aborting shutdown and hanging the session with error: setRawMode failed with errno: 5.

Why this lives in the fork

  • Raw-mode ownership and teardown are private ProcessTerminal lifecycle 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.

Expected merge conflict zones

  • LOW: packages/tui/src/terminal.ts around the dead-terminal errno constants and the isDeadTerminalError() body.
  • LOW: packages/tui/test/terminal.test.ts around the ProcessTerminal stop suite.

2026-08-26 - Guard stdin EIO when the controlling terminal detaches

What changed

  • packages/tui/src/terminal.ts arms a process.stdin "error" guard from ProcessTerminal.start() until a 250ms grace window after stop(): 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's code: "EIO" and Bun's raw errno: 5/-5 shapes — 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.

Why

  • 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's ProcessTerminal and adds the numeric-errno shape from the shutdown-time classifier.

Why this lives in the fork

  • The crash topology (launcher chain + orphaned engine) and the Bun runtime shim are fork-owned; the fork's terminal lifecycle differs from upstream's.

Expected merge conflict zones

  • ProcessTerminal.start()/stop() in packages/tui/src/terminal.ts during upstream syncs.

TUI runtime re-diverges from upstream dcd4619 (2026-08-25)

What changed

  • packages/tui/src/components/markdown.ts keeps the fork LaTeX pipeline (latex_block / latex_inline / latex_literal token kinds, latexToUnicode, formula length caps, and word-boundary guards) on top of upstream's renderer.
  • packages/tui/src/terminal.ts keeps dead-terminal detection (EIO/EPIPE/ENOTCONN plus Bun's raw errno-5 macOS tty shim) and the PI_TUI_KEYBOARD_PROTOCOL enhancement gate.

Why

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.

Why this lives in the fork

The divergence lives in core wiring, package identity, or build plumbing that executes before any extension loads, so no extension hook can express it.

Expected merge conflict zones

  • The token-scanner section of packages/tui/src/components/markdown.ts and the raw-mode setup in packages/tui/src/terminal.ts.

Alt-screen Kitty teardown keeps its disambiguated helper name after the 59a71b23 sync (2026-08-19)

What changed

  • packages/tui/src/tui-alt-screen.ts: re-diverges from upstream 59a71b235d by exactly one identifier. The private teardown helper stays deleteAltScreenKittyImages() (upstream calls it deleteKittyImages()), and both call sites keep the fork name: the stop() 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.

Why

  • 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 whether deleteKittyImages is the imported protocol helper or the class method that gates it on imageProtocol === "kitty".

Why an extension could not handle it

  • TuiAltScreen teardown 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.

Expected merge conflict zones

  • LOW: packages/tui/src/tui-alt-screen.ts — the stop() teardown write, the private helper declaration, and the clearImages ternary in the full-clear path. Upstream edits to the same three hunks resolve by keeping the fork identifier and taking upstream's byte content.

Image markers canonicalize on insert/prune and carry owner payloads across undo (2026-08-18)

What changed

  • packages/tui/src/components/editor.ts: insertImageMarker() renumbers the visible markers to canonical 1..k in reading order (via ImageMarkerRegistry.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]; EditorSnapshot carries an opaque attachmentState captured through the new owner hooks and undo() restores it BEFORE firing the marker-order notification; cursor position is preserved across the renumbering rewrite.
  • packages/tui/src/editor-component.ts: new optional paired snapshotAttachmentState/restoreAttachmentState contract next to onImageMarkersChanged, documented together with the tightened insertImageMarker id semantics.
  • Regression coverage: test/editor-image-marker.test.ts pins out-of-order insert canonicalization, post-prune renumbering, and multi-marker delete+undo payload restoration.

Why

  • 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.

Why an extension could not handle it

  • The marker registry, undo stack, and the id semantics of insertImageMarker are Editor internals below the component contract; extensions cannot renumber marker text or hook the undo pop.

Expected merge conflict zones

  • MEDIUM: insertImageMarker() and the undo snapshot/restore block in packages/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)

What changed

  • 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.ts holds the fork's differential renderer in TuiBase — synchronized autowrap-guarded frames, viewport-bounded normalize/diff, scrollback replay, the insert-scroll fast path, the configurable render fps cap, over-wide containment, the component dispose() 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.ts is reduced to a thin main-screen subclass that owns render-state capture/restore; packages/tui/src/tui-alt-screen.ts differs from the pin only by the deleteAltScreenKittyImages() 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), and packages/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 (messageFormatter animation plus dispose()), packages/tui/src/components/markdown.ts (LaTeX tokenizers and the bounded highlight cache), packages/tui/src/components/select-list.ts (the renderRow theme 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, the coalesceAdjacentSgr utility), and packages/tui/src/index.ts (the fork export surface: paste markers, select-list row types, tmux helpers, markdown cache controls).
  • packages/tui/src/latex.ts is the upstream LaTeX module path, deleted in this fork: the converter was rewritten dependency-free and relocated to packages/tui/src/components/latex.ts (see the relocation entry below).

Why

  • 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.

Why an extension could not handle it

  • 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.

Expected merge conflict zones

  • HIGH: packages/tui/src/tui.ts (TuiBase render paths, scheduler, dispose, focus routing) and packages/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, and packages/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 the packages/tui/src/index.ts export lists; packages/tui/src/latex.ts is a whole-file deletion to reconcile against packages/tui/src/components/latex.ts.

Component-tree disposal bounds long-session cleanup (2026-08-17)

Landed 2026-06-17 (commit 4f6749bb7).

What changed

  • packages/tui/src/tui.ts: Component declares optional dispose?() and Container implements tree-wide disposal — dispose() runs once (guarded by a disposed flag), clear() disposes the children it removes, removeChild() disposes the removed child, and detachAll() detaches without disposing for callers that reuse components.
  • packages/tui/src/components/box.ts: the same contract locally — clear() and removeChild() dispose affected children, dispose() is idempotent, and detachAll() 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, and clearRenderCache() plus highlight call counters are exported through packages/tui/src/index.ts for teardown and tests.
  • Coverage: packages/tui/test/component-dispose.test.ts and packages/tui/test/markdown-highlight.test.ts.

Why

  • 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.

Why an extension could not handle it

  • Component lifecycle and module-level caches are TUI internals; extensions compose components but cannot inject tree-wide teardown or clear renderer-owned caches.

Expected merge conflict zones

  • LOW: the disposal methods in packages/tui/src/components/box.ts and packages/tui/src/components/loader.ts.
  • MEDIUM: packages/tui/src/components/markdown.ts cache accounting; LOW: its packages/tui/src/index.ts re-exports.
  • LOW: the Container method block in packages/tui/src/tui.ts.

SelectList theme renderRow composer (2026-08-17)

Landed 2026-07-26 (commit 8abee395c).

What changed

  • packages/tui/src/components/select-list.ts: SelectListTheme gains optional renderRow, a composer receiving decomposed SelectListRowParts — selection prefix (with selectedPrefix already 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 dead selectedPrefix callback 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.ts exports SelectListRenderRow and SelectListRowParts.
  • 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), and packages/tui/test/editor-render-row.test.ts.

Why

  • 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.

Why an extension could not handle it

  • 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.

Expected merge conflict zones

  • MEDIUM: packages/tui/src/components/select-list.ts around composeRow() and the theme interface.
  • LOW: the theme plumbing in packages/tui/src/components/editor.ts and the packages/tui/src/index.ts export list.

Fuzzy matcher hot path and alphanumeric swap variants (2026-08-17)

Landed 2026-06-08 (commit af0ab07a0).

What changed

  • packages/tui/src/fuzzy.ts: fuzzyMatch scoring moved from a per-call closure into a top-level scoreMatch, and the per-character regex word-boundary test became char-code classification (isWordBoundaryPrefix). The whole-token letter/digit swap regex is generalized into buildAlphanumericSwapQueries(): every adjacent letter/digit transposition plus whole-token swaps, each scored with the flat ALPHANUMERIC_SWAP_PENALTY (5), best matching variant wins — so queries like gpt5a match gpt-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.ts pins the adjacent-swap case.

Why

  • 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.

Why an extension could not handle it

  • fuzzyFilter is the ranking primitive inside the shared autocomplete and selector stack; extensions receive filtered lists and cannot replace the matcher.

Expected merge conflict zones

  • MEDIUM: scoring and swap-variant construction in packages/tui/src/fuzzy.ts; upstream edits to the same functions will conflict textually.

LaTeX converter relocated under components (2026-08-17)

Landed 2026-07-29 (commit 5655c1cd8).

What changed

  • 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 at packages/tui/src/components/latex.ts, beside its only consumer, the Markdown tokenizers in packages/tui/src/components/markdown.ts.
  • packages/tui/src/index.ts no longer re-exports renderLatex from the old path; conversion is internal to the Markdown component (the paste-marker exports took that slot).

Why

  • 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.

Why an extension could not handle it

  • Math tokenization happens inside the Markdown component before extension-facing UI hooks; consistent rendering across every Markdown consumer requires the parser seam.

Expected merge conflict zones

  • The deleted packages/tui/src/latex.ts is a whole-file divergence: an upstream sync touching it must reconcile against packages/tui/src/components/latex.ts. LOW: the packages/tui/src/index.ts export slot.

Fullscreen focus routing and the PR #892 v0.84.2 sync repairs (2026-08-17)

Landed 2026-08-16 (commit 03f46f57e, shipped in PR #892).

What changed

  • packages/tui/src/tui.ts: TuiBase.handleTerminalInput() consumes tmux focus events only when mode !== "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 TuiBase focus interception forced a redraw before the alt-screen selection logic ran. The routing above is the fork-side repair; b25d5bdeb realigned 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.

Why

  • 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.

Why an extension could not handle it

  • Focus events are consumed inside the renderer's input path before any component or extension sees the bytes.

Expected merge conflict zones

  • MEDIUM: the handleTerminalInput() focus branch in packages/tui/src/tui.ts. LOW: packages/tui/src/index.ts import ordering.

Selection copy routes through the host clipboard (2026-08-17)

What changed

  • packages/tui/src/tui-alt-screen.ts carries 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.

Why

  • 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.

Why an extension could not handle it

  • Selection copy executes inside the fullscreen renderer's mouse/selection handler; no extension seam intercepts terminal mouse bytes.

Expected merge conflict zones

  • LOW: the deleteAltScreenKittyImages() rename sites; the clipboard path itself is upstream-owned.

Generic SGR mouse releases finish selection (2026-08-17)

What changed

  • packages/tui/src/tui-alt-screen.ts carries upstream v0.84.2's generic SGR mouse-release handling (upstream issue #7963, delivered by the PR #892 sync): handleSelectionMouseEvent accepts 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.

Why

  • 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.

Why an extension could not handle it

  • SGR mouse parsing and selection state are private to the fullscreen renderer's input path.

Expected merge conflict zones

  • LOW: the release guard in handleSelectionMouseEvent; upstream-owned otherwise.

2026-08-16: add a prompt-leading mixed dollar invocation picker (PR #909)

What changed

  • CombinedAutocompleteProvider recognizes 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 /command rows before $skill rows 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.

Why

  • OmO Desktop and Senpi RPC now expose one mixed command/skill surface; the terminal needs the same invocation affordance without teaching providers a new $command execution syntax.
  • Canonical insertion keeps existing slash command dispatch and the shared dollar skill parser authoritative.

Expected merge conflict zones

  • MEDIUM: autocomplete.ts trigger ordering and completion replacement.
  • LOW: components/editor.ts default autocomplete trigger characters.
  • LOW: additive dollar-invocation-autocomplete.ts and its focused test.

2026-08-14: replay above-viewport growth in the viewport-remap branch

What changed

  • When a frame's content grows above the viewport and a visible row also changes (viewportTop !== prevViewportTop with lineCountDelta !== 0), the renderer now falls back to the canonical renderScrollbackReplay / mux dispatch instead of repainting only the visible rows in place.

Why

  • The in-place repaint emitted exactly height rows and returned, so rows inserted above the viewport (e.g. Ctrl+O expanding several tool blocks in one frame) never reached terminal scrollback even though setPreviousLines marked them painted — leaving mismatched headers and truncated results. The replay path re-emits the full canonical transcript.

Expected merge conflict zones

  • LOW: tui.ts the viewportTop !== prevViewportTop branch; LOW in tui-render.test.ts.

2026-08-05: dead-terminal raw-mode restoration is best-effort during shutdown

What changed

  • packages/tui/src/terminal.ts: ProcessTerminal.stop() still restores the raw-mode state captured by start(), but now treats EIO, EPIPE, and ENOTCONN from the teardown-time setRawMode() 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 positive errno: 5 shape, using numeric errno only when no string code is available.
  • The separate coding-agent classifier handles asynchronous stdout/stderr stream error events; this numeric fallback stays scoped to the synchronous stdin setRawMode() 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.ts covers successful restoration, the dead-terminal EIO regression, and unexpected-error propagation.

Why

  • 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: 5 stack.

Why this cannot be expressed externally

  • Raw-mode ownership and restoration are private ProcessTerminal lifecycle responsibilities. Extensions receive neither the saved raw-mode state nor a teardown hook around the stdin ioctl.

Expected merge conflict zones

  • LOW: packages/tui/src/terminal.ts around the terminal error classifier and ProcessTerminal.stop() raw-mode restoration.
  • LOW: packages/tui/test/terminal.test.ts around lifecycle coverage.

2026-07-31: atomic visible-cursor frames for IME and animations

What changed

  • 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_MARKER for IME placement.
  • The renderer also removes a colocated inverse-video cursor after CURSOR_MARKER, covering focused single-line Input consumers 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.

Why

  • 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.

Expected merge conflict zones

  • HIGH: tui.ts synchronized render exits and cursor positioning.
  • LOW: components/editor.ts cursor rendering.

2026-07-31: memoized line normalization and viewport-bounded rendering by default

What changed

  • tui.ts reuses normalizeTerminalOutput results 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.ts viewportRenderEnabled() now defaults on; PI_TUI_VIEWPORT_RENDER=0 opts out of viewport-bounded normalize+diff and 1 still forces it on. Output byte-equivalence between the bounded and full paths is pinned by test/viewport-render.test.ts (streaming, offscreen line-count changes, offscreen in-place mutations).
  • scripts/perf-trend-local.sh pins the two baseline frame-cost lanes to PI_TUI_VIEWPORT_RENDER=0 so their historical meaning (unbounded full pass) survives the default flip.
  • bench/frame-cost.ts 300-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.ts proves 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.ts pins the default-on/opt-out switch semantics.

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • MEDIUM: tui.ts normalizeLine() / applyLineResets() bodies and the render-state field block.
  • LOW: mux.ts viewportRenderEnabled(), test/mux.test.ts, test/viewport-render.test.ts, scripts/perf-trend-local.sh bench lanes.

2026-07-31: Contextual skill slash-command discovery

What changed

  • Bare / no longer lists every skill:<name> command, and partial /skill input exposes one skill: 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.

Why

  • The shared skill: prefix flooded the root slash-command overview and obscured the smaller set of general commands, while filtering every child also left /skill as a discoverability dead end.

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • LOW: slash-command-autocomplete.ts skill filtering and its focused autocomplete regression test.

2026-07-29: Native Unicode LaTeX in Markdown conversations

What changed

  • components/markdown.ts registers 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.ts converter 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.ts proves ordinary-text boundaries, nested/budgeted conversion, streamed partial currency/shell and inline-code frames, CJK wide cells, inherited styles, malformed preservation, and focused VirtualTerminal cell widths including a column-zero combining mark.

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • MEDIUM: components/markdown.ts parser construction and custom token branches.
  • LOW: components/latex.ts symbol/script conversion tables and test/markdown.test.ts LaTeX cases.

2026-07-28: over-wide diagnostics stop rescanning settled large histories

What changed

  • tui.ts now 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 through visibleWidth() after the one-shot dump has already been written.
  • __renderDiagnosticStats() exposes diagnostic line-scan counts only under PI_TUI_TEST_SEAMS=1.
  • test/render-contract.test.ts proves the first over-wide release frame scans diagnostic input and a second frame neither writes nor rescans the transcript.

Why

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.

Expected merge conflict zones

  • LOW: tui.ts around release-mode over-wide truncation and crash diagnostics.
  • LOW: test/render-contract.test.ts over-wide release behavior.

2026-07-28: setText prunes instead of clearing the paste registry; paste-state transfer API

What changed

  • components/editor.ts setText() 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 programmatic getText()setText() round-trip stay live: they remain atomic segments and still expand to the full pasted body on submit and in getExpandedText().
  • 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 use setPasteState(), which skips the provenance check by design.
  • New getPasteState() / setPasteState() on Editor plus optional getPasteState?/setPasteState? on the EditorComponent interface (exported EditorPasteState): 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 with setPasteState but no getPasteState as paste-unaware, because it could not re-export collapsed markers on a later hand-off.
  • The submit/getExpandedText() expansion logic is extracted as the exported expandPasteMarkers(text, state) helper so consumers holding a paste snapshot (e.g. an editor hand-off where the source lacks getExpandedText) 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 setText round-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.

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • LOW: components/editor.ts setText(), prunePastes(), formatPasteMarker(), getPasteState()/setPasteState(), and the handlePaste marker-insertion line.
  • LOW: editor-component.ts optional paste-state methods; index.ts EditorPasteState export.
  • LOW: test/editor.test.ts paste marker suite.

2026-07-17: Kitty graphics through tmux passthrough

What changed

  • terminal-image.ts: detectCapabilities no longer hard-disables images under tmux. It probes the effective #{allow-passthrough} value for the current pane (plus #{client_termname}) via tmux display-message -p; when passthrough is on/all and the outer terminal implements the Kitty graphics protocol (kitty/Ghostty/WezTerm via client_termname or leaked env hints), capabilities become images: "kitty", tmuxPassthrough: true. Both probes are dependency-injectable for tests.
  • terminal-image.ts: new exported wrapTmuxPassthrough(sequence) wraps a sequence in a tmux DCS envelope (ESC Ptmux; … ESC \ with every payload ESC doubled). encodeKitty wraps each APC chunk individually and deleteKittyImage/deleteAllKittyImages wrap their delete commands when tmuxPassthrough is 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) get kittyUnicodePlaceholders: true: encodeKitty gains a virtual option (U=1 virtual placement), buildKittyPlaceholderRow emits U+10EEEE cells with row/column (and id high-byte) diacritics plus the image id in the 24-bit foreground color, and renderImage returns per-row lines (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|direct overrides the heuristic. The Image component uses result.lines when present instead of one sequence line plus empty padding rows.
  • terminal-image.ts: the tmux probe also reports client_cell_width/client_cell_height; when tmux images are enabled the detected cell size is adopted via setCellDimensions because tmux never answers the CSI 16 t cell-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 recommending allow-passthrough is useful for the attached terminal.
  • utils.ts: extractAnsiCode learned DCS sequences (ESC P … ST), skipping doubled-ESC pairs so the escaped inner ST does not terminate the envelope early. Wrapped image lines therefore keep visibleWidth === 0 and stay compatible with the TUI's Kitty image-line bookkeeping (id/row extraction in tui.ts uses indexOf("\x1b_G"), which still matches inside the doubled-ESC payload).

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • MEDIUM: terminal-image.ts tmux branch of detectCapabilities, encodeKitty chunk assembly, and renderImage kitty branches.
  • LOW: utils.ts extractAnsiCode escape-sequence branches; components/image.ts kitty line assembly.
  • LOW: index.ts terminal-image export list; test/terminal-image.test.ts tmux capability tests.

2026-07-26: composable leading skill autocomplete

What changed

  • autocomplete.ts reopens 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.

Why this cannot be expressed externally

The shared autocomplete provider owns the suggestion and insertion decisions that editor consumers use before the coding-agent session receives a prompt.

Expected merge conflict zones

  • LOW: autocomplete.ts slash-command suggestion and completion branches. This fork-local diff is deliberately minimal because the file is shared with upstream pi.

2026-07-19: configurable render fps cap and shared segmenter exports

What changed

  • 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) plus setMaxRenderFps(fps): fps is clamped to 30-120 and stored as Math.floor(1000 / fps) (120fps ⇒ 8ms interval).
  • packages/tui/src/index.ts: exports getGraphemeSegmenter and getWordSegmenter from utils.ts so consumers (smooth-streaming reveal in coding-agent) share the single Intl.Segmenter instances.
  • Tests: packages/tui/test/render-fps-cap.test.ts (mocked-timer throttle-delay assertions) and packages/tui/test/segmenter-exports.test.ts (root re-export identity).

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • LOW: packages/tui/src/tui.ts around the scheduler field declarations and scheduleRender().
  • LOW: packages/tui/src/index.ts around the utils.ts re-export list.

2026-07-04: terminal ownership and restart hardening

What changed

  • packages/tui/src/terminal.ts (+ index.ts export): ProcessTerminal accepts onExternalStdoutWrite. While started, process.stdout.write is 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 on stop(), and a throwing handler falls back to raw stdout so output is never lost.
  • packages/tui/src/terminal.ts: setTitle strips 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: renderRequested and inputRenderPending are reset in both stop() and start(). A render requested within the pending window (nextTick or the 16ms throttle) or while stopped left renderRequested set, so every plain requestRender() after restart silently no-oped until a keypress.

Why this cannot be expressed externally

  • stdout ownership, OSC emission, and render-scheduling flags are ProcessTerminal/TUI internals; components and extensions cannot patch process streams or reset private scheduler state safely.

Expected merge conflict zones

  • MEDIUM: packages/tui/src/terminal.ts around start()/stop() stream handling and setTitle.
  • LOW: packages/tui/src/tui.ts stop()/start() scheduling-state resets.
  • LOW: packages/tui/test/external-stdout-guard.test.ts, packages/tui/test/terminal.test.ts.

2026-07-03: TUI rendering excellence gates

What changed

  • 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.

Why this cannot be expressed externally

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.

Expected merge conflict zones

  • HIGH: packages/tui/src/tui.ts around doRender(), fullRender(), renderViewportInsertScroll(), renderScrollbackReplay(), positionHardwareCursor(), and render-error diagnostic handling.
  • MEDIUM: packages/tui/src/utils.ts around width caching, terminal-output normalization, and ANSI parsing helpers.
  • LOW: packages/tui/test/tui-render.test.ts flicker-budget and scrollback assertions when upstream changes renderer byte expectations.

2026-07-02: autowrap disabled during frame writes (ghost-line fix)

What changed

  • In packages/tui/src/tui.ts, every frame write is bracketed by TUI.FRAME_BEGIN (DECSET 2026 + DECRST 7) and TUI.FRAME_END (DECSET 7 + DECRST 2026) instead of bare synchronized-output markers.
  • New regression: packages/tui/test/regression-wrap-desync-ghost-line.test.ts.

Why

  • 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 as Working (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.

Expected upstream conflict zone

  • MEDIUM: every let buffer = "\x1b[?2026h" / buffer += "\x1b[?2026l" site in TUI.doRender(), fullRender(), renderViewportInsertScroll(), and renderScrollbackReplay() — upstream edits to those literals will conflict with the FRAME_BEGIN/FRAME_END constants.

2026-05-20: Loader message animation is part of the shipped normal TUI

What changed

  • packages/tui/src/components/loader.ts supports messageFormatter with 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.

Why this cannot be expressed externally

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.

Expected upstream conflict zone

  • HIGH: packages/tui/src/components/loader.ts around LoaderIndicatorOptions, setIndicator(), restartAnimation(), and updateDisplay().
  • HIGH: package/release wiring that decides whether @code-yeongyu/senpi bundles this forked TUI runtime or installs upstream npm @earendil-works/pi-tui.

2026-05-18: flicker-free scrollback replay for offscreen expansion

What changed

  • In packages/tui/src/tui.ts TUI.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.

Why

  • Terminal scrollback rows above the visible viewport cannot be rewritten in place. The earlier fork-only differential remap updated previousLines without 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 both ESC[2J and ESC[3J and validates the newest canonical suffix instead of trying to delete historical rows.

Expected merge conflict zones

  • HIGH: TUI.doRender() around the firstChanged < prevViewportTop branch, because this preserves the fork's no-viewport-clear behavior while adding a scrollback-only replay path.
  • LOW: packages/tui/test/tui-render.test.ts under TUI viewport remap for above-viewport growth.

2026-05-15: in-place repaint for above-viewport collapse

What changed

  • In packages/tui/src/tui.ts TUI.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 forcing fullRender(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.

Why

  • 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.

Expected merge conflict zones

  • MEDIUM: TUI.doRender() around the firstChanged < prevViewportTop remap branch, because this fork already carries upstream-divergent differential repaint logic there.
  • LOW: packages/tui/test/tui-render.test.ts under TUI viewport remap for above-viewport growth.

2026-05-11: insert-scroll fast path for expanded streaming output

What changed

  • In packages/tui/src/tui.ts TUI.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.

Why this cannot be expressed externally

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.

Expected upstream conflict zone

  • packages/tui/src/tui.ts near the viewport remap and differential render branches in doRender().
  • packages/tui/test/tui-render.test.ts in TUI viewport remap for above-viewport growth.

2026-05-10: viewport remap repaint fix for Ctrl-O expansion

What changed

  • In packages/tui/src/tui.ts TUI.doRender(), above-viewport growth that remaps viewportTop now 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[3J appears and verifies visible expanded rows are repainted while DECSET 2026 remains balanced.

Why this cannot be expressed externally

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.

Expected upstream conflict zone

  • packages/tui/src/tui.ts around the firstChanged < prevViewportTop branch inside doRender() (viewport remap handling and fallback path).
  • packages/tui/test/tui-render.test.ts in TUI viewport remap for above-viewport growth assertions.

What changed

  • 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.

Why this cannot be expressed externally

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.

Expected upstream conflict zones

  • 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: fullRender paths and fullRedrawCount accounting.
  • 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 near message_update and tool_execution_update.

Test surface added

  • flicker budget under streaming in packages/tui/test/tui-render.test.ts verifies:
    • full clear sequence count stays at the initial render only,
    • ANSI escape bytes remain below the content-byte budget,
    • every DECSET 2026 begin has a matching end,
    • no fullRender(true) equivalent clear occurs after the init phase.

Atomic image markers for clipboard-pasted images (2026-08-18)

What changed

  • packages/tui/src/image-markers.ts (new): ImageMarkerRegistry tracks 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 contiguous 1..k sequence (via canonicalize()), exposes authorizedMarkers() for markers occurring exactly once (the only ones safe to treat as atomic), and supports single-occurrence removal plus EditorImageState snapshots 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, and onImageMarkersChanged reports the ids in text reading order whenever markers are added, removed, pruned, or renumbered.
  • packages/tui/src/editor-component.ts: the EditorComponent interface 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).

Why

  • 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.

Why an extension could not handle it

  • 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.

Expected merge conflict zones

  • MEDIUM: packages/tui/src/components/editor.ts (segment handling around cursor movement and deletion) and packages/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 the packages/tui/src/index.ts export lists.

2026-09-03 - Port upstream #8028 bounded main-screen rendering

What changed

  • 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.

Why

  • 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 thin TuiMainScreen subclass.

Why this lives in the fork

  • TuiBase owns 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.

Expected merge conflict zones

  • packages/tui/src/tui.ts around full-render and differential-render terminal writes; packages/tui/src/tui-main-screen.ts remains a thin state-capture subclass.

2026-09-12 - Upstream sync (upstream/main@71dca871) integration repairs

What changed

  • packages/tui/src/components/box.ts: fork Container disposal semantics (dispose() idempotent via a disposed flag, 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/removeImageMarker with renumbering), undo snapshots carrying attachment payloads, normalizeWarpWslShiftEnterInput seam and the @/#/$ autocomplete triggers, on top of upstream's mouse selection.
  • packages/tui/src/components/select-list.ts: fork SelectListRowParts/renderRow row composer and ranking beside upstream's mousePressedIndex/handleMouse/getVisibleRange.
  • packages/tui/src/index.ts: fork exports (fullscreen transcript search, atomic image markers, expandPasteMarkers, ProcessTerminalOptions, calculateImageRows, sanitizeTerminalLabel/shortenImagePath) with upstream's MouseRegion/native clipboard exports.
  • packages/tui/src/terminal.ts: fork dead-terminal detection (EIO/EPIPE/ENOTCONN codes, Bun errno fallbacks), shared stdin error dispatcher, keyboard-enhancement state, Warp/WSL shift+enter normalization, ProcessTerminalOptions.onExternalStdoutWrite, multiplexer detection; upstream's getNativePlatformHelper() VT-input path was adopted.
  • packages/tui/src/tui-alt-screen.ts: the fork keeps the deleteAltScreenKittyImages teardown 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's getActiveBackgroundAnsi.

Why

  • 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.

Why an extension could not handle it

  • These are the TUI library primitives every component and the coding agent build on.

Expected merge conflict zones

  • HIGH: packages/tui/src/components/editor.ts marker handling and input dispatch; packages/tui/src/terminal.ts ProcessTerminal start/stop.
  • MEDIUM: packages/tui/src/index.ts export list; packages/tui/src/utils.ts width cache and ANSI helpers; select-list.ts render path.
  • LOW: box.ts lifecycle methods; tui-alt-screen.ts teardown call sites.