Adam 3 месяцев назад
Родитель
Сommit
e1aed0cd01

+ 56 - 30
packages/desktop/src/components/message-progress.tsx

@@ -1,23 +1,47 @@
-import { For, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
+import { For, JSXElement, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
 import { Part } from "@opencode-ai/ui"
 import { useSync } from "@/context/sync"
-import type { AssistantMessage as AssistantMessageType } from "@opencode-ai/sdk"
+import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk"
 
-export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[] }) {
+export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) {
   const sync = useSync()
-  const items = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
+  const parts = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))
+  const done = createMemo(() => props.done ?? false)
+  const currentTask = createMemo(
+    () =>
+      parts().findLast(
+        (p) =>
+          p &&
+          p.type === "tool" &&
+          p.tool === "task" &&
+          p.state &&
+          "metadata" in p.state &&
+          p.state.metadata &&
+          p.state.metadata.sessionId &&
+          p.state.status === "running",
+      ) as ToolPart,
+  )
 
-  const finishedItems = createMemo(() => [
-    "",
-    "",
-    "Loading...",
-    ...items().filter(
+  const eligibleItems = createMemo(() => {
+    let allParts = parts()
+    const task = currentTask()
+    if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) {
+      const messages = sync.data.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant")
+      allParts = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts()
+    }
+    return allParts.filter(
       (p) =>
         p?.type === "text" ||
         (p?.type === "reasoning" && p.time?.end) ||
         (p?.type === "tool" && p.state.status === "completed"),
-    ),
+    )
+  })
+  const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [
+    "",
     "",
+    <div class="text-text-diff-add-base">Loading...</div>,
+    ...eligibleItems(),
+    ...(done() ? ["", "", ""] : []),
   ])
 
   const MINIMUM_DELAY = 400
@@ -54,26 +78,28 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa
       >
         <For each={finishedItems()}>
           {(part) => {
-            if (typeof part === "string") return <div class="h-8 flex items-center w-full">{part}</div>
-            const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
-            return (
-              <div class="h-8 flex items-center w-full">
-                <Switch>
-                  <Match when={part.type === "text" && part}>
-                    {(p) => (
-                      <div
-                        textContent={p().text}
-                        class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
-                      />
-                    )}
-                  </Match>
-                  <Match when={part.type === "reasoning" && part}>
-                    {(p) => <Part message={message()!} part={p()} />}
-                  </Match>
-                  <Match when={part.type === "tool" && part}>{(p) => <Part message={message()!} part={p()} />}</Match>
-                </Switch>
-              </div>
-            )
+            if (part && typeof part === "object" && "type" in part) {
+              const message = createMemo(() => sync.data.message[part.sessionID].find((m) => m.id === part.messageID))
+              return (
+                <div class="h-8 flex items-center w-full">
+                  <Switch>
+                    <Match when={part.type === "text" && part}>
+                      {(p) => (
+                        <div
+                          textContent={p().text}
+                          class="text-12-regular text-text-base whitespace-nowrap truncate w-full"
+                        />
+                      )}
+                    </Match>
+                    <Match when={part.type === "reasoning" && part}>
+                      {(p) => <Part message={message()!} part={p()} />}
+                    </Match>
+                    <Match when={part.type === "tool" && part}>{(p) => <Part message={message()!} part={p()} />}</Match>
+                  </Switch>
+                </div>
+              )
+            }
+            return <div class="h-8 flex items-center w-full">{part}</div>
           }}
         </For>
       </div>

+ 13 - 7
packages/desktop/src/pages/index.tsx

@@ -583,7 +583,8 @@ export default function Page() {
                             <For each={local.session.userMessages()}>
                               {(message) => {
                                 const isActive = createMemo(() => local.session.activeMessage()?.id === message.id)
-                                const [initialized, setInitialized] = createSignal(!!message.summary?.title)
+                                const [titled, setTitled] = createSignal(!!message.summary?.title)
+                                const [completed, setCompleted] = createSignal(!!message.summary?.body)
                                 const [expanded, setExpanded] = createSignal(false)
                                 const parts = createMemo(() => sync.data.part[message.id])
                                 const title = createMemo(() => message.summary?.title)
@@ -597,11 +598,16 @@ export default function Page() {
                                 const hasToolPart = createMemo(() =>
                                   assistantMessages()
                                     ?.flatMap((m) => sync.data.part[m.id])
-                                    .some((p) => p.type === "tool"),
+                                    .some((p) => p?.type === "tool"),
                                 )
                                 const working = createMemo(() => !summary())
+
+                                // allowing time for the animations to finish
+                                createEffect(() => {
+                                  setTimeout(() => setTitled(!!title()), 10_000)
+                                })
                                 createEffect(() => {
-                                  setTimeout(() => setInitialized(!!title()), 10_000)
+                                  setTimeout(() => setCompleted(!!summary()), 3_000)
                                 })
 
                                 return (
@@ -613,7 +619,7 @@ export default function Page() {
                                       {/* Title */}
                                       <div class="py-2 flex flex-col items-start gap-2 self-stretch sticky top-0 bg-background-stronger z-10">
                                         <div class="text-14-medium text-text-strong overflow-hidden text-ellipsis min-w-0">
-                                          <Show when={initialized()} fallback={<Typewriter as="h1" text={title()} />}>
+                                          <Show when={titled()} fallback={<Typewriter as="h1" text={title()} />}>
                                             <h1>{title()}</h1>
                                           </Show>
                                         </div>
@@ -684,10 +690,10 @@ export default function Page() {
                                       {/* Response */}
                                       <div class="w-full">
                                         <Switch>
-                                          <Match when={working()}>
-                                            <MessageProgress assistantMessages={assistantMessages} />
+                                          <Match when={!completed()}>
+                                            <MessageProgress assistantMessages={assistantMessages} done={!working()} />
                                           </Match>
-                                          <Match when={!working() && hasToolPart()}>
+                                          <Match when={completed() && hasToolPart()}>
                                             <Collapsible variant="ghost" open={expanded()} onOpenChange={setExpanded}>
                                               <Collapsible.Trigger class="text-text-weak hover:text-text-strong">
                                                 <div class="flex items-center gap-1 self-stretch">