diff options
| author | Adam <[email protected]> | 2025-10-31 15:37:45 -0500 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-10-31 15:37:50 -0500 |
| commit | 342aa27e03dd0db02b60a15a1779254bce395e19 (patch) | |
| tree | b735062a9fe1c673be733aa861876cfbc94b212d /packages/desktop/src | |
| parent | e1aed0cd01d23e433519621f8d21c0a8ffa3977d (diff) | |
| download | opencode-342aa27e03dd0db02b60a15a1779254bce395e19.tar.gz opencode-342aa27e03dd0db02b60a15a1779254bce395e19.zip | |
wip: desktop work
Diffstat (limited to 'packages/desktop/src')
| -rw-r--r-- | packages/desktop/src/components/message-progress.tsx | 124 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 2 | ||||
| -rw-r--r-- | packages/desktop/src/pages/index.tsx | 108 |
3 files changed, 171 insertions, 63 deletions
diff --git a/packages/desktop/src/components/message-progress.tsx b/packages/desktop/src/components/message-progress.tsx index fd66c5caf..5533ae413 100644 --- a/packages/desktop/src/components/message-progress.tsx +++ b/packages/desktop/src/components/message-progress.tsx @@ -1,7 +1,8 @@ -import { For, JSXElement, Match, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { Part } from "@opencode-ai/ui" +import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" +import { Markdown, Part } from "@opencode-ai/ui" import { useSync } from "@/context/sync" import type { AssistantMessage as AssistantMessageType, Part as PartType, ToolPart } from "@opencode-ai/sdk" +import { Spinner } from "./spinner" export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) { const sync = useSync() @@ -22,37 +23,42 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa ) as ToolPart, ) - const eligibleItems = createMemo(() => { - let allParts = parts() + const resolvedParts = createMemo(() => { + let resolved = 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() + resolved = 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"), - ) + return resolved + }) + const currentText = createMemo( + () => + resolvedParts().findLast((p) => p?.type === "text")?.text || + resolvedParts().findLast((p) => p?.type === "reasoning")?.text, + ) + const eligibleItems = createMemo(() => { + return resolvedParts().filter((p) => p?.type === "tool" && p.state.status === "completed") }) const finishedItems = createMemo<(JSXElement | PartType)[]>(() => [ - "", - "", - <div class="text-text-diff-add-base">Loading...</div>, + <div class="h-8 w-full" />, + <div class="h-8 w-full" />, + <div class="flex items-center gap-x-5 pl-3 text-text-base"> + <Spinner /> <span class="text-12-medium">Thinking...</span> + </div>, ...eligibleItems(), - ...(done() ? ["", "", ""] : []), + ...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []), ]) - const MINIMUM_DELAY = 400 - const [visibleCount, setVisibleCount] = createSignal(1) + const delay = createMemo(() => (done() ? 220 : 400)) + const [visibleCount, setVisibleCount] = createSignal(eligibleItems().length) createEffect(() => { const total = finishedItems().length if (total > visibleCount()) { const timer = setTimeout(() => { setVisibleCount((prev) => prev + 1) - }, MINIMUM_DELAY) + }, delay()) onCleanup(() => clearTimeout(timer)) } else if (total < visibleCount()) { setVisibleCount(total) @@ -66,43 +72,57 @@ export function MessageProgress(props: { assistantMessages: () => AssistantMessa }) 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="flex flex-col gap-3"> <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()})` }} + class="h-30 overflow-hidden pointer-events-none pb-1 + mask-alpha mask-t-from-33% mask-t-from-background-base mask-t-to-transparent + mask-b-from-95% mask-b-from-background-base mask-b-to-transparent" > - <For each={finishedItems()}> - {(part) => { - 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 + 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 (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> </div> + <Show when={currentText()}> + {(text) => ( + <div + class="max-h-36 flex flex-col justify-end overflow-hidden py-3 + mask-alpha mask-t-from-80% mask-t-from-background-base mask-t-to-transparent" + > + <Markdown text={text()} class="w-full shrink-0 overflow-visible" /> + </div> + )} + </Show> </div> ) } diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 10487612b..9b2c10df7 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -334,7 +334,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { onSubmit={handleSubmit} classList={{ "bg-surface-raised-stronger-non-alpha border border-border-strong-base": true, - "rounded-2xl overflow-clip focus-within:shadow-xs-border-selected": true, + "rounded-2xl overflow-clip focus-within:border-transparent focus-within:shadow-xs-border-select": true, [props.class ?? ""]: !!props.class, }} > diff --git a/packages/desktop/src/pages/index.tsx b/packages/desktop/src/pages/index.tsx index 929aeda7a..35e415aaf 100644 --- a/packages/desktop/src/pages/index.tsx +++ b/packages/desktop/src/pages/index.tsx @@ -548,7 +548,79 @@ export default function Page() { <For each={local.session.userMessages()}> {(message) => { const diffs = createMemo(() => message.summary?.diffs ?? []) - const working = createMemo(() => !message.summary?.title) + const working = createMemo(() => !message.summary?.body) + const assistantMessages = createMemo(() => { + return sync.data.message[activeSession().id]?.filter( + (m) => m.role === "assistant" && m.parentID == message.id, + ) as AssistantMessageType[] + }) + const parts = createMemo(() => + assistantMessages().flatMap((m) => sync.data.part[m.id]), + ) + const lastPart = createMemo(() => parts().slice(-1)?.at(0)) + const rawStatus = createMemo(() => { + const defaultStatus = "Working..." + const last = lastPart() + if (!last) return defaultStatus + + if (last.type === "tool") { + switch (last.tool) { + case "task": + return "Delegating work..." + case "todowrite": + case "todoread": + return "Planning next steps..." + case "read": + return "Gathering context..." + case "list": + case "grep": + case "glob": + return "Searching the codebase..." + case "webfetch": + return "Searching the web..." + case "edit": + case "write": + return "Making edits..." + case "bash": + return "Running commands..." + default: + break + } + } else if (last.type === "reasoning") { + return "Thinking..." + } else if (last.type === "text") { + return "Gathering thoughts..." + } + return defaultStatus + }) + + const [status, setStatus] = createSignal(rawStatus()) + let lastStatusChange = Date.now() + let statusTimeout: number | undefined + + createEffect(() => { + const newStatus = rawStatus() + if (newStatus === status()) return + + const timeSinceLastChange = Date.now() - lastStatusChange + + if (timeSinceLastChange >= 1000) { + setStatus(newStatus) + lastStatusChange = Date.now() + if (statusTimeout) { + clearTimeout(statusTimeout) + statusTimeout = undefined + } + } else { + if (statusTimeout) clearTimeout(statusTimeout) + statusTimeout = setTimeout(() => { + setStatus(rawStatus()) + lastStatusChange = Date.now() + statusTimeout = undefined + }, 1000 - timeSinceLastChange) as unknown as number + } + }) + return ( <li class="group/li flex items-center self-stretch"> <button @@ -570,7 +642,10 @@ export default function Page() { "text-text-weak data-[active=true]:text-text-strong group-hover/li:text-text-base": true, }} > - {message.summary?.title ?? local.session.getMessageText(message)} + <Switch> + <Match when={working()}>{status()}</Match> + <Match when={true}>{message.summary?.title}</Match> + </Switch> </div> </button> </li> @@ -604,10 +679,12 @@ export default function Page() { // allowing time for the animations to finish createEffect(() => { + title() setTimeout(() => setTitled(!!title()), 10_000) }) createEffect(() => { - setTimeout(() => setCompleted(!!summary()), 3_000) + summary() + setTimeout(() => setCompleted(!!summary()), 1200) }) return ( @@ -618,9 +695,18 @@ 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={titled()} fallback={<Typewriter as="h1" text={title()} />}> - <h1>{title()}</h1> + <div class="w-full text-14-medium text-text-strong"> + <Show + when={titled()} + fallback={ + <Typewriter + as="h1" + text={title()} + class="overflow-hidden text-ellipsis min-w-0 text-nowrap" + /> + } + > + <h1 class="overflow-hidden text-ellipsis min-w-0 text-nowrap">{title()}</h1> </Show> </div> </div> @@ -628,7 +714,7 @@ export default function Page() { <Message message={message} parts={parts()} /> </div> {/* Summary */} - <Show when={!working()}> + <Show when={completed()}> <div class="w-full flex flex-col gap-6 items-start self-stretch"> <div class="flex flex-col items-start gap-1 self-stretch"> <h2 class="text-12-medium text-text-weak"> @@ -637,7 +723,9 @@ export default function Page() { <Match when={true}>Response</Match> </Switch> </h2> - <Show when={summary()}>{(summary) => <Markdown text={summary()} />}</Show> + <Show when={summary()}> + {(summary) => <Markdown class="[&>*]:fade-up-text" text={summary()} />} + </Show> </div> <Accordion class="w-full" multiple> <For each={diffs()}> @@ -699,8 +787,8 @@ export default function Page() { <div class="flex items-center gap-1 self-stretch"> <div class="text-12-medium"> <Switch> - <Match when={expanded()}>Hide steps</Match> - <Match when={!expanded()}>Show steps</Match> + <Match when={expanded()}>Hide details</Match> + <Match when={!expanded()}>Show details</Match> </Switch> </div> <Collapsible.Arrow /> |
