Adam 3 месяцев назад
Родитель
Сommit
5bcc93851c
3 измененных файлов с 44 добавлено и 31 удалено
  1. 1 0
      bun.lock
  2. 1 0
      packages/desktop/package.json
  3. 42 31
      packages/ui/src/components/session-turn.tsx

+ 1 - 0
bun.lock

@@ -133,6 +133,7 @@
         "@solid-primitives/active-element": "2.1.3",
         "@solid-primitives/audio": "1.4.2",
         "@solid-primitives/event-bus": "1.1.2",
+        "@solid-primitives/media": "2.3.3",
         "@solid-primitives/resize-observer": "2.1.3",
         "@solid-primitives/scroll": "2.1.3",
         "@solid-primitives/storage": "4.3.3",

+ 1 - 0
packages/desktop/package.json

@@ -37,6 +37,7 @@
     "@solid-primitives/active-element": "2.1.3",
     "@solid-primitives/audio": "1.4.2",
     "@solid-primitives/event-bus": "1.1.2",
+    "@solid-primitives/media": "2.3.3",
     "@solid-primitives/resize-observer": "2.1.3",
     "@solid-primitives/scroll": "2.1.3",
     "@solid-primitives/storage": "4.3.3",

+ 42 - 31
packages/ui/src/components/session-turn.tsx

@@ -3,7 +3,7 @@ import { useData } from "../context"
 import { useDiffComponent } from "../context/diff"
 import { getDirectory, getFilename } from "@opencode-ai/util/path"
 import { checksum } from "@opencode-ai/util/encode"
-import { createEffect, createMemo, createSignal, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
+import { createEffect, createMemo, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
 import { createResizeObserver } from "@solid-primitives/resize-observer"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
@@ -49,56 +49,67 @@ export function SessionTurn(
   const working = createMemo(() => status()?.type !== "idle")
 
   let scrollRef: HTMLDivElement | undefined
-  const [contentRef, setContentRef] = createSignal<HTMLDivElement>()
-  const [stickyTitleRef, setStickyTitleRef] = createSignal<HTMLDivElement>()
-  const [stickyTriggerRef, setStickyTriggerRef] = createSignal<HTMLDivElement>()
-  const [userScrolled, setUserScrolled] = createSignal(false)
-  const [stickyHeaderHeight, setStickyHeaderHeight] = createSignal(0)
-  const [scrollY, setScrollY] = createSignal(0)
+  const [state, setState] = createStore({
+    contentRef: undefined as HTMLDivElement | undefined,
+    stickyTitleRef: undefined as HTMLDivElement | undefined,
+    stickyTriggerRef: undefined as HTMLDivElement | undefined,
+    userScrolled: false,
+    stickyHeaderHeight: 0,
+    scrollY: 0,
+  })
 
   function handleScroll() {
     if (!scrollRef) return
-    setScrollY(scrollRef.scrollTop)
+    setState("scrollY", scrollRef.scrollTop)
     const { scrollTop, scrollHeight, clientHeight } = scrollRef
     const atBottom = scrollHeight - scrollTop - clientHeight < 50
     if (!atBottom && working()) {
-      setUserScrolled(true)
+      setState("userScrolled", true)
     }
   }
 
   function handleInteraction() {
     if (working()) {
-      setUserScrolled(true)
+      setState("userScrolled", true)
     }
   }
 
   createEffect(() => {
     if (!working()) {
-      setUserScrolled(false)
+      setState("userScrolled", false)
     }
   })
 
-  createResizeObserver(contentRef, () => {
-    if (!scrollRef || userScrolled() || !working()) return
-    requestAnimationFrame(() => {
-      scrollRef.scrollTop = scrollRef.scrollHeight
-    })
-  })
+  createResizeObserver(
+    () => state.contentRef,
+    () => {
+      if (!scrollRef || state.userScrolled || !working()) return
+      requestAnimationFrame(() => {
+        scrollRef.scrollTop = scrollRef.scrollHeight
+      })
+    },
+  )
 
-  createResizeObserver(stickyTitleRef, ({ height }) => {
-    const triggerHeight = stickyTriggerRef()?.offsetHeight ?? 0
-    setStickyHeaderHeight(height + triggerHeight + 8)
-  })
+  createResizeObserver(
+    () => state.stickyTitleRef,
+    ({ height }) => {
+      const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
+      setState("stickyHeaderHeight", height + triggerHeight + 8)
+    },
+  )
 
-  createResizeObserver(stickyTriggerRef, ({ height }) => {
-    const titleHeight = stickyTitleRef()?.offsetHeight ?? 0
-    setStickyHeaderHeight(titleHeight + height + 8)
-  })
+  createResizeObserver(
+    () => state.stickyTriggerRef,
+    ({ height }) => {
+      const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
+      setState("stickyHeaderHeight", titleHeight + height + 8)
+    },
+  )
 
   return (
-    <div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${scrollY()}px` }}>
+    <div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${state.scrollY}px` }}>
       <div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
-        <div ref={setContentRef} onClick={handleInteraction}>
+        <div ref={(el) => setState("contentRef", el)} onClick={handleInteraction}>
           <Show when={message()}>
             {(message) => {
               const assistantMessages = createMemo(() => {
@@ -237,7 +248,7 @@ export function SessionTurn(
 
               createEffect((prev) => {
                 const isWorking = working()
-                if (prev && !isWorking && !userScrolled()) {
+                if (prev && !isWorking && !state.userScrolled) {
                   setStore("stepsExpanded", false)
                 }
                 return isWorking
@@ -248,10 +259,10 @@ export function SessionTurn(
                   data-message={message().id}
                   data-slot="session-turn-message-container"
                   class={props.classes?.container}
-                  style={{ "--sticky-header-height": `${stickyHeaderHeight()}px` }}
+                  style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
                 >
                   {/* Title (sticky) */}
-                  <div ref={setStickyTitleRef} data-slot="session-turn-sticky-title">
+                  <div ref={(el) => setState("stickyTitleRef", el)} data-slot="session-turn-sticky-title">
                     <div data-slot="session-turn-message-header">
                       <div data-slot="session-turn-message-title">
                         <Switch>
@@ -270,7 +281,7 @@ export function SessionTurn(
                     <Message message={message()} parts={parts()} />
                   </div>
                   {/* Trigger (sticky) */}
-                  <div ref={setStickyTriggerRef} data-slot="session-turn-response-trigger">
+                  <div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
                     <Button
                       data-slot="session-turn-collapsible-trigger-content"
                       variant="ghost"