diff options
| author | Adam <[email protected]> | 2025-10-31 11:54:27 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-31 12:00:44 -0500 |
| commit | ffc889b99e61c6f21ce68985ee398c3031a5b19b (patch) | |
| tree | b4356c27d343f127265ae020b015e8224b658ba4 /packages/desktop/src/components | |
| parent | 36b48a44ac1f0c9593a4abdf1d21980a2bfaee22 (diff) | |
| download | opencode-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.tsx | 82 | ||||
| -rw-r--r-- | packages/desktop/src/components/spinner.tsx | 39 |
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> + ) +} |
