From da8f3e92a7bbc3b288f89f6b535b72b94c1d1c19 Mon Sep 17 00:00:00 2001 From: adamelmore <2363879+adamdottv@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:18:54 -0600 Subject: [PATCH] perf(app): better session stream rendering --- packages/app/src/pages/session.tsx | 42 ++++++++++++++------ packages/ui/src/components/session-turn.tsx | 16 +++++++- packages/ui/src/hooks/create-auto-scroll.tsx | 11 ++--- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index c5997193f..2fc4ab62c 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -316,12 +316,22 @@ export default function Page() { return sync.session.history.loading(id) }) const emptyUserMessages: UserMessage[] = [] - const userMessages = createMemo(() => messages().filter((m) => m.role === "user") as UserMessage[], emptyUserMessages) - const visibleUserMessages = createMemo(() => { - const revert = revertMessageID() - if (!revert) return userMessages() - return userMessages().filter((m) => m.id < revert) - }, emptyUserMessages) + const userMessages = createMemo( + () => messages().filter((m) => m.role === "user") as UserMessage[], + emptyUserMessages, + { equals: same }, + ) + const visibleUserMessages = createMemo( + () => { + const revert = revertMessageID() + if (!revert) return userMessages() + return userMessages().filter((m) => m.id < revert) + }, + emptyUserMessages, + { + equals: same, + }, + ) const lastUserMessage = createMemo(() => visibleUserMessages().at(-1)) createEffect( @@ -347,13 +357,19 @@ export default function Page() { promptHeight: 0, }) - const renderedUserMessages = createMemo(() => { - const msgs = visibleUserMessages() - const start = store.turnStart - if (start <= 0) return msgs - if (start >= msgs.length) return emptyUserMessages - return msgs.slice(start) - }, emptyUserMessages) + const renderedUserMessages = createMemo( + () => { + const msgs = visibleUserMessages() + const start = store.turnStart + if (start <= 0) return msgs + if (start >= msgs.length) return emptyUserMessages + return msgs.slice(start) + }, + emptyUserMessages, + { + equals: same, + }, + ) const newSessionWorktree = createMemo(() => { if (store.newSessionWorktree === "create") return "create" diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index ca63d17ab..fe53c0939 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -457,9 +457,16 @@ export function SessionTurn( }) createEffect(() => { - const timer = setInterval(() => { + const update = () => { setStore("duration", duration()) - }, 1000) + } + + update() + + // Only keep ticking while the active (in-progress) turn is running. + if (!working()) return + + const timer = setInterval(update, 1000) onCleanup(() => clearInterval(timer)) }) @@ -495,6 +502,11 @@ export function SessionTurn( } }) + onCleanup(() => { + if (!statusTimeout) return + clearTimeout(statusTimeout) + }) + return (
| undefined let autoTimer: ReturnType | undefined let cleanup: (() => void) | undefined - let resizeFrame: number | undefined let auto: { top: number; time: number } | undefined const threshold = () => options.bottomThreshold ?? 10 @@ -152,11 +151,10 @@ export function createAutoScroll(options: AutoScrollOptions) { () => { if (!active()) return if (store.userScrolled) return - if (resizeFrame !== undefined) return - resizeFrame = requestAnimationFrame(() => { - resizeFrame = undefined - scrollToBottom(false) - }) + // ResizeObserver fires after layout, before paint. + // Keep the bottom locked in the same frame to avoid visible + // "jump up then catch up" artifacts while streaming content. + scrollToBottom(false) }, ) @@ -190,7 +188,6 @@ export function createAutoScroll(options: AutoScrollOptions) { onCleanup(() => { if (settleTimer) clearTimeout(settleTimer) if (autoTimer) clearTimeout(autoTimer) - if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame) if (cleanup) cleanup() })