summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'packages/desktop/src/components')
-rw-r--r--packages/desktop/src/components/file-tree.tsx5
-rw-r--r--packages/desktop/src/components/message-progress.tsx187
-rw-r--r--packages/desktop/src/components/prompt-input.tsx15
-rw-r--r--packages/desktop/src/components/session-review.tsx104
-rw-r--r--packages/desktop/src/components/spinner.tsx39
-rw-r--r--packages/desktop/src/components/sticky-accordion-header.tsx17
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)}&lrm;</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>
- )
-}