import { currentId, _openGen, historyLoad } from "../../stores/session"; import { historyT as t } from "./copy"; import { recoverConversation } from "./open"; import { useEffect, useLayoutEffect, useRef } from "preact/hooks"; import { bindStreamingPre, type StreamingPreHandle } from "./delta"; import { bindMessageScroll, down, unbindMessageScroll } from "./scroll"; export type StreamingPreProps = { className?: string; handleRef?: { current: StreamingPreHandle | null }; }; /** * Tool-output `
`. The text node is held on a ref; chunks call
* `handle.append(delta)` → `textNode.appendData`. Do not set `textContent`
* from the parent on every chunk.
*/
export function StreamingPre(props: StreamingPreProps) {
const preRef = useRef(null);
useLayoutEffect(() => {
const pre = preRef.current;
if (!pre) return;
const textNode = document.createTextNode("");
pre.appendChild(textNode);
const handle = bindStreamingPre(textNode, "");
if (props.handleRef) props.handleRef.current = handle;
return () => {
if (props.handleRef) props.handleRef.current = null;
};
}, [props.handleRef]);
return ;
}
/** Read feedback lives outside the transcript replaced by imperative history renders. */
export function HistoryLoadStatus() {
const state = historyLoad.value;
if (!state || state.fid !== currentId.value || state.generation !== _openGen.value || state.status === "loaded") return null;
return (
{t("history." + state.status)}
{Object.entries(state.errors).map(([part, message]) => {t("history.part." + part)}: {message}
)}
{state.status !== "loading" && }
);
}
/**
* Message column. `#messages` and `#jump-pill` keep the frozen DOM contract.
* Live streaming is imperative (feed / flushRender) inside this host.
*/
export function MessageList() {
const hostRef = useRef(null);
useEffect(() => {
bindMessageScroll(hostRef.current);
return () => unbindMessageScroll();
}, []);
return (
);
}