Brooklyn Nicholson
42721dbe1c
fix(tui): big-session /resume now renders without first keystroke
...
useVirtualHistory set up its useSyncExternalStore subscription during
the first render, when scrollRef.current was still null (the ScrollBox
ref attaches during commit, after render). Its useCallback for
subscribe had a stable scrollRef identity as its only dep, so it never
re-subscribed once the ref actually attached — the hook stayed stuck
with vp=0, top=0, no scroll subscription. Small sessions fit entirely
in cold-start so you didn't notice; big /resume sessions got sliced to
the last 40 items with a huge topSpacer and the viewport sat on empty
space until some unrelated state change (e.g. a keystroke) re-rendered
and finally read a real vp.
- flip a hasScrollRef flag in useLayoutEffect once the ref attaches and
add it to the subscribe useCallback deps so useSyncExternalStore
rearms with a real subscription
- on resume, scrollToBottom() after history hydrates so the ScrollBox
lands at the newest messages instead of scrollTop=0 (stickyScroll
doesn't auto-engage on the initial empty→full dump)
2026-04-17 11:04:29 -05:00
Brooklyn Nicholson
39231f29c6
refactor(tui): /clean pass across ui-tui — 49 files, −217 LOC
...
Full codebase pass using the /clean doctrine (KISS/DRY, no one-off
helpers, no variables-used-once, pure functional where natural,
inlined obvious one-liners, killed dead exports, narrowed types,
spaced JSX). All contracts preserved — no RPC method, event name,
or exported type shape changed.
app/ — 15 files, -134 LOC
- inlined 4 one-off helpers (titleCase, isLong, statusToneFrom,
focusOutside predicate)
- stores to arrow-const style (buildUiState, buildTurnState,
buildOverlayState plus get/patch/reset triplets)
- functional slash/registry byName map (flatMap over for-loops)
- dropped dead param `live` in cancelOverlayFromCtrlC
- DRY'd duplicate shift() call in scrollWithSelection
- consolidated sections.push calls in /help
components/ — 12 files, -40 LOC
- extracted inline prop types to interfaces at file bottom (13×)
- inlined 6 one-off vars (pctLabel, logoW, heroW, cwd, title, hint)
- promoted HEART_COLORS + OPTS/LABELS to module scope
- JSX sibling spacing across 9 files
- un-shadowed `raw` in textInput
- components/thinking.tsx + components/markdown.tsx untouched
(structurally load-bearing / edge-case-heavy)
config content domain protocol/ — 8 files, -77 LOC
- tightened 3 regexes (MOUSE_TRACKING, looksLikeSlashCommand,
hasInterpolation — dropped stateful lastIndex dance)
- dead export ParsedSlashCommand removed
- MODES narrowed to `as const`, `.find(m => m === s)` replaces
`.includes() ? (as cast) : null`
- fortunes.ts hash via reduce
- fmtDuration ternary chain
- inlined aboveViewport predicate in viewport.ts
hooks/ + lib/ — 9 files, -38 LOC
- ANSI_RE via String.fromCharCode(27) + WS_RE lifted to module
scope (no more eslint-disable no-control-regex)
- compactPreview/edgePreview/thinkingPreview → ternary arrows
- useCompletion: hoisted pathReplace, moved stale-ref guard earlier
- useInputHistory: dropped useCallback wrapper (append is stable)
- useVirtualHistory: replaced 4× any with unknown + narrow
MeasuredNode interface + one cast site
root TS — 3 files, -63 LOC
- banner.ts: parseRichMarkup via matchAll instead of exec/lastIndex,
artWidth via reduce
- gatewayClient.ts: resolvePython candidate list collapse, inlined
one-branch guards in dispatch/pushLog/drain/request
- types.ts: alpha-sorted ActiveTool / Msg / SudoReq / SecretReq
members
eslint config
- disabled react-hooks/exhaustive-deps on packages/hermes-ink/**
(compiled by react/compiler, deps live in $[N] memo arrays that
eslint can't introspect) and removed the now-orphan in-file
disable directive in ScrollBox.tsx
fixes (not from the cleaner pass)
- useComposerState: unlinkSync(file) + try/catch → rmSync(file,
{ force: true }) — kills the no-empty lint error and is more
idiomatic
- useConfigSync: added setBellOnComplete + setVoiceEnabled to the
two useEffect dep arrays (they're stable React setState setters;
adding is safe and silences exhaustive-deps)
verification
- npx eslint src/ packages/ → 0 errors, 0 warnings
- npm run type-check → clean
- npm test → 50/50
- npm run build → 394.8kb ink-bundle.js, 11ms esbuild
- pytest tests/tui_gateway/ tests/test_tui_gateway_server.py
tests/hermes_cli/test_tui_resume_flow.py
tests/hermes_cli/test_tui_npm_install.py → 57/57
2026-04-16 22:32:53 -05:00