Files
hermes-agent/ui-tui-opentui-v2/src/view/transcript.tsx
T
alt-glitch 4b81ded58b feat(opentui-v2): Phase 2a — scrollbox transcript + textarea composer + header
Turns the read-only Phase-1 view into an interactive shell, split into focused
view components (spec v4 §2 layout):

- view/transcript.tsx: ONE full-height <scrollbox> with a reactive <For>
  (opencode's no-scrollback model). Applies the §8 #2 gotchas exactly:
  minHeight:0 on the wrapper AND the scrollbox, NO flexDirection on the
  scrollbox root, stickyScroll + stickyStart="bottom".
- view/composer.tsx: a native <textarea> captured by ref — flexShrink:0,
  focus-on-mount, Enter->submit via keyBindings, imperative .clear() on submit,
  and a `submitting` re-entrancy guard. Wired by the entry to fire prompt.submit
  (Effect.runFork on the in-hand service value); it's now the PRIMARY input, with
  the HERMES_TUI_PROMPT stand-in kept only for launch-with-prompt.
- view/header.tsx + view/messageLine.tsx: extracted, themed (no hardcoded
  styles). MessageLine stays flat-text this slice; ordered parts (§7) land in 2b.

test/lib/render.ts now flushes 3 renderOnce passes before capture — a <scrollbox>
needs more than one pass to measure content + apply sticky, else the transcript
row paints blank.

Verified: bun run check green (12 tests / 4 files / 31 expects). Live tmux drive:
typed into the composer -> cleared -> user row -> streamed reply ("Here are three
words"); Ctrl+C quits cleanly even with the textarea focused, no orphan child.
Composer placeholder rendered the live skin's welcome string (skin->theme live).
Smoke P2a + parity matrix updated. Phase 2b (ordered parts/tool render/markdown)
is the next slice.
2026-06-08 14:28:59 +00:00

28 lines
1.2 KiB
TypeScript

/**
* Transcript — the scrolling message pane (spec v4 §2 `view/transcript.tsx`).
*
* ONE full-height <scrollbox> with a reactive <For> (opencode's model — the
* viewport clips growing output so terminal scrollback is never corrupted; no
* `writeToScrollback`). Carries the §8 #2 gotchas EXACTLY:
* - `minHeight:0` on BOTH the wrapper box AND the <scrollbox> (so the flex
* child can shrink below content height instead of pushing the composer off),
* - NO `flexDirection` on the <scrollbox> ROOT style (it has internal
* viewport/content children; setting it there breaks content-height
* measurement → phantom scroll offset that clips the top + leaves a gap),
* - `stickyScroll` + `stickyStart="bottom"` to pin the latest line.
*/
import { For } from 'solid-js'
import type { SessionStore } from '../logic/store.ts'
import { MessageLine } from './messageLine.tsx'
export function Transcript(props: { store: SessionStore }) {
return (
<box style={{ flexGrow: 1, minHeight: 0, marginTop: 1 }}>
<scrollbox style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom">
<For each={props.store.state.messages}>{message => <MessageLine message={message} />}</For>
</scrollbox>
</box>
)
}