summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-10-31 11:54:27 -0500
committerAdam <[email protected]>2025-10-31 12:00:44 -0500
commitffc889b99e61c6f21ce68985ee398c3031a5b19b (patch)
treeb4356c27d343f127265ae020b015e8224b658ba4 /packages/desktop/src/components
parent36b48a44ac1f0c9593a4abdf1d21980a2bfaee22 (diff)
downloadopencode-ffc889b99e61c6f21ce68985ee398c3031a5b19b.tar.gz
opencode-ffc889b99e61c6f21ce68985ee398c3031a5b19b.zip
wip: desktop work
Diffstat (limited to 'packages/desktop/src/components')
-rw-r--r--packages/desktop/src/components/message-progress.tsx82
-rw-r--r--packages/desktop/src/components/spinner.tsx39
2 files changed, 121 insertions, 0 deletions
diff --git a/packages/desktop/src/components/message-progress.tsx b/packages/desktop/src/components/message-progress.tsx
new file mode 100644
index 000000000..f77e196b5
--- /dev/null
+++ b/packages/desktop/src/components/message-progress.tsx
@@ -0,0 +1,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>
+ )
+}
diff --git a/packages/desktop/src/components/spinner.tsx b/packages/desktop/src/components/spinner.tsx
new file mode 100644
index 000000000..5fc4cda64
--- /dev/null
+++ b/packages/desktop/src/components/spinner.tsx
@@ -0,0 +1,39 @@
+import { ComponentProps, For } from "solid-js"
+
+export function Spinner(props: { class?: string; classList?: ComponentProps<"div">["classList"] }) {
+ const squares = Array.from({ length: 16 }, (_, i) => ({
+ id: i,
+ x: (i % 4) * 4,
+ y: Math.floor(i / 4) * 4,
+ delay: Math.random() * 3,
+ duration: 2 + Math.random() * 2,
+ }))
+
+ return (
+ <svg
+ viewBox="0 0 15 15"
+ classList={{
+ "size-4": true,
+ ...(props.classList ?? {}),
+ [props.class ?? ""]: !!props.class,
+ }}
+ fill="currentColor"
+ >
+ <For each={squares}>
+ {(square) => (
+ <rect
+ x={square.x}
+ y={square.y}
+ width="3"
+ height="3"
+ rx="1"
+ style={{
+ animation: `pulse-opacity ${square.duration}s ease-in-out infinite`,
+ "animation-delay": `${square.delay}s`,
+ }}
+ />
+ )}
+ </For>
+ </svg>
+ )
+}