chore: cleanup
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user