From 3394402aefecbaa7f7f469344811b4089a2ddb01 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Wed, 18 Feb 2026 06:31:26 -0600 Subject: [PATCH] chore: cleanup --- packages/ui/src/components/basic-tool.tsx | 40 +++++- packages/ui/src/components/message-part.css | 62 +++++---- packages/ui/src/components/message-part.tsx | 134 +++++++++++++------- packages/ui/src/components/session-turn.css | 39 ++++-- packages/ui/src/components/session-turn.tsx | 111 ++++++++++++---- 5 files changed, 275 insertions(+), 111 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) { - {props.children} + + {props.children} + ) 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="apply-patch-file"] { - display: flex; - flex-direction: column; - - [data-slot="apply-patch-file-header"] { +[data-component="accordion"][data-scope="apply-patch"] { + [data-slot="apply-patch-trigger-content"] { display: flex; align-items: center; - gap: 8px; - padding: 8px 12px; - background-color: transparent; + justify-content: space-between; + width: 100%; + min-width: 0; + gap: 12px; + } + + [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-trigger-actions"] { + flex-shrink: 0; + display: inline-flex; + align-items: center; + 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({
@@ -1542,6 +1544,7 @@ ToolRegistry.register({
@@ -1602,6 +1605,16 @@ ToolRegistry.register({ const i18n = useI18n() const diffComponent = useDiffComponent() const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) + const [expanded, setExpanded] = createSignal([]) + 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({ 0}> -
+ setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > - {(file) => ( -
-
- - - - {i18n.t("ui.patch.action.deleted")} - - - - - {i18n.t("ui.patch.action.created")} - - - - - {i18n.t("ui.patch.action.moved")} - - - - - {i18n.t("ui.patch.action.patched")} - - - - {file.relativePath} - - - - - -{file.deletions} - -
- -
- -
-
-
- )} + {(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 ( + + + +
+ {file.relativePath} +
+ + + + {i18n.t("ui.patch.action.deleted")} + + + + + {i18n.t("ui.patch.action.created")} + + + + + {i18n.t("ui.patch.action.moved")} + + + + + + + + -{file.deletions} + + + + +
+
+
+
+ + +
+ +
+
+
+
+ ) + }}
-
+
) 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([]) + + createEffect( + on( + open, + (value, prev) => { + if (!value && prev) setExpanded([]) + }, + { defer: true }, + ), + ) const assistantMessages = createMemo( () => { @@ -280,7 +293,7 @@ export function SessionTurn( />
- 0}> + 0 && !working()}>
@@ -302,30 +315,76 @@ export function SessionTurn(
- - {(diff) => ( -
-
- - - {getDirectory(diff.file)} - - {getFilename(diff.file)} - - - - -
-
- -
-
- )} -
+ setExpanded(Array.isArray(value) ? value : value ? [value] : [])} + > + + {(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 ( + + + +
+ + + + {getDirectory(diff.file)} + + + + {getFilename(diff.file)} + + +
+ + + + + + +
+
+
+
+ + +
+ +
+
+
+
+ ) + }} +
+