diff options
Diffstat (limited to 'packages/desktop/src/components')
| -rw-r--r-- | packages/desktop/src/components/file-tree.tsx | 5 | ||||
| -rw-r--r-- | packages/desktop/src/components/message-progress.tsx | 187 | ||||
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 15 | ||||
| -rw-r--r-- | packages/desktop/src/components/session-review.tsx | 104 | ||||
| -rw-r--r-- | packages/desktop/src/components/spinner.tsx | 39 | ||||
| -rw-r--r-- | packages/desktop/src/components/sticky-accordion-header.tsx | 17 |
6 files changed, 13 insertions, 354 deletions
diff --git a/packages/desktop/src/components/file-tree.tsx b/packages/desktop/src/components/file-tree.tsx index 1347ecae6..f3729a8d3 100644 --- a/packages/desktop/src/components/file-tree.tsx +++ b/packages/desktop/src/components/file-tree.tsx @@ -1,6 +1,7 @@ import { useLocal, type LocalFile } from "@/context/local" -import { Tooltip } from "@opencode-ai/ui" -import { Collapsible, FileIcon } from "@/ui" +import { Collapsible } from "@/ui" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { Tooltip } from "@opencode-ai/ui/tooltip" import { For, Match, Switch, Show, type ComponentProps, type ParentProps } from "solid-js" import { Dynamic } from "solid-js/web" diff --git a/packages/desktop/src/components/message-progress.tsx b/packages/desktop/src/components/message-progress.tsx deleted file mode 100644 index a9be2ae5e..000000000 --- a/packages/desktop/src/components/message-progress.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import { For, JSXElement, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js" -import { Part } from "@opencode-ai/ui" -import { useSync } from "@/context/sync" -import type { AssistantMessage as AssistantMessageType, ToolPart } from "@opencode-ai/sdk" -import { Spinner } from "./spinner" - -export function MessageProgress(props: { assistantMessages: () => AssistantMessageType[]; done?: boolean }) { - const sync = useSync() - 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 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") - resolved = messages?.flatMap((m) => sync.data.part[m.id]) ?? parts() - } - 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") as ToolPart[] - }) - const finishedItems = createMemo<(JSXElement | ToolPart)[]>(() => [ - <div class="h-8 w-full" />, - <div class="h-8 w-full" />, - <div class="h-8 w-full" />, - ...eligibleItems(), - ...(done() ? [<div class="h-8 w-full" />, <div class="h-8 w-full" />, <div class="h-8 w-full" />] : []), - ]) - - 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) - }, 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` - }) - - const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0)) - const rawStatus = createMemo(() => { - const last = lastPart() - if (!last) return undefined - - 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 undefined - }) - - const [status, setStatus] = createSignal(rawStatus()) - let lastStatusChange = Date.now() - let statusTimeout: number | undefined - - createEffect(() => { - const newStatus = rawStatus() - if (newStatus === status() || !newStatus) return - - const timeSinceLastChange = Date.now() - lastStatusChange - - if (timeSinceLastChange >= 1500) { - 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 ( - <div class="flex flex-col gap-3"> - {/* <Show when={currentText()}> */} - {/* {(text) => ( */} - {/* <div */} - {/* class="h-20 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 class="flex items-center gap-x-5 pl-3 border border-transparent text-text-base"> - <Spinner /> <span class="text-12-medium">{status() ?? "Considering next steps..."}</span> - </div> - <Show when={eligibleItems().length > 0}> - <div - 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" - > - <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) => ( - <Switch> - <Match when={part && typeof part === "object" && "type" in part && part}> - {(p) => { - const part = p() as ToolPart - const message = createMemo(() => - sync.data.message[part.sessionID].find((m) => m.id === part.messageID), - ) - return ( - <div class="h-8 flex items-center w-full"> - <Part message={message()!} part={part} /> - </div> - ) - }} - </Match> - <Match when={true}> - <div class="h-8 flex items-center w-full">{part as JSXElement}</div> - </Match> - </Switch> - )} - </For> - </div> - </div> - </Show> - </div> - ) -} diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index b2e552f71..94d8ff882 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -1,8 +1,6 @@ -import { Button, Icon, IconButton, Select, SelectDialog, Tooltip } from "@opencode-ai/ui" import { useFilteredList } from "@opencode-ai/ui/hooks" import { createEffect, on, Component, Show, For, onMount, onCleanup, Switch, Match } from "solid-js" import { createStore } from "solid-js/store" -import { FileIcon } from "@/ui" import { getDirectory, getFilename } from "@/utils" import { createFocusSignal } from "@solid-primitives/active-element" import { useLocal } from "@/context/local" @@ -11,6 +9,13 @@ import { ContentPart, DEFAULT_PROMPT, isPromptEqual, Prompt, useSession } from " import { useSDK } from "@/context/sdk" import { useNavigate } from "@solidjs/router" import { useSync } from "@/context/sync" +import { FileIcon } from "@opencode-ai/ui/file-icon" +import { SelectDialog } from "@opencode-ai/ui/select-dialog" +import { Button } from "@opencode-ai/ui/button" +import { Icon } from "@opencode-ai/ui/icon" +import { Tooltip } from "@opencode-ai/ui/tooltip" +import { IconButton } from "@opencode-ai/ui/icon-button" +import { Select } from "@opencode-ai/ui/select" interface PromptInputProps { class?: string @@ -184,8 +189,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => { const range = selection.getRangeAt(0) if (atMatch) { - let node: Node | null = range.startContainer - let offset = range.startOffset + // let node: Node | null = range.startContainer + // let offset = range.startOffset let runningLength = 0 const walker = document.createTreeWalker(editorRef, NodeFilter.SHOW_TEXT, null) @@ -448,7 +453,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { {(i) => ( <div class="w-full flex items-center justify-between gap-x-3"> <div class="flex items-center gap-x-2.5 text-text-muted grow min-w-0"> - <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0 " /> + <img src={`https://models.dev/logos/${i.provider.id}.svg`} class="size-6 p-0.5 shrink-0" /> <div class="flex gap-x-3 items-baseline flex-[1_0_0]"> <span class="text-14-medium text-text-strong overflow-hidden text-ellipsis">{i.name}</span> <Show when={i.release_date}> diff --git a/packages/desktop/src/components/session-review.tsx b/packages/desktop/src/components/session-review.tsx deleted file mode 100644 index 6022164c5..000000000 --- a/packages/desktop/src/components/session-review.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useSession } from "@/context/session" -import { FileIcon } from "@/ui" -import { getDirectory, getFilename } from "@/utils" -import { Accordion, Button, Diff, DiffChanges, Icon, IconButton, Tooltip } from "@opencode-ai/ui" -import { For, Match, Show, Switch } from "solid-js" -import { StickyAccordionHeader } from "./sticky-accordion-header" -import { createStore } from "solid-js/store" -import { useLayout } from "@/context/layout" - -export const SessionReview = (props: { split?: boolean; class?: string; hideExpand?: boolean }) => { - const layout = useLayout() - const session = useSession() - const [store, setStore] = createStore({ - open: session.diffs().map((d) => d.file), - }) - - const handleChange = (open: string[]) => { - setStore("open", open) - } - - const handleExpandOrCollapseAll = () => { - if (store.open.length > 0) { - setStore("open", []) - } else { - setStore( - "open", - session.diffs().map((d) => d.file), - ) - } - } - - return ( - <div - classList={{ - "flex flex-col gap-3 h-full overflow-y-auto no-scrollbar": true, - [props.class ?? ""]: !!props.class, - }} - > - <div class="sticky top-0 z-20 bg-background-stronger h-8 shrink-0 flex justify-between items-center self-stretch"> - <div class="text-14-medium text-text-strong">Session changes</div> - <div class="flex items-center gap-x-4 pr-px"> - <Button size="normal" icon="chevron-grabber-vertical" onClick={handleExpandOrCollapseAll}> - <Switch> - <Match when={store.open.length > 0}>Collapse all</Match> - <Match when={true}>Expand all</Match> - </Switch> - </Button> - <Show when={!props.hideExpand}> - <Tooltip value="Open in tab"> - <IconButton - icon="expand" - variant="ghost" - onClick={() => { - layout.review.tab() - session.layout.setActiveTab("review") - }} - /> - </Tooltip> - </Show> - </div> - </div> - <Accordion multiple value={store.open} onChange={handleChange}> - <For each={session.diffs()}> - {(diff) => ( - <Accordion.Item value={diff.file}> - <StickyAccordionHeader class="top-11 data-expanded:before:-top-11"> - <Accordion.Trigger class="bg-background-stronger"> - <div class="flex items-center justify-between w-full gap-5"> - <div class="grow flex items-center gap-5 min-w-0"> - <FileIcon node={{ path: diff.file, type: "file" }} class="shrink-0 size-4" /> - <div class="flex grow min-w-0"> - <Show when={diff.file.includes("/")}> - <span class="text-text-base truncate-start">{getDirectory(diff.file)}‎</span> - </Show> - <span class="text-text-strong shrink-0">{getFilename(diff.file)}</span> - </div> - </div> - <div class="shrink-0 flex gap-4 items-center justify-end"> - <DiffChanges changes={diff} /> - <Icon name="chevron-grabber-vertical" size="small" /> - </div> - </div> - </Accordion.Trigger> - </StickyAccordionHeader> - <Accordion.Content> - <Diff - diffStyle={props.split ? "split" : "unified"} - before={{ - name: diff.file!, - contents: diff.before!, - }} - after={{ - name: diff.file!, - contents: diff.after!, - }} - /> - </Accordion.Content> - </Accordion.Item> - )} - </For> - </Accordion> - </div> - ) -} diff --git a/packages/desktop/src/components/spinner.tsx b/packages/desktop/src/components/spinner.tsx deleted file mode 100644 index 5fc4cda64..000000000 --- a/packages/desktop/src/components/spinner.tsx +++ /dev/null @@ -1,39 +0,0 @@ -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> - ) -} diff --git a/packages/desktop/src/components/sticky-accordion-header.tsx b/packages/desktop/src/components/sticky-accordion-header.tsx deleted file mode 100644 index cb3f0d5eb..000000000 --- a/packages/desktop/src/components/sticky-accordion-header.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Accordion } from "@opencode-ai/ui" -import { ParentProps } from "solid-js" - -export function StickyAccordionHeader(props: ParentProps<{ class?: string }>) { - return ( - <Accordion.Header - classList={{ - "sticky top-0 data-expanded:z-10": true, - "data-expanded:before:content-[''] data-expanded:before:z-[-10]": true, - "data-expanded:before:absolute data-expanded:before:inset-0 data-expanded:before:bg-background-stronger": true, - [props.class ?? ""]: !!props.class, - }} - > - {props.children} - </Accordion.Header> - ) -} |
