/** * Transcript — the scrolling message pane (spec v4 §2 `view/transcript.tsx`). * * ONE full-height with a reactive (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 (so the flex * child can shrink below content height instead of pushing the composer off), * - NO `flexDirection` on the 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. * * A `ScrollAnchorProvider` gives collapse/expand toggles (tool/thinking) a handle * to hold the viewport in place so expanding doesn't yank to the bottom (#4). */ import type { ScrollBoxRenderable } from '@opentui/core' import { createMemo, createSignal, For, Show } from 'solid-js' import type { SessionStore } from '../logic/store.ts' import { DisplayProvider } from './display.tsx' import { HomeHint } from './homeHint.tsx' import { MessageLine } from './messageLine.tsx' import { ScrollAnchorProvider } from './scrollAnchor.tsx' import { useTheme } from './theme.tsx' export function Transcript(props: { store: SessionStore }) { const [scroll, setScroll] = createSignal() const theme = useTheme() const dropped = () => props.store.state.dropped const sid = () => props.store.state.sessionId // The NEWEST assistant answer's index — gold is earned (design pass): only // that turn's `⚕` glyph stays primary; older answers demote to grey. const latestAssistant = createMemo(() => { const messages = props.store.state.messages for (let i = messages.length - 1; i >= 0; i--) { if (messages[i]?.role === 'assistant') return i } return -1 }) return ( {/* display flags (/compact, /details — Epic 3) for the rows below */} ({ compact: props.store.state.compact, details: props.store.state.details })}> {/* empty-transcript home screen (item 12); replaced by messages on the first turn */} {/* Honest truncation notice: the rolling cap hides the OLDEST rows from the DISPLAY (never the model's context — that lives on the gateway). Point to the dashboard for the full transcript. selectable=false → it's chrome, excluded from copy/selection. */} 0}> {`⤒ ${dropped()} earlier message${dropped() === 1 ? '' : 's'} — scroll-back capped; full transcript on the dashboard${sid() ? ` · session ${sid()}` : ''}`} {(message, i) => } ) }