Przeglądaj źródła

wip(desktop): progress

Adam 2 miesięcy temu
rodzic
commit
0aa3e6c270
1 zmienionych plików z 67 dodań i 23 usunięć
  1. 67 23
      packages/ui/src/components/session-turn.tsx

+ 67 - 23
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, onMount, ParentProps, Show, Switch } from "solid-js"
+import { createEffect, createMemo, createSignal, For, Match, onCleanup, ParentProps, Show, Switch } from "solid-js"
 import { DiffChanges } from "./diff-changes"
 import { Typewriter } from "./typewriter"
 import { Message } from "./message-part"
@@ -17,6 +17,13 @@ import { MessageProgress } from "./message-progress"
 import { Collapsible } from "./collapsible"
 import { Dynamic } from "solid-js/web"
 
+// Track animation state per message ID - persists across re-renders
+// "empty" = first saw with no value (should animate when value arrives)
+// "animating" = currently animating (keep returning true)
+// "done" = already animated or first saw with value (never animate)
+const titleAnimationState = new Map<string, "empty" | "animating" | "done">()
+const summaryAnimationState = new Map<string, "empty" | "animating" | "done">()
+
 export function SessionTurn(
   props: ParentProps<{
     sessionID: string
@@ -55,11 +62,37 @@ export function SessionTurn(
       <div data-slot="session-turn-content" class={props.classes?.content}>
         <Show when={message()}>
           {(msg) => {
-            const titleKey = `app:seen:session:${props.sessionID}:${msg().id}:title`
-            const contentKey = `app:seen:session:${props.sessionID}:${msg().id}:content`
             const [detailsExpanded, setDetailsExpanded] = createSignal(false)
-            const [titled, setTitled] = createSignal(true)
-            const [faded, setFaded] = createSignal(true)
+
+            // Animation logic: only animate if we witness the value transition from empty to non-empty
+            // Track in module-level Maps keyed by message ID so it persists across re-renders
+
+            // Initialize animation state for current message (reactive - runs when msg().id changes)
+            createEffect(() => {
+              const id = msg().id
+              if (!titleAnimationState.has(id)) {
+                titleAnimationState.set(id, msg().summary?.title ? "done" : "empty")
+              }
+              if (!summaryAnimationState.has(id)) {
+                const assistantMsgs = messages()?.filter(
+                  (m) => m.role === "assistant" && m.parentID == id,
+                ) as AssistantMessage[]
+                const parts = assistantMsgs?.flatMap((m) => data.store.part[m.id])
+                const lastText = parts?.filter((p) => p?.type === "text")?.at(-1)
+                const summaryValue = msg().summary?.body ?? lastText?.text
+                summaryAnimationState.set(id, summaryValue ? "done" : "empty")
+              }
+
+              // When message changes or component unmounts, mark any "animating" states as "done"
+              onCleanup(() => {
+                if (titleAnimationState.get(id) === "animating") {
+                  titleAnimationState.set(id, "done")
+                }
+                if (summaryAnimationState.get(id) === "animating") {
+                  summaryAnimationState.set(id, "done")
+                }
+              })
+            })
 
             const assistantMessages = createMemo(() => {
               return messages()?.filter((m) => m.role === "assistant" && m.parentID == msg().id) as AssistantMessage[]
@@ -79,27 +112,38 @@ export function SessionTurn(
             const summary = createMemo(() => msg().summary?.body ?? lastTextPart()?.text)
             const lastTextPartShown = createMemo(() => !msg().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0)
 
-            // allowing time for the animations to finish
-            onMount(() => {
-              const titleSeen = sessionStorage.getItem(titleKey) === "true"
-              const contentSeen = sessionStorage.getItem(contentKey) === "true"
-
-              if (!titleSeen) {
-                setTitled(false)
-                const title = msg().summary?.title
-                if (title) setTimeout(() => setTitled(true), 10_000)
-                setTimeout(() => sessionStorage.setItem(titleKey, "true"), 1000)
+            // Should animate: state is "empty" AND value now exists, or state is "animating"
+            // Transition: empty -> animating -> done (done happens on cleanup)
+            const animateTitle = createMemo(() => {
+              const id = msg().id
+              const state = titleAnimationState.get(id)
+              const title = msg().summary?.title
+              if (state === "animating") {
+                return true
               }
-
-              if (!contentSeen) {
-                setFaded(false)
-                setTimeout(() => sessionStorage.setItem(contentKey, "true"), 1000)
+              if (state === "empty" && title) {
+                titleAnimationState.set(id, "animating")
+                return true
+              }
+              return false
+            })
+            const animateSummary = createMemo(() => {
+              const id = msg().id
+              const state = summaryAnimationState.get(id)
+              const value = summary()
+              if (state === "animating") {
+                return true
+              }
+              if (state === "empty" && value) {
+                summaryAnimationState.set(id, "animating")
+                return true
               }
+              return false
             })
 
             createEffect(() => {
-              const completed = !messageWorking()
-              setTimeout(() => setCompleted(completed), 1200)
+              const done = !messageWorking()
+              setTimeout(() => setCompleted(done), 1200)
             })
 
             return (
@@ -108,7 +152,7 @@ export function SessionTurn(
                 <div data-slot="session-turn-message-header">
                   <div data-slot="session-turn-message-title">
                     <Show
-                      when={titled()}
+                      when={!animateTitle()}
                       fallback={<Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" />}
                     >
                       <h1>{msg().summary?.title}</h1>
@@ -133,7 +177,7 @@ export function SessionTurn(
                           <Markdown
                             data-slot="session-turn-markdown"
                             data-diffs={!!msg().summary?.diffs?.length}
-                            data-fade={!msg().summary?.diffs?.length && !faded()}
+                            data-fade={!msg().summary?.diffs?.length && animateSummary()}
                             text={summary()}
                           />
                         )}