Browse Source

perf(app): better session stream rendering

adamelmore 1 month ago
parent
commit
da8f3e92a7

+ 29 - 13
packages/app/src/pages/session.tsx

@@ -316,12 +316,22 @@ export default function Page() {
     return sync.session.history.loading(id)
     return sync.session.history.loading(id)
   })
   })
   const emptyUserMessages: UserMessage[] = []
   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))
   const lastUserMessage = createMemo(() => visibleUserMessages().at(-1))
 
 
   createEffect(
   createEffect(
@@ -347,13 +357,19 @@ export default function Page() {
     promptHeight: 0,
     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(() => {
   const newSessionWorktree = createMemo(() => {
     if (store.newSessionWorktree === "create") return "create"
     if (store.newSessionWorktree === "create") return "create"

+ 14 - 2
packages/ui/src/components/session-turn.tsx

@@ -457,9 +457,16 @@ export function SessionTurn(
   })
   })
 
 
   createEffect(() => {
   createEffect(() => {
-    const timer = setInterval(() => {
+    const update = () => {
       setStore("duration", duration())
       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))
     onCleanup(() => clearInterval(timer))
   })
   })
 
 
@@ -495,6 +502,11 @@ export function SessionTurn(
     }
     }
   })
   })
 
 
+  onCleanup(() => {
+    if (!statusTimeout) return
+    clearTimeout(statusTimeout)
+  })
+
   return (
   return (
     <div data-component="session-turn" class={props.classes?.root} ref={setRootRef}>
     <div data-component="session-turn" class={props.classes?.root} ref={setRootRef}>
       <div
       <div

+ 4 - 7
packages/ui/src/hooks/create-auto-scroll.tsx

@@ -15,7 +15,6 @@ export function createAutoScroll(options: AutoScrollOptions) {
   let settleTimer: ReturnType<typeof setTimeout> | undefined
   let settleTimer: ReturnType<typeof setTimeout> | undefined
   let autoTimer: ReturnType<typeof setTimeout> | undefined
   let autoTimer: ReturnType<typeof setTimeout> | undefined
   let cleanup: (() => void) | undefined
   let cleanup: (() => void) | undefined
-  let resizeFrame: number | undefined
   let auto: { top: number; time: number } | undefined
   let auto: { top: number; time: number } | undefined
 
 
   const threshold = () => options.bottomThreshold ?? 10
   const threshold = () => options.bottomThreshold ?? 10
@@ -152,11 +151,10 @@ export function createAutoScroll(options: AutoScrollOptions) {
     () => {
     () => {
       if (!active()) return
       if (!active()) return
       if (store.userScrolled) 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(() => {
   onCleanup(() => {
     if (settleTimer) clearTimeout(settleTimer)
     if (settleTimer) clearTimeout(settleTimer)
     if (autoTimer) clearTimeout(autoTimer)
     if (autoTimer) clearTimeout(autoTimer)
-    if (resizeFrame !== undefined) cancelAnimationFrame(resizeFrame)
     if (cleanup) cleanup()
     if (cleanup) cleanup()
   })
   })