/** Accumulate deltas and render at most once per animation frame. */ export function createStreamRenderer({onReply, onThinking, schedule = callback => requestAnimationFrame(callback), cancel = id => cancelAnimationFrame(id)}) { const text = {reply: "", thinking: ""}; const dirty = new Set(); let frame = null; const flush = () => { if (frame !== null) cancel(frame); frame = null; if (dirty.has("thinking")) onThinking?.(text.thinking); if (dirty.has("reply")) onReply?.(text.reply); dirty.clear(); }; const update = (target, value, replace = false) => { text[target] = replace ? value : text[target] + value; dirty.add(target); if (frame === null) frame = schedule(flush); }; return { onChunk: ({chunk, fullReply}) => update("reply", typeof fullReply === "string" ? fullReply : chunk, typeof fullReply === "string"), onThinking: ({chunk, fullThinking}) => update("thinking", typeof fullThinking === "string" ? fullThinking : chunk, typeof fullThinking === "string"), onReplace: ({target, text: value}) => update(target, value, true), flush, dispose() { if (frame !== null) cancel(frame); frame = null; dirty.clear(); } }; }