summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/components/message-progress.tsx
blob: f77e196b58d73cacfc7df2b46a85e04dbe1553b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { For, 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"

export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[] }) {
  const sync = useSync()
  const items = createMemo(() => props.assistantMessages().flatMap((m) => sync.data.part[m.id]))

  const finishedItems = createMemo(() => [
    "",
    "",
    "Loading...",
    ...items().filter(
      (p) =>
        p?.type === "text" ||
        (p?.type === "reasoning" && p.time?.end) ||
        (p?.type === "tool" && p.state.status === "completed"),
    ),
    "",
  ])

  const MINIMUM_DELAY = 400
  const [visibleCount, setVisibleCount] = createSignal(1)

  createEffect(() => {
    const total = finishedItems().length
    if (total > visibleCount()) {
      const timer = setTimeout(() => {
        setVisibleCount((prev) => prev + 1)
      }, MINIMUM_DELAY)
      onCleanup(() => clearTimeout(timer))
    } else if (total < visibleCount()) {
      setVisibleCount(total)
    }
  })

  const translateY = createMemo(() => {
    const total = visibleCount()
    if (total < 2) return "0px"
    return `-${(total - 2) * 40 - 8}px`
  })

  return (
    <div
      class="h-30 overflow-hidden pointer-events-none 
             mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent
             mask-b-from-90% mask-b-from-background-base mask-b-to-transparent"
    >
      <div
        class="w-full flex flex-col items-start self-stretch gap-2 py-8
               transform transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)]"
        style={{ transform: `translateY(${translateY()})` }}
      >
        <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>
            )
          }}
        </For>
      </div>
    </div>
  )
}