Promote strictness in ui-tui-opentui-v2:
- eslint: @typescript-eslint/no-non-null-assertion: error (with a test
override block keeping `!` in *.test.ts/tsx fixtures), and promote
unused-imports/no-unused-vars warn → error.
- tsconfig: add noUnusedLocals + noImplicitReturns.
Remove all 24 production `!` non-null assertions by replacing each with
a real guard / default / early-return, preserving rendered behavior:
- gateway/client.ts: read the pending entry once and guard (vs has()+get()!).
- logic/theme.ts: guard the parseHex regex match; `?? 0` on the always-
in-bounds XTERM_6_LEVELS lookups; restructure backgroundLuminance to
branch into a typed tuple and use charAt() for the 3-digit hex expand.
- view/homeHint.tsx: use Solid's <Show>{value => …} callback form to
narrow info().model / info().cwd instead of `!`.
- view/reasoningPart.tsx: guard the regex match before slicing m[0].
- view/statusBar.tsx: read model/cwd/pct into locals + guard; `?? 0` on
the showBar()-guarded context-bar percentage.
- view/toolPart.tsx: guard the single-arg entry; `?? 0` on the
duration-guarded fmtDuration.
Raw useTerminalDimensions fires on every SIGWINCH tick; during a drag that's a
recompute/reflow storm across every width-sensitive component (tool bodies,
tables, status bar, banner). Add a DimensionsProvider that runs the raw hook
ONCE and feeds a single leading+trailing-debounced (40ms) signal — mirroring the
gateway's 16ms event coalescing / opencode's createLeadingTrailingSignal — that
every consumer shares via useDimensions(). They now reflow together (no tearing)
and at most once per window. Falls back to the raw hook outside a provider
(headless tests). Verified: single resizes converge clean (wide banner ⇄ compact
brand at the 102-col threshold); rapid bursts coalesce. 90 pass.
Visual-hierarchy pass (design-reviewed against free-code/opencode):
- header: brand glyph in accent + name in primary/bold + a bottom rule, so it
reads as chrome and bookends the transcript with the status bar's top rule
(fixes 'nothing differentiates the header from the text stream').
- status bar: a dim │ divider segments model·effort from the context meter.
- user/assistant turn glyphs bold + the user ❯ in accent so turns are scannable.
- reasoning 'Thought' label uses label (not warn) so it matches tool headers —
warn is reserved for warnings; reasoning/tool now read as one aside family.
- home screen: brand in primary/bold, command names in accent vs muted descs,
wider column.
- FIX (load-bearing): strip ANSI/SGR escape sequences from slash/notice text
(pushSystem + openPager) — the gateway colors them for Ink, which interprets
them; the native <text> rendered them as literal glyphs. +stripAnsi
+ 3 tests. All tokens themed (no hardcoded colors). 88 pass.
Item 1 — copy/paste:
- boundary/clipboard.ts (ported/trimmed from opencode): writeClipboard = OSC 52
(SSH/tmux-safe) + a native command (pbcopy/wl-copy/xclip/xsel/clip);
readClipboardImage = clipboard PNG via wl-paste/xclip/pngpaste/powershell.
- Ctrl+C copies a live MOUSE SELECTION (renderer.getSelection) before the
interrupt/quit machine runs (opencode's selection-key precedence), with a
"Copied to clipboard" hint; falls through to interrupt/quit when there's no
selection.
- text paste inserts natively (textarea handlePaste); the composer's onPaste only
intercepts an EMPTY bracketed paste (image-only clipboard) → readClipboardImage
→ image.attach_bytes (the next prompt.submit picks it up).
Item 4 — mouse selection now ignores decorative glyphs: selectable={false} on the
message/tool gutter glyphs and all chrome (header, status bar, status line,
composer prompt glyph), so a drag copies the message text, not ❯/⚕/▶/⚡.
Live-smoked (this env has no clipboard tools/DISPLAY, so native copy + image read
can't be confirmed here, but): drag-select + Ctrl+C → "Copied to clipboard" (not
quit); no-selection Ctrl+C still arms quit; bracketed text paste lands in the
composer. 71 pass.
Item 14: a persistent bottom-chrome status bar, ported from Ink's appChrome
StatusRule. Sourced from the session.info event (model / reasoning_effort /
fast / cwd / branch / running / usage.context_*) which was decoded but dropped
until now; also folded session.create/resume result.info and message.complete
usage into a new store `info` slice.
- store: SessionInfo slice + applyInfo(); session.info handler; message.start/
complete flip `running` (the flag the Ctrl-C interrupt will read); refresh
usage on complete.
- schema: MessageComplete.payload gains loose `usage` so it survives decode.
- view/statusBar.tsx: width-aware (Ink progressive disclosure) — context bar
drops on narrow terminals, cwd compacts to last two segments + left-truncates
so the row never wraps. Turn/connection dot ◐/●/○.
- App: status bar sits ABOVE the composer; a top-edge rule (border:['top'])
visually separates the status bar + textbox input region from the transcript.
- tests: store info slice (3) + headless status-bar render (1); bumped the
approval-prompt capture height for the taller input region. 59 pass.
Live-smoked: bar shows model·effort·context%·dir; context updates 0→4% across a
turn; running dot flips; separator divides input region from transcript.