chore: cleanup

This commit is contained in:
Adam
2026-02-18 06:31:26 -06:00
parent 6cd3a59022
commit 3394402aef
5 changed files with 275 additions and 111 deletions

View File

@@ -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 { Collapsible } from "./collapsible"
import type { IconProps } from "./icon" import type { IconProps } from "./icon"
import { TextShimmer } from "./text-shimmer" import { TextShimmer } from "./text-shimmer"
@@ -27,18 +27,52 @@ export interface BasicToolProps {
hideDetails?: boolean hideDetails?: boolean
defaultOpen?: boolean defaultOpen?: boolean
forceOpen?: boolean forceOpen?: boolean
defer?: boolean
locked?: boolean locked?: boolean
onSubtitleClick?: () => void onSubtitleClick?: () => void
} }
export function BasicTool(props: BasicToolProps) { export function BasicTool(props: BasicToolProps) {
const [open, setOpen] = createSignal(props.defaultOpen ?? false) const [open, setOpen] = createSignal(props.defaultOpen ?? false)
const [ready, setReady] = createSignal(open())
const pending = () => props.status === "pending" || props.status === "running" 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(() => { createEffect(() => {
if (props.forceOpen) setOpen(true) 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) => { const handleOpenChange = (value: boolean) => {
if (pending()) return if (pending()) return
if (props.locked && !value) return if (props.locked && !value) return
@@ -114,7 +148,9 @@ export function BasicTool(props: BasicToolProps) {
</div> </div>
</Collapsible.Trigger> </Collapsible.Trigger>
<Show when={props.children && !props.hideDetails}> <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> </Show>
</Collapsible> </Collapsible>
) )

View File

@@ -326,8 +326,7 @@
} }
[data-slot="collapsible-content"]:has([data-component="edit-content"]), [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="write-content"]) {
[data-slot="collapsible-content"]:has([data-component="apply-patch-files"]) {
border: 1px solid var(--border-weak-base); border: 1px solid var(--border-weak-base);
border-radius: 6px; border-radius: 6px;
background: transparent; background: transparent;
@@ -1219,21 +1218,31 @@
} }
} }
[data-component="apply-patch-files"] { [data-component="accordion"][data-scope="apply-patch"] {
display: flex; [data-slot="apply-patch-trigger-content"] {
flex-direction: column;
}
[data-component="apply-patch-file"] {
display: flex;
flex-direction: column;
[data-slot="apply-patch-file-header"] {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: space-between;
padding: 8px 12px; width: 100%;
background-color: transparent; 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"] { [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"] { [data-slot="apply-patch-deletion-count"] {
font-family: var(--font-family-mono); font-family: var(--font-family-mono);
font-size: var(--font-size-small); font-size: var(--font-size-small);
color: var(--text-critical-base); color: var(--text-critical-base);
flex-shrink: 0; flex-shrink: 0;
} }
}
[data-component="apply-patch-file"] + [data-component="apply-patch-file"] { [data-slot="apply-patch-file-chevron"] {
border-top: 1px solid var(--border-weaker-base); 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"] { [data-component="apply-patch-file-diff"] {

View File

@@ -35,6 +35,7 @@ import { useDialog } from "../context/dialog"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
import { BasicTool } from "./basic-tool" import { BasicTool } from "./basic-tool"
import { GenericTool } from "./basic-tool" import { GenericTool } from "./basic-tool"
import { Accordion } from "./accordion"
import { Button } from "./button" import { Button } from "./button"
import { Card } from "./card" import { Card } from "./card"
import { Collapsible } from "./collapsible" import { Collapsible } from "./collapsible"
@@ -1482,6 +1483,7 @@ ToolRegistry.register({
<BasicTool <BasicTool
{...props} {...props}
icon="code-lines" icon="code-lines"
defer
trigger={ trigger={
<div data-component="edit-trigger"> <div data-component="edit-trigger">
<div data-slot="message-part-title-area"> <div data-slot="message-part-title-area">
@@ -1542,6 +1544,7 @@ ToolRegistry.register({
<BasicTool <BasicTool
{...props} {...props}
icon="code-lines" icon="code-lines"
defer
trigger={ trigger={
<div data-component="write-trigger"> <div data-component="write-trigger">
<div data-slot="message-part-title-area"> <div data-slot="message-part-title-area">
@@ -1602,6 +1605,16 @@ ToolRegistry.register({
const i18n = useI18n() const i18n = useI18n()
const diffComponent = useDiffComponent() const diffComponent = useDiffComponent()
const files = createMemo(() => (props.metadata.files ?? []) as ApplyPatchFile[]) 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 subtitle = createMemo(() => {
const count = files().length const count = files().length
@@ -1613,60 +1626,89 @@ ToolRegistry.register({
<BasicTool <BasicTool
{...props} {...props}
icon="code-lines" icon="code-lines"
defer
trigger={{ trigger={{
title: i18n.t("ui.tool.patch"), title: i18n.t("ui.tool.patch"),
subtitle: subtitle(), subtitle: subtitle(),
}} }}
> >
<Show when={files().length > 0}> <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()}> <For each={files()}>
{(file) => ( {(file) => {
<div data-component="apply-patch-file"> const active = createMemo(() => expanded().includes(file.filePath))
<div data-slot="apply-patch-file-header"> const [visible, setVisible] = createSignal(false)
<Switch>
<Match when={file.type === "delete"}> createEffect(() => {
<span data-slot="apply-patch-file-action" data-type="delete"> if (!active()) {
{i18n.t("ui.patch.action.deleted")} setVisible(false)
</span> return
</Match> }
<Match when={file.type === "add"}>
<span data-slot="apply-patch-file-action" data-type="add"> requestAnimationFrame(() => {
{i18n.t("ui.patch.action.created")} if (!active()) return
</span> setVisible(true)
</Match> })
<Match when={file.type === "move"}> })
<span data-slot="apply-patch-file-action" data-type="move">
{i18n.t("ui.patch.action.moved")} return (
</span> <Accordion.Item value={file.filePath} data-type={file.type}>
</Match> <Accordion.Header>
<Match when={file.type === "update"}> <Accordion.Trigger>
<span data-slot="apply-patch-file-action" data-type="update"> <div data-slot="apply-patch-trigger-content">
{i18n.t("ui.patch.action.patched")} <span data-slot="apply-patch-file-path">{file.relativePath}</span>
</span> <div data-slot="apply-patch-trigger-actions">
</Match> <Switch>
</Switch> <Match when={file.type === "delete"}>
<span data-slot="apply-patch-file-path">{file.relativePath}</span> <span data-slot="apply-patch-file-action" data-type="delete">
<Show when={file.type !== "delete"}> {i18n.t("ui.patch.action.deleted")}
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} /> </span>
</Show> </Match>
<Show when={file.type === "delete"}> <Match when={file.type === "add"}>
<span data-slot="apply-patch-deletion-count">-{file.deletions}</span> <span data-slot="apply-patch-file-action" data-type="add">
</Show> {i18n.t("ui.patch.action.created")}
</div> </span>
<Show when={file.type !== "delete"}> </Match>
<div data-component="apply-patch-file-diff"> <Match when={file.type === "move"}>
<Dynamic <span data-slot="apply-patch-file-action" data-type="move">
component={diffComponent} {i18n.t("ui.patch.action.moved")}
before={{ name: file.filePath, contents: file.before }} </span>
after={{ name: file.filePath, contents: file.after }} </Match>
/> </Switch>
</div> <Show when={file.type !== "delete"}>
</Show> <DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
</div> </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> </For>
</div> </Accordion>
</Show> </Show>
</BasicTool> </BasicTool>
) )

View File

@@ -130,19 +130,13 @@
gap: 12px; gap: 12px;
} }
[data-component="session-turn-diff"] { [data-slot="session-turn-diff-trigger"] {
border: 1px solid var(--border-weaker-base);
border-radius: var(--radius-md);
overflow: clip;
}
[data-slot="session-turn-diff-header"] {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
padding: 6px 10px; width: 100%;
border-bottom: 1px solid var(--border-weaker-base); min-width: 0;
} }
[data-slot="session-turn-diff-path"] { [data-slot="session-turn-diff-path"] {
@@ -166,9 +160,36 @@
font-weight: var(--font-weight-medium); 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"] { [data-slot="session-turn-diff-view"] {
background-color: var(--surface-inset-base); background-color: var(--surface-inset-base);
width: 100%; width: 100%;
min-width: 0; 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;
} }
} }

View File

@@ -4,12 +4,14 @@ import { useDiffComponent } from "../context/diff"
import { Binary } from "@opencode-ai/util/binary" import { Binary } from "@opencode-ai/util/binary"
import { getDirectory, getFilename } from "@opencode-ai/util/path" 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 { Dynamic } from "solid-js/web"
import { AssistantParts, Message } from "./message-part" import { AssistantParts, Message } from "./message-part"
import { Card } from "./card" import { Card } from "./card"
import { Accordion } from "./accordion"
import { Collapsible } from "./collapsible" import { Collapsible } from "./collapsible"
import { DiffChanges } from "./diff-changes" import { DiffChanges } from "./diff-changes"
import { Icon } from "./icon"
import { TextShimmer } from "./text-shimmer" import { TextShimmer } from "./text-shimmer"
import { createAutoScroll } from "../hooks" import { createAutoScroll } from "../hooks"
import { useI18n } from "../context/i18n" import { useI18n } from "../context/i18n"
@@ -175,6 +177,17 @@ export function SessionTurn(
}) })
const edited = createMemo(() => diffs().length) const edited = createMemo(() => diffs().length)
const [open, setOpen] = createSignal(false) const [open, setOpen] = createSignal(false)
const [expanded, setExpanded] = createSignal<string[]>([])
createEffect(
on(
open,
(value, prev) => {
if (!value && prev) setExpanded([])
},
{ defer: true },
),
)
const assistantMessages = createMemo( const assistantMessages = createMemo(
() => { () => {
@@ -280,7 +293,7 @@ export function SessionTurn(
/> />
</div> </div>
</Show> </Show>
<Show when={edited() > 0}> <Show when={edited() > 0 && !working()}>
<div data-slot="session-turn-diffs"> <div data-slot="session-turn-diffs">
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost"> <Collapsible open={open()} onOpenChange={setOpen} variant="ghost">
<Collapsible.Trigger> <Collapsible.Trigger>
@@ -302,30 +315,76 @@ export function SessionTurn(
<Collapsible.Content> <Collapsible.Content>
<Show when={open()}> <Show when={open()}>
<div data-component="session-turn-diffs-content"> <div data-component="session-turn-diffs-content">
<For each={diffs()}> <Accordion
{(diff) => ( multiple
<div data-component="session-turn-diff"> value={expanded()}
<div data-slot="session-turn-diff-header"> onChange={(value) => setExpanded(Array.isArray(value) ? value : value ? [value] : [])}
<span data-slot="session-turn-diff-path"> >
<Show when={diff.file.includes("/")}> <For each={diffs()}>
<span data-slot="session-turn-diff-directory">{getDirectory(diff.file)}</span> {(diff) => {
</Show> const active = createMemo(() => expanded().includes(diff.file))
<span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span> const [visible, setVisible] = createSignal(false)
</span>
<span data-slot="session-turn-diff-changes"> createEffect(
<DiffChanges changes={diff} /> on(
</span> active,
</div> (value) => {
<div data-slot="session-turn-diff-view"> if (!value) {
<Dynamic setVisible(false)
component={diffComponent} return
before={{ name: diff.file, contents: diff.before }} }
after={{ name: diff.file, contents: diff.after }}
/> requestAnimationFrame(() => {
</div> if (!active()) return
</div> setVisible(true)
)} })
</For> },
{ 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> </div>
</Show> </Show>
</Collapsible.Content> </Collapsible.Content>