diff options
| author | Adam <[email protected]> | 2026-02-18 06:31:26 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-18 06:32:35 -0600 |
| commit | 3394402aefecbaa7f7f469344811b4089a2ddb01 (patch) | |
| tree | d139c7ba725e0f3081b98acfb38ac2f9e0578a77 /packages | |
| parent | 6cd3a5902260764899a566b33d7f76123b9c9800 (diff) | |
| download | opencode-3394402aefecbaa7f7f469344811b4089a2ddb01.tar.gz opencode-3394402aefecbaa7f7f469344811b4089a2ddb01.zip | |
chore: cleanup
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/ui/src/components/basic-tool.tsx | 40 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.css | 60 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 134 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.css | 39 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 111 |
5 files changed, 274 insertions, 110 deletions
diff --git a/packages/ui/src/components/basic-tool.tsx b/packages/ui/src/components/basic-tool.tsx index 5cc4367a6..53bdc9ce1 100644 --- a/packages/ui/src/components/basic-tool.tsx +++ b/packages/ui/src/components/basic-tool.tsx @@ -1,4 +1,4 @@ -import { createEffect, createSignal, For, Match, Show, Switch, type JSX } from "solid-js" +import { createEffect, createSignal, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js" import { Collapsible } from "./collapsible" import type { IconProps } from "./icon" import { TextShimmer } from "./text-shimmer" @@ -27,18 +27,52 @@ export interface BasicToolProps { hideDetails?: boolean defaultOpen?: boolean forceOpen?: boolean + defer?: boolean locked?: boolean onSubtitleClick?: () => void } export function BasicTool(props: BasicToolProps) { const [open, setOpen] = createSignal(props.defaultOpen ?? false) + const [ready, setReady] = createSignal(open()) const pending = () => props.status === "pending" || props.status === "running" + let frame: number | undefined + + const cancel = () => { + if (frame === undefined) return + cancelAnimationFrame(frame) + frame = undefined + } + + onCleanup(cancel) + createEffect(() => { if (props.forceOpen) setOpen(true) }) + createEffect( + on( + open, + (value) => { + if (!props.defer) return + if (!value) { + cancel() + setReady(false) + return + } + + cancel() + frame = requestAnimationFrame(() => { + frame = undefined + if (!open()) return + setReady(true) + }) + }, + { defer: true }, + ), + ) + const handleOpenChange = (value: boolean) => { if (pending()) return if (props.locked && !value) return @@ -114,7 +148,9 @@ export function BasicTool(props: BasicToolProps) { </div> </Collapsible.Trigger> <Show when={props.children && !props.hideDetails}> - <Collapsible.Content>{props.children}</Collapsible.Content> + <Collapsible.Content> + <Show when={!props.defer || ready()}>{props.children}</Show> + </Collapsible.Content> </Show> </Collapsible> ) diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index bfcedde83..60c411bfd 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -326,8 +326,7 @@ } [data-slot="collapsible-content"]:has([data-component="edit-content"]), -[data-slot="collapsible-content"]:has([data-component="write-content"]), -[data-slot="collapsible-content"]:has([data-component="apply-patch-files"]) { +[data-slot="collapsible-content"]:has([data-component="write-content"]) { border: 1px solid var(--border-weak-base); border-radius: 6px; background: transparent; @@ -1219,21 +1218,31 @@ } } -[data-component="apply-patch-files"] { - display: flex; - flex-direction: column; -} +[data-component="accordion"][data-scope="apply-patch"] { + [data-slot="apply-patch-trigger-content"] { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-width: 0; + gap: 12px; + } -[data-component="apply-patch-file"] { - display: flex; - flex-direction: column; + [data-slot="apply-patch-file-path"] { + font-family: var(--font-family-mono); + font-size: var(--font-size-small); + color: var(--text-weak); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-grow: 1; + } - [data-slot="apply-patch-file-header"] { - display: flex; + [data-slot="apply-patch-trigger-actions"] { + flex-shrink: 0; + display: inline-flex; align-items: center; - gap: 8px; - padding: 8px 12px; - background-color: transparent; + gap: 10px; } [data-slot="apply-patch-file-action"] { @@ -1257,26 +1266,23 @@ } } - [data-slot="apply-patch-file-path"] { - font-family: var(--font-family-mono); - font-size: var(--font-size-small); - color: var(--text-weak); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-grow: 1; - } - [data-slot="apply-patch-deletion-count"] { font-family: var(--font-family-mono); font-size: var(--font-size-small); color: var(--text-critical-base); flex-shrink: 0; } -} -[data-component="apply-patch-file"] + [data-component="apply-patch-file"] { - border-top: 1px solid var(--border-weaker-base); + [data-slot="apply-patch-file-chevron"] { + display: inline-flex; + color: var(--icon-weaker); + transform: rotate(-90deg); + transition: transform 0.15s ease; + } + + [data-slot="accordion-item"][data-expanded] [data-slot="apply-patch-file-chevron"] { + transform: rotate(0deg); + } } [data-component="apply-patch-file-diff"] { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index 24ae16a31..875f88611 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -35,6 +35,7 @@ import { useDialog } from "../context/dialog" import { useI18n } from "../context/i18n" import { BasicTool } from "./basic-tool" import { GenericTool } from "./basic-tool" +import { Accordion } from "./accordion" import { Button } from "./button" import { Card } from "./card" import { Collapsible } from "./collapsible" @@ -1482,6 +1483,7 @@ ToolRegistry.register({ <BasicTool {...props} icon="code-lines" + defer trigger={ <div data-component="edit-trigger"> <div data-slot="message-part-title-area"> @@ -1542,6 +1544,7 @@ ToolRegistry.register({ <BasicTool {...props} icon="code-lines" + defer trigger={ <div data-component="write-trigger"> <div data-slot="message-part-title-area"> @@ -1602,6 +1605,16 @@ ToolRegistry.register({ const i18n = useI18n() const diffComponent = useDiffComponent() const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) + const [expanded, setExpanded] = createSignal<string[]>([]) + let seeded = false + + createEffect(() => { + const list = files() + if (list.length === 0) return + if (seeded) return + seeded = true + setExpanded(list.filter((f) => f.type !== "delete").map((f) => f.filePath)) + }) const subtitle = createMemo(() => { const count = files().length @@ -1613,60 +1626,89 @@ ToolRegistry.register({ <BasicTool {...props} icon="code-lines" + defer trigger={{ title: i18n.t("ui.tool.patch"), subtitle: subtitle(), }} > <Show when={files().length > 0}> - <div data-component="apply-patch-files"> + <Accordion + multiple + data-scope="apply-patch" + value={expanded()} + onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > <For each={files()}> - {(file) => ( - <div data-component="apply-patch-file"> - <div data-slot="apply-patch-file-header"> - <Switch> - <Match when={file.type === "delete"}> - <span data-slot="apply-patch-file-action" data-type="delete"> - {i18n.t("ui.patch.action.deleted")} - </span> - </Match> - <Match when={file.type === "add"}> - <span data-slot="apply-patch-file-action" data-type="add"> - {i18n.t("ui.patch.action.created")} - </span> - </Match> - <Match when={file.type === "move"}> - <span data-slot="apply-patch-file-action" data-type="move"> - {i18n.t("ui.patch.action.moved")} - </span> - </Match> - <Match when={file.type === "update"}> - <span data-slot="apply-patch-file-action" data-type="update"> - {i18n.t("ui.patch.action.patched")} - </span> - </Match> - </Switch> - <span data-slot="apply-patch-file-path">{file.relativePath}</span> - <Show when={file.type !== "delete"}> - <DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} /> - </Show> - <Show when={file.type === "delete"}> - <span data-slot="apply-patch-deletion-count">-{file.deletions}</span> - </Show> - </div> - <Show when={file.type !== "delete"}> - <div data-component="apply-patch-file-diff"> - <Dynamic - component={diffComponent} - before={{ name: file.filePath, contents: file.before }} - after={{ name: file.filePath, contents: file.after }} - /> - </div> - </Show> - </div> - )} + {(file) => { + const active = createMemo(() => expanded().includes(file.filePath)) + const [visible, setVisible] = createSignal(false) + + createEffect(() => { + if (!active()) { + setVisible(false) + return + } + + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }) + + return ( + <Accordion.Item value={file.filePath} data-type={file.type}> + <Accordion.Header> + <Accordion.Trigger> + <div data-slot="apply-patch-trigger-content"> + <span data-slot="apply-patch-file-path">{file.relativePath}</span> + <div data-slot="apply-patch-trigger-actions"> + <Switch> + <Match when={file.type === "delete"}> + <span data-slot="apply-patch-file-action" data-type="delete"> + {i18n.t("ui.patch.action.deleted")} + </span> + </Match> + <Match when={file.type === "add"}> + <span data-slot="apply-patch-file-action" data-type="add"> + {i18n.t("ui.patch.action.created")} + </span> + </Match> + <Match when={file.type === "move"}> + <span data-slot="apply-patch-file-action" data-type="move"> + {i18n.t("ui.patch.action.moved")} + </span> + </Match> + </Switch> + <Show when={file.type !== "delete"}> + <DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} /> + </Show> + <Show when={file.type === "delete"}> + <span data-slot="apply-patch-deletion-count">-{file.deletions}</span> + </Show> + <span data-slot="apply-patch-file-chevron"> + <Icon name="chevron-down" size="small" /> + </span> + </div> + </div> + </Accordion.Trigger> + </Accordion.Header> + <Accordion.Content> + <Show when={visible()}> + <div data-component="apply-patch-file-diff"> + <Dynamic + component={diffComponent} + before={{ name: file.filePath, contents: file.before }} + after={{ name: file.movePath ?? file.filePath, contents: file.after }} + /> + </div> + </Show> + </Accordion.Content> + </Accordion.Item> + ) + }} </For> - </div> + </Accordion> </Show> </BasicTool> ) diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 5d58f0f71..e7da2b6f0 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -130,19 +130,13 @@ gap: 12px; } - [data-component="session-turn-diff"] { - border: 1px solid var(--border-weaker-base); - border-radius: var(--radius-md); - overflow: clip; - } - - [data-slot="session-turn-diff-header"] { + [data-slot="session-turn-diff-trigger"] { display: flex; align-items: center; justify-content: space-between; gap: 12px; - padding: 6px 10px; - border-bottom: 1px solid var(--border-weaker-base); + width: 100%; + min-width: 0; } [data-slot="session-turn-diff-path"] { @@ -166,9 +160,36 @@ font-weight: var(--font-weight-medium); } + [data-slot="session-turn-diff-meta"] { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 10px; + } + + [data-slot="session-turn-diff-chevron"] { + display: inline-flex; + color: var(--icon-weaker); + transform: rotate(-90deg); + transition: transform 0.15s ease; + } + + [data-slot="accordion-item"][data-expanded] [data-slot="session-turn-diff-chevron"] { + transform: rotate(0deg); + } + [data-slot="session-turn-diff-view"] { background-color: var(--surface-inset-base); width: 100%; min-width: 0; + max-height: 420px; + overflow-y: auto; + overflow-x: hidden; + scrollbar-width: none; + -ms-overflow-style: none; + } + + [data-slot="session-turn-diff-view"]::-webkit-scrollbar { + display: none; } } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index e4c0a2273..a418fddd9 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -4,12 +4,14 @@ import { useDiffComponent } from "../context/diff" import { Binary } from "@opencode-ai/util/binary" import { getDirectory, getFilename } from "@opencode-ai/util/path" -import { createMemo, createSignal, For, ParentProps, Show } from "solid-js" +import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js" import { Dynamic } from "solid-js/web" import { AssistantParts, Message } from "./message-part" import { Card } from "./card" +import { Accordion } from "./accordion" import { Collapsible } from "./collapsible" import { DiffChanges } from "./diff-changes" +import { Icon } from "./icon" import { TextShimmer } from "./text-shimmer" import { createAutoScroll } from "../hooks" import { useI18n } from "../context/i18n" @@ -175,6 +177,17 @@ export function SessionTurn( }) const edited = createMemo(() => diffs().length) const [open, setOpen] = createSignal(false) + const [expanded, setExpanded] = createSignal<string[]>([]) + + createEffect( + on( + open, + (value, prev) => { + if (!value && prev) setExpanded([]) + }, + { defer: true }, + ), + ) const assistantMessages = createMemo( () => { @@ -280,7 +293,7 @@ export function SessionTurn( /> </div> </Show> - <Show when={edited() > 0}> + <Show when={edited() > 0 && !working()}> <div data-slot="session-turn-diffs"> <Collapsible open={open()} onOpenChange={setOpen} variant="ghost"> <Collapsible.Trigger> @@ -302,30 +315,76 @@ export function SessionTurn( <Collapsible.Content> <Show when={open()}> <div data-component="session-turn-diffs-content"> - <For each={diffs()}> - {(diff) => ( - <div data-component="session-turn-diff"> - <div data-slot="session-turn-diff-header"> - <span data-slot="session-turn-diff-path"> - <Show when={diff.file.includes("/")}> - <span data-slot="session-turn-diff-directory">{getDirectory(diff.file)}</span> - </Show> - <span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span> - </span> - <span data-slot="session-turn-diff-changes"> - <DiffChanges changes={diff} /> - </span> - </div> - <div data-slot="session-turn-diff-view"> - <Dynamic - component={diffComponent} - before={{ name: diff.file, contents: diff.before }} - after={{ name: diff.file, contents: diff.after }} - /> - </div> - </div> - )} - </For> + <Accordion + multiple + value={expanded()} + onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + <For each={diffs()}> + {(diff) => { + const active = createMemo(() => expanded().includes(diff.file)) + const [visible, setVisible] = createSignal(false) + + createEffect( + on( + active, + (value) => { + if (!value) { + setVisible(false) + return + } + + requestAnimationFrame(() => { + if (!active()) return + setVisible(true) + }) + }, + { defer: true }, + ), + ) + + return ( + <Accordion.Item value={diff.file}> + <Accordion.Header> + <Accordion.Trigger> + <div data-slot="session-turn-diff-trigger"> + <span data-slot="session-turn-diff-path"> + <Show when={diff.file.includes("/")}> + <span data-slot="session-turn-diff-directory"> + {getDirectory(diff.file)} + </span> + </Show> + <span data-slot="session-turn-diff-filename"> + {getFilename(diff.file)} + </span> + </span> + <div data-slot="session-turn-diff-meta"> + <span data-slot="session-turn-diff-changes"> + <DiffChanges changes={diff} /> + </span> + <span data-slot="session-turn-diff-chevron"> + <Icon name="chevron-down" size="small" /> + </span> + </div> + </div> + </Accordion.Trigger> + </Accordion.Header> + <Accordion.Content> + <Show when={visible()}> + <div data-slot="session-turn-diff-view" data-scrollable> + <Dynamic + component={diffComponent} + before={{ name: diff.file, contents: diff.before }} + after={{ name: diff.file, contents: diff.after }} + /> + </div> + </Show> + </Accordion.Content> + </Accordion.Item> + ) + }} + </For> + </Accordion> </div> </Show> </Collapsible.Content> |
