You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A decision on, and a phased migration for, replacing the @jrichman/ink fork pinned in this repository ("ink": "npm:@jrichman/ink@6.4.8", root package.json:333 and packages/cli/package.json:90) with upstream ink, plus the porting work that requires.
Proposed sequence:
P0, inventory and spike. Build a spike branch against one pinned upstream release (7.1.1 as of 2026-08-25), catalog every compile failure and behavior gap, and publish the list. No mainline dependency change. P0 must also validate the P1 inventory below: it was assembled by source inspection during the Every completed shell tool call permanently retains its child process and ~2.56 MB of output buffers #3329 investigation and may be missing further render-option, type, or behavior differences. P0 must additionally evaluate whether any community viewport package (ink-scroll-view, ink-scroll-list, ink-virtual-list; all exist on npm and declare Ink 7 compatibility) can supply clipping, translation, or scrollbar primitives before we build a full viewport layer; record the specific missing contracts if all are rejected.
Selection go/no-go gate. After P0 and the selection design note below, maintainers approve or reject the interim selection architecture before P1 implementation begins. A valid outcome is to stop the migration, patch the current fork instead, or wait for a released upstream selection bridge if the proposed implementation requires broad UI rewrites or private Ink internals.
P1, isolate fork-only surfaces while still building on the fork. Replace or wrap each confirmed fork-only dependency so production code no longer imports it:
selection model and hit testing: Range, comparePoints, hitTest, DOMNode, and useApp().selection are imported from ink in packages/cli/src/ui/hooks/useMouseSelection.ts; clipboard text comes from Selection.toString(); scroll targeting reads internal_scrollState and fork internals from the same file;
sticky headers: the sticky, stickyChildren, and opaque Box props in StickyHeader.tsx (used for tool message headers in ToolMessage.tsx) do not exist upstream;
scroll plumbing: VirtualizedList.tsx already owns scroll state but pushes translation, clipping, and scrollbar rendering into fork core via overflowY="scroll", scrollTop, and scrollbarThumbColor; upstream core is clip-only, so this needs an app-owned viewport layer that preserves the existing anchor, stick-to-bottom, imperative scrolling, smooth-scroll, wheel, keyboard, and thumb-drag contracts, and exposes scroll-coordinate translation for selection through an app-owned interface;
coordinates: fork getBoundingBox (used in useMouseSelection.ts:98,129, ScrollProvider.tsx, and useMouseClick.ts) walks ancestors and subtracts their scroll offsets for scroll boxes (visible in the installed node_modules/ink/build/measure-element.js). Upstream measureElement (Cancel does not drain queued tool calls; execution continues after user cancellation #968, released in 7.1.1) returns element boxes in live-layout coordinates and expects callers to convert using the live region's viewport anchor, so the replacement is an app-owned geometry service, not a drop-in rename: it must translate live-layout coordinates into terminal mouse coordinates, account for app-owned scroll offsets, and handle the inline case where <Static> output sits above the live region. P0 must prove how the viewport anchor is obtained;
render-mode plumbing: the repository's useAlternateBuffer setting is passed as fork option alternateBuffer (packages/cli/src/ui/inkRenderOptions.ts:137), read by mouseEventsEnabled.ts:18-21, and logged in startup diagnostics (interactiveUI.tsx:530). Upstream names this option alternateScreen, with lifecycle differences: map the option, update mouseEventsEnabled.ts and diagnostics, and characterize startup, resize, clear, failure rollback, and teardown behavior before the swap.
P2 + P3, one merge gate: dependency swap behind acceptance. P2 flips both manifests to the chosen upstream release, updates bun.lock and package-lock.json, audits ink-gradient, ink-spinner, and ink-testing-library for compatibility with the selected release and React 19.2, removes ink-select-input (declared in both manifests, imported nowhere), and migrates the test harness (below). P3 defines acceptance. The swap branch must not merge until every P3 check passes or the corresponding behavior change is explicitly approved.
Test harness migration: most bun tests redirect bare ink and ink-testing-library imports to a stub (packages/cli/bun-test-setup.ts and the tsconfig.bun.json paths alias), so they prove nothing about a replacement package. A smaller set already exercises the real renderer through private build paths (test-utils/real-ink.ts, test-utils/ink-testing-library.ts). The acceptance configuration must bypass both redirect mechanisms, use supported imports instead of node_modules/ink/build/... deep paths, set the intended interactive mode (our custom test Stdout exposes columns but not isTTY; confirm how upstream's renderer treats that), and run the real package.
Acceptance checks: typecheck and build against the real package; real-ink component tests without the stub; PTY tests for inline mode; wheel/keyboard/thumb-drag scrolling; drag selection and copy; virtualized-list anchors, resize, stick-to-bottom, and variable-height behavior; sticky tool headers or an explicitly approved replacement; wide/combining characters, wrapped and nested text, and selection across boundaries; <Static> remount, clear, and long-output cases; screen-reader mode; and a new heap regression that repeatedly commits static content and asserts retained heap stays bounded (the shell heap-threshold tests from PR Eliminate per-execution shell retention and bound secondary accumulators #3343 do not cover Ink). Also: Bun execution and bundling (not only Node >= 24), both raw-package and published prebuilt-bundle launch paths, alternate-screen entry and restoration on normal exit, startup failure, signals, and resize, custom stdout/stderr proxy compatibility, incremental rendering and render-metrics callbacks, macOS/Linux/Windows differences where CI or PTY infrastructure permits, render latency and retained heap after replacing fork-native scrolling with app rendering, and license/attribution review if fork algorithms are copied into our source.
Selection port design note (gated deliverable before P1 selection work). The fork's hitTest/Range/Selection.toString() provide cell-to-text hit testing and copy reconstruction. Upstream has no public mechanism for cell-to-text mapping or renderer-level highlighting as of 7.1.1. The interim implementation needs a design covering: hit testing from screen coordinates to text offsets; copy reconstruction across wrapping, nested Text, transforms, ANSI styling, wide and combining characters, and current (not aspirational) selection coverage of virtualized or scrolled-off content; and highlight painting without rewriting every text-producing component. This is the largest open design risk in the migration; the existence of #984/#985 upstream reflects the same gap.
Non-goals: carrying two UI implementations long-term (temporary migration scaffolding during P1 is acceptable and should be deleted), rewriting the UI layer, and upstreaming our code beyond feedback on #984/#985.
Fork ownership cost. This repository pins fork 6.4.8 while the fork has since published 7.1.0 (last push 2026-06-24) and has diverged from upstream master in both directions: it carries fork-only work and is missing upstream fixes, including the memory family above at our pinned version. Staying on the fork means either synchronizing work we do not currently perform, or accepting that divergence permanently.
Patch installed 6.4.8 via Bun patchedDependencies (option recorded in project-plans/issue3329/plan.md): the smallest repository-local mitigation for the fullStaticOutput defect. Its coverage across the published prebuilt bundle, raw package execution, and npm-installed consumers using package-lock.json must be verified, as must behavior in the redraw and clear paths; it preserves the fork API surface, not automatically every behavior, and leaves the divergence and all other upstream fixes unaddressed.
Upgrade to the fork's current 7.x and fix the leak there: may retain the fork-only scroll, sticky, and selection APIs; requires compatibility testing and confirming which upstream fixes the fork incorporated; fork ownership risk remains either way.
Switch to upstream ink (this proposal): resolves the memory family at its source and ends fork ownership; requires replacing everything in the P1 inventory, with the interim selection engine as the main cost.
Sequence: patch now, then evaluate migration after P0 and the selection gate: contains the immediate fullStaticOutput defect without committing to an interim selection engine, while keeping a separate path to upstream once replacement cost and release contents are known. This is viable because patching and migrating are not mutually exclusive, and because 7.1.1 itself lacks the unreleased text-cache fix.
This issue recommends the switch, with P0 and the selection gate as explicit exit points. The recommendation is open to challenge on the interim-selection cost; the data above is laid out so the tradeoff can be argued either way.
Data gathered 2026-08-25 (repository source inspection; upstream GitHub and npm via API):
app-owned viewport; community packages to vet in P0
Sticky headers
none
-
-
sticky/stickyChildren/opaque replacement
Alternate screen
alternateScreen option
-
-
rename from fork alternateBuffer plus lifecycle characterization
Mouse input
none in core
-
-
parsing already ours (utils/mouse.ts SGR/X11 parse, MouseContext.tsx stdin buffering) and reusable; mouseEventsEnabled.ts still rides the alternateBuffer migration
Known upstream open bug to watch while migrating: #973 (tall <Static> output can overwrite its last line).
Community-package download counts were not checked and are volatile; check npm at implementation time. Phase-level issues can be split out from P0-P4 once the direction is agreed.
What would you like to be added?
A decision on, and a phased migration for, replacing the
@jrichman/inkfork pinned in this repository ("ink": "npm:@jrichman/ink@6.4.8", rootpackage.json:333andpackages/cli/package.json:90) with upstreamink, plus the porting work that requires.Proposed sequence:
ink-scroll-view,ink-scroll-list,ink-virtual-list; all exist on npm and declare Ink 7 compatibility) can supply clipping, translation, or scrollbar primitives before we build a full viewport layer; record the specific missing contracts if all are rejected.Range,comparePoints,hitTest,DOMNode, anduseApp().selectionare imported frominkinpackages/cli/src/ui/hooks/useMouseSelection.ts; clipboard text comes fromSelection.toString(); scroll targeting readsinternal_scrollStateand fork internals from the same file;sticky,stickyChildren, andopaqueBox props inStickyHeader.tsx(used for tool message headers inToolMessage.tsx) do not exist upstream;VirtualizedList.tsxalready owns scroll state but pushes translation, clipping, and scrollbar rendering into fork core viaoverflowY="scroll",scrollTop, andscrollbarThumbColor; upstream core is clip-only, so this needs an app-owned viewport layer that preserves the existing anchor, stick-to-bottom, imperative scrolling, smooth-scroll, wheel, keyboard, and thumb-drag contracts, and exposes scroll-coordinate translation for selection through an app-owned interface;getBoundingBox(used inuseMouseSelection.ts:98,129,ScrollProvider.tsx, anduseMouseClick.ts) walks ancestors and subtracts their scroll offsets for scroll boxes (visible in the installednode_modules/ink/build/measure-element.js). UpstreammeasureElement(Cancel does not drain queued tool calls; execution continues after user cancellation #968, released in 7.1.1) returns element boxes in live-layout coordinates and expects callers to convert using the live region's viewport anchor, so the replacement is an app-owned geometry service, not a drop-in rename: it must translate live-layout coordinates into terminal mouse coordinates, account for app-owned scroll offsets, and handle the inline case where<Static>output sits above the live region. P0 must prove how the viewport anchor is obtained;useAlternateBuffersetting is passed as fork optionalternateBuffer(packages/cli/src/ui/inkRenderOptions.ts:137), read bymouseEventsEnabled.ts:18-21, and logged in startup diagnostics (interactiveUI.tsx:530). Upstream names this optionalternateScreen, with lifecycle differences: map the option, updatemouseEventsEnabled.tsand diagnostics, and characterize startup, resize, clear, failure rollback, and teardown behavior before the swap.bun.lockandpackage-lock.json, auditsink-gradient,ink-spinner, andink-testing-libraryfor compatibility with the selected release and React 19.2, removesink-select-input(declared in both manifests, imported nowhere), and migrates the test harness (below). P3 defines acceptance. The swap branch must not merge until every P3 check passes or the corresponding behavior change is explicitly approved.measureText/wrapTextcaches (commitad9e3ea, closing Project-scoped memories saved to .llxprt/LLXPRT.md are not loaded by memory discovery #986 and Tool scheduler hangs on result publishing - race condition in publishBufferedResults #987) landed on master on 2026-08-11, after the 7.1.1 release on 2026-07-16, so 7.1.1 still contains that leak. P2 must pin a published release containing the required memory fixes, or explicitly patch the remaining defects on top of the chosen release.inkandink-testing-libraryimports to a stub (packages/cli/bun-test-setup.tsand thetsconfig.bun.jsonpaths alias), so they prove nothing about a replacement package. A smaller set already exercises the real renderer through private build paths (test-utils/real-ink.ts,test-utils/ink-testing-library.ts). The acceptance configuration must bypass both redirect mechanisms, use supported imports instead ofnode_modules/ink/build/...deep paths, set the intended interactive mode (our custom test Stdout exposescolumnsbut notisTTY; confirm how upstream's renderer treats that), and run the real package.<Static>remount, clear, and long-output cases; screen-reader mode; and a new heap regression that repeatedly commits static content and asserts retained heap stays bounded (the shell heap-threshold tests from PR Eliminate per-execution shell retention and bound secondary accumulators #3343 do not cover Ink). Also: Bun execution and bundling (not only Node >= 24), both raw-package and published prebuilt-bundle launch paths, alternate-screen entry and restoration on normal exit, startup failure, signals, and resize, custom stdout/stderr proxy compatibility, incremental rendering and render-metrics callbacks, macOS/Linux/Windows differences where CI or PTY infrastructure permits, render latency and retained heap after replacing fork-native scrolling with app rendering, and license/attribution review if fork algorithms are copied into our source.setSelection()highlight,subscribe()) and Project-scoped memories saved to .llxprt/LLXPRT.md are not loaded by memory discovery #985 (copy semantics:selectable,selectionFlow, boundaries) are open and unmerged; Project-scoped memories saved to .llxprt/LLXPRT.md are not loaded by memory discovery #985 is a draft stacked on Shell tool can hang indefinitely when PTY output processing stalls #984. Their PR descriptions target applications that own their viewport and selection logic; they remain proposals whose final API or scope may change. If both merge and ship a release with semantics we need, evaluate replacing the interim selection implementation with the bridge. Until then the interim implementation stands on its own; fork removal must not depend on an unmerged upstream schedule.Selection port design note (gated deliverable before P1 selection work). The fork's
hitTest/Range/Selection.toString()provide cell-to-text hit testing and copy reconstruction. Upstream has no public mechanism for cell-to-text mapping or renderer-level highlighting as of 7.1.1. The interim implementation needs a design covering: hit testing from screen coordinates to text offsets; copy reconstruction across wrapping, nestedText, transforms, ANSI styling, wide and combining characters, and current (not aspirational) selection coverage of virtualized or scrolled-off content; and highlight painting without rewriting every text-producing component. This is the largest open design risk in the migration; the existence of #984/#985 upstream reflects the same gap.Non-goals: carrying two UI implementations long-term (temporary migration scaffolding during P1 is acceptable and should be deleted), rewriting the UI layer, and upstreaming our code beyond feedback on #984/#985.
Why is this needed?
<Static>chunk to a renderer-levelfullStaticOutputstring with no reset during the renderer lifetime (installed copy:node_modules/ink/build/ink.js, accumulation around lines 211-275), and replays it in debug, alternate-buffer, and full-terminal-clear paths. This is the Ink-side retention found during the Every completed shell tool call permanently retains its child process and ~2.56 MB of output buffers #3329 investigation and deferred by PR Eliminate per-execution shell retention and bound secondary accumulators #3343 (REQ-3329-09) to this follow-up. Upstream contains several<Static>correctness and lifetime fixes: dangling-node cleanup (fix: prevent duplicate output for streaming tools in subagents (#898) #905), keyed-remount correctness and stale-output reset (shell tool won't cancel #948 and Difficulty referencing specific lines in large file reads #950, released in 7.0.3), and full-clear line accounting (Provider switching from OpenAI back to Anthropic improperly clears context #974, released in 7.1.1). For this repository, Difficulty referencing specific lines in large file reads #950 should let keyed static refreshes discard stale accumulated output, but boundedness under our actual history and remount pattern must be shown by the P3 heap regression rather than asserted. The unbounded text-cache leak is fixed only on unreleased master (see P2 release-selection note).Additional context
Alternatives considered:
patchedDependencies(option recorded inproject-plans/issue3329/plan.md): the smallest repository-local mitigation for thefullStaticOutputdefect. Its coverage across the published prebuilt bundle, raw package execution, and npm-installed consumers usingpackage-lock.jsonmust be verified, as must behavior in the redraw and clear paths; it preserves the fork API surface, not automatically every behavior, and leaves the divergence and all other upstream fixes unaddressed.fullStaticOutputdefect without committing to an interim selection engine, while keeping a separate path to upstream once replacement cost and release contents are known. This is viable because patching and migrating are not mutually exclusive, and because 7.1.1 itself lacks the unreleased text-cache fix.This issue recommends the switch, with P0 and the selection gate as explicit exit points. The recommendation is open to challenge on the interim-selection cost; the data above is laid out so the tradeoff can be argued either way.
Data gathered 2026-08-25 (repository source inspection; upstream GitHub and npm via API):
<Static>lifetime fixesmeasureElementcoordinatesmeasureElement(scroll and viewport-anchor translation)measureText/wrapTextcachesad9e3ea(#986, #987) on 2026-08-11suspendTerminal()sticky/stickyChildren/opaquereplacementalternateScreenoptionalternateBufferplus lifecycle characterizationutils/mouse.tsSGR/X11 parse,MouseContext.tsxstdin buffering) and reusable;mouseEventsEnabled.tsstill rides thealternateBuffermigrationKnown upstream open bug to watch while migrating: #973 (tall
<Static>output can overwrite its last line).Community-package download counts were not checked and are volatile; check npm at implementation time. Phase-level issues can be split out from P0-P4 once the direction is agreed.