Two defects in packages/cli/src/ui share one unmade decision, so they are tracked together. Both were raised in review of #15 and deliberately not fixed there.
1. The live region miscounts its rows after a terminal resize
LiveRegion repaints by moving up #liveRowCount - 1 lines and clearing to the end of the screen. That count is logical rows, and one logical row is one screen line only while the row fits the terminal width.
Narrow the terminal while the boot row is animating and a previously painted row reflows onto more than one physical line. The next repaint then moves up by the logical count, which lands the cursor inside the wrapped block, and ESC[0J clears only from there down — leaving the first wrapped line stranded above it. Every later repaint inherits the wrong origin.
Raised by cubic on #15: #15 (packages/cli/src/ui/live-region.ts)
2. The hardware cursor is never hidden
LiveRegion exposes hideCursor() / showCursor() and boot-row.ts, its only consumer, calls neither. The cursor sits at the end of the animated row and jumps on every repaint — for the whole ~30s of an image pull and adapter bootstrap.
Raised in a local review pass on the same branch (packages/cli/src/ui/boot-row.ts).
Why one issue
Both fixes are process-level terminal state that must be undone on every exit path, including SIGINT:
- The resize fix needs a
SIGWINCH listener, which leaks if stop() does not remove it.
- The cursor fix needs the cursor restored on
Ctrl-C. A hidden cursor never restored is a worse failure than the jumping one it fixes: it outlives the process and the user's terminal stays broken.
So the question is not either fix in isolation — it is who owns terminal lifecycle in this package, and how stop() relates to signal teardown. please dev has no command yet (docs/dev-tui.md), and the answer likely belongs with whatever ends up owning the handoff to @ai-sdk/tui, which takes the alternate screen buffer and does its own teardown.
Suggested shape
One teardown registry per boot-chrome session: enter/exit pairs registered together, run on stop() and on SIGINT/SIGTERM, idempotent. Then the resize listener and the cursor hide both become registrations rather than one-off calls.
Two defects in
packages/cli/src/uishare one unmade decision, so they are tracked together. Both were raised in review of #15 and deliberately not fixed there.1. The live region miscounts its rows after a terminal resize
LiveRegionrepaints by moving up#liveRowCount - 1lines and clearing to the end of the screen. That count is logical rows, and one logical row is one screen line only while the row fits the terminal width.Narrow the terminal while the boot row is animating and a previously painted row reflows onto more than one physical line. The next repaint then moves up by the logical count, which lands the cursor inside the wrapped block, and
ESC[0Jclears only from there down — leaving the first wrapped line stranded above it. Every later repaint inherits the wrong origin.Raised by cubic on #15: #15 (
packages/cli/src/ui/live-region.ts)2. The hardware cursor is never hidden
LiveRegionexposeshideCursor()/showCursor()andboot-row.ts, its only consumer, calls neither. The cursor sits at the end of the animated row and jumps on every repaint — for the whole ~30s of an image pull and adapter bootstrap.Raised in a local review pass on the same branch (
packages/cli/src/ui/boot-row.ts).Why one issue
Both fixes are process-level terminal state that must be undone on every exit path, including
SIGINT:SIGWINCHlistener, which leaks ifstop()does not remove it.Ctrl-C. A hidden cursor never restored is a worse failure than the jumping one it fixes: it outlives the process and the user's terminal stays broken.So the question is not either fix in isolation — it is who owns terminal lifecycle in this package, and how
stop()relates to signal teardown.please devhas no command yet (docs/dev-tui.md), and the answer likely belongs with whatever ends up owning the handoff to@ai-sdk/tui, which takes the alternate screen buffer and does its own teardown.Suggested shape
One teardown registry per boot-chrome session: enter/exit pairs registered together, run on
stop()and onSIGINT/SIGTERM, idempotent. Then the resize listener and the cursor hide both become registrations rather than one-off calls.