From c949e5b390814348a2a86802d4c350e964864da6 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:12:48 -0600 Subject: feat(app): incrementally render turns, markdown cache, lazily render diffs --- packages/ui/src/components/markdown.tsx | 38 ++++++++++++++- packages/ui/src/components/message-part.tsx | 4 +- packages/ui/src/components/session-turn.tsx | 76 +++++++++++++++++++++++------ 3 files changed, 100 insertions(+), 18 deletions(-) (limited to 'packages/ui/src') diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index 6e40b700a..2b0b01874 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -1,19 +1,53 @@ import { useMarked } from "../context/marked" +import { checksum } from "@opencode-ai/util/encode" import { ComponentProps, createResource, splitProps } from "solid-js" +type Entry = { + hash: string + html: string +} + +const max = 200 +const cache = new Map() + +function touch(key: string, value: Entry) { + cache.delete(key) + cache.set(key, value) + + if (cache.size <= max) return + + const first = cache.keys().next().value + if (!first) return + cache.delete(first) +} + export function Markdown( props: ComponentProps<"div"> & { text: string + cacheKey?: string class?: string classList?: Record }, ) { - const [local, others] = splitProps(props, ["text", "class", "classList"]) + const [local, others] = splitProps(props, ["text", "cacheKey", "class", "classList"]) const marked = useMarked() const [html] = createResource( () => local.text, async (markdown) => { - return marked.parse(markdown) + const hash = checksum(markdown) + const key = local.cacheKey ?? hash + + if (key && hash) { + const cached = cache.get(key) + if (cached && cached.hash === hash) { + touch(key, cached) + return cached.html + } + } + + const next = await marked.parse(markdown) + if (key && hash) touch(key, { hash, html: next }) + return next }, { initialValue: "" }, ) diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 8102c2ce7..534ea8f50 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -566,7 +566,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) { return (
- +
) @@ -580,7 +580,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) { return (
- +
) diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 005b6e5a3..f69d414be 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -350,15 +350,31 @@ export function SessionTurn( onUserInteracted: props.onUserInteracted, }) + const diffInit = 20 + const diffBatch = 20 + const [store, setStore] = createStore({ stickyTitleRef: undefined as HTMLDivElement | undefined, stickyTriggerRef: undefined as HTMLDivElement | undefined, stickyHeaderHeight: 0, retrySeconds: 0, + diffsOpen: [] as string[], + diffLimit: diffInit, status: rawStatus(), duration: duration(), }) + createEffect( + on( + () => message()?.id, + () => { + setStore("diffsOpen", []) + setStore("diffLimit", diffInit) + }, + { defer: true }, + ), + ) + createEffect(() => { const r = retry() if (!r) { @@ -542,10 +558,23 @@ export function SessionTurn(

Response

- +
- - + { + if (!Array.isArray(value)) return + setStore("diffsOpen", value) + }} + > + {(diff) => ( @@ -573,22 +602,41 @@ export function SessionTurn( - + + + )} + store.diffLimit}> + +
-- cgit v1.2.3