review(tui): route cursorLayout through @hermes/ink wrapAnsi shim (Bun runtime parity)

Copilot caught an important runtime parity gap on PR #27489: the fix
imported the npm `wrap-ansi` package directly, but Ink's `<Text
wrap="wrap">` uses a runtime-selecting shim
(`ui-tui/packages/hermes-ink/src/ink/wrapAnsi.ts`) that prefers
`Bun.wrapAnsi` when running under Bun and falls back to the npm package
elsewhere. So under Bun, Ink would render via `Bun.wrapAnsi` while
`cursorLayout` would compute breaks via the npm package — any
disagreement reintroduces the exact cursor-drift symptom the PR is
meant to eliminate.

Fix:

- Export `wrapAnsi` from `@hermes/ink` (`packages/hermes-ink/src/entry-exports.ts`
  and `packages/hermes-ink/index.d.ts`) so the shim is the public surface.
- Switch `ui-tui/src/lib/inputMetrics.ts` from `import wrapAnsi from
  'wrap-ansi'` to `import { wrapAnsi } from '@hermes/ink'`. Both
  renderer (Ink) and cursor layout now traverse the same shim, so
  they share the runtime-selected implementation by construction.
- Same swap in `textInputWrap.test.ts` and `cursorDriftRegression.test.ts`
  — tests now assert parity through the shim, which means under Bun
  they actually exercise Bun's implementation instead of asserting a
  tautology against the npm package.
- Drop the direct `"wrap-ansi": "^9.0.0"` from `ui-tui/package.json`.
  `@hermes/ink` (which IS a declared dep) pulls wrap-ansi in
  transitively — that's not a phantom dep because the import path
  goes through `@hermes/ink`'s public exports, not through a
  hoisting accident.

Verified: 791/791 vitest tests pass. `@hermes/ink` rebuilt
(`dist/entry-exports.js` includes `wrapAnsi` export). TUI bundle
rebuilt clean.
This commit is contained in:
Brooklyn Nicholson 2026-05-17 11:52:21 -05:00
parent 55f13be65d
commit 8c78f533dd
7 changed files with 7 additions and 8 deletions

View file

@ -14,8 +14,7 @@
"ink-text-input": "^6.0.0",
"nanostores": "^1.2.0",
"react": "^19.2.4",
"unicode-animations": "^1.0.3",
"wrap-ansi": "^9.0.0"
"unicode-animations": "^1.0.3"
},
"devDependencies": {
"@babel/cli": "^7.28.6",

View file

@ -22,8 +22,7 @@
"ink-text-input": "^6.0.0",
"nanostores": "^1.2.0",
"react": "^19.2.4",
"unicode-animations": "^1.0.3",
"wrap-ansi": "^9.0.0"
"unicode-animations": "^1.0.3"
},
"devDependencies": {
"@babel/cli": "^7.28.6",

View file

@ -34,5 +34,6 @@ export { default as measureElement } from './src/ink/measure-element.ts'
export { createRoot, forceRedraw, default as render, renderSync } from './src/ink/root.ts'
export type { Instance, RenderOptions, Root } from './src/ink/root.ts'
export { stringWidth } from './src/ink/stringWidth.ts'
export { wrapAnsi } from './src/ink/wrapAnsi.ts'
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'
export type { Props as TextInputProps } from 'ink-text-input'

View file

@ -26,5 +26,6 @@ export { default as measureElement } from './ink/measure-element.js'
export { scrollFastPathStats, type ScrollFastPathStats } from './ink/render-node-to-output.js'
export { createRoot, forceRedraw, default as render, renderSync } from './ink/root.js'
export { stringWidth } from './ink/stringWidth.js'
export { wrapAnsi } from './ink/wrapAnsi.js'
export { isXtermJs } from './ink/terminal.js'
export { default as TextInput, UncontrolledTextInput } from 'ink-text-input'

View file

@ -21,8 +21,8 @@
* the end-of-text position that wrap-ansi would render. Any future
* regression that lets the two diverge re-introduces the drift.
*/
import { wrapAnsi } from '@hermes/ink'
import { describe, expect, it } from 'vitest'
import wrapAnsi from 'wrap-ansi'
import { cursorLayout, inputVisualHeight } from '../lib/inputMetrics.js'

View file

@ -1,5 +1,5 @@
import { wrapAnsi } from '@hermes/ink'
import { describe, expect, it } from 'vitest'
import wrapAnsi from 'wrap-ansi'
import { offsetFromPosition } from '../components/textInput.js'
import { composerPromptWidth, cursorLayout, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js'

View file

@ -1,5 +1,4 @@
import { stringWidth } from '@hermes/ink'
import wrapAnsi from 'wrap-ansi'
import { stringWidth, wrapAnsi } from '@hermes/ink'
import type { Role } from '../types.js'