mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-24 06:45:22 +00:00
Merge branch 'dev' into brendan/refactor-server-management-backend
This commit is contained in:
@@ -550,8 +550,15 @@ export default function FileTree(props: {
|
||||
</Match>
|
||||
<Match when={!node.ignored}>
|
||||
<span class="filetree-iconpair size-4">
|
||||
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--color" />
|
||||
<FileIcon node={node} class="size-4 filetree-icon filetree-icon--mono" mono />
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--color opacity-0 group-hover/filetree:opacity-100"
|
||||
/>
|
||||
<FileIcon
|
||||
node={node}
|
||||
class="size-4 filetree-icon filetree-icon--mono group-hover/filetree:opacity-0"
|
||||
mono
|
||||
/>
|
||||
</span>
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
@@ -1375,8 +1375,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||
name={mode === "shell" ? "console" : "prompt"}
|
||||
class="size-[18px]"
|
||||
classList={{
|
||||
"text-icon-strong-base": mode === "shell" && store.mode === "shell",
|
||||
"text-icon-interactive-base": mode === "normal" && store.mode === "normal",
|
||||
"text-icon-strong-base": store.mode === mode,
|
||||
"text-icon-weak": store.mode !== mode,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -23,15 +23,3 @@
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
[data-component="filetree"] .filetree-iconpair .filetree-icon--color {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
[data-component="filetree"]:hover .filetree-iconpair .filetree-icon--color {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-component="filetree"]:hover .filetree-iconpair .filetree-icon--mono {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -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,64 +1218,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
[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"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: transparent;
|
||||
[data-component="accordion"][data-scope="apply-patch"] {
|
||||
[data-slot="accordion-trigger"] {
|
||||
background-color: var(--background-stronger) !important;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-file-action"] {
|
||||
[data-slot="apply-patch-trigger-content"] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-file-info"] {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-file-name-container"] {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-directory"] {
|
||||
color: var(--text-base);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-filename"] {
|
||||
color: var(--text-strong);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-trigger-actions"] {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
[data-slot="apply-patch-change"] {
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: var(--line-height-large);
|
||||
color: var(--text-base);
|
||||
flex-shrink: 0;
|
||||
|
||||
&[data-type="delete"] {
|
||||
color: var(--text-critical-base);
|
||||
}
|
||||
|
||||
&[data-type="add"] {
|
||||
color: var(--text-success-base);
|
||||
}
|
||||
|
||||
&[data-type="move"] {
|
||||
color: var(--text-warning-base);
|
||||
}
|
||||
}
|
||||
|
||||
[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-change"][data-type="added"] {
|
||||
color: var(--icon-diff-add-base);
|
||||
}
|
||||
|
||||
[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-slot="apply-patch-change"][data-type="removed"] {
|
||||
color: var(--icon-diff-delete-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="apply-patch-file"] + [data-component="apply-patch-file"] {
|
||||
border-top: 1px solid var(--border-weaker-base);
|
||||
[data-slot="apply-patch-change"][data-type="modified"] {
|
||||
color: var(--icon-diff-modified-base);
|
||||
}
|
||||
}
|
||||
|
||||
[data-component="apply-patch-file-diff"] {
|
||||
|
||||
@@ -35,9 +35,11 @@ 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"
|
||||
import { FileIcon } from "./file-icon"
|
||||
import { Icon } from "./icon"
|
||||
import { Checkbox } from "./checkbox"
|
||||
import { DiffChanges } from "./diff-changes"
|
||||
@@ -670,13 +672,6 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
||||
|
||||
const agents = createMemo(() => (props.parts?.filter((p) => p.type === "agent") as AgentPart[]) ?? [])
|
||||
|
||||
const provider = createMemo(() => {
|
||||
const id = props.message.model?.providerID
|
||||
if (!id) return ""
|
||||
const match = data.store.provider?.all?.find((p) => p.id === id)
|
||||
return match?.name ?? id
|
||||
})
|
||||
|
||||
const model = createMemo(() => {
|
||||
const providerID = props.message.model?.providerID
|
||||
const modelID = props.message.model?.modelID
|
||||
@@ -697,7 +692,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
||||
|
||||
const metaHead = createMemo(() => {
|
||||
const agent = props.message.agent
|
||||
const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", provider(), model()]
|
||||
const items = [agent ? agent[0]?.toUpperCase() + agent.slice(1) : "", model()]
|
||||
return items.filter((x) => !!x).join("\u00A0\u00B7\u00A0")
|
||||
})
|
||||
|
||||
@@ -1053,13 +1048,6 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
props.message.role === "assistant" && (props.message as AssistantMessage).error?.name === "MessageAbortedError",
|
||||
)
|
||||
|
||||
const provider = createMemo(() => {
|
||||
if (props.message.role !== "assistant") return ""
|
||||
const id = (props.message as AssistantMessage).providerID
|
||||
const match = data.store.provider?.all?.find((p) => p.id === id)
|
||||
return match?.name ?? id
|
||||
})
|
||||
|
||||
const model = createMemo(() => {
|
||||
if (props.message.role !== "assistant") return ""
|
||||
const message = props.message as AssistantMessage
|
||||
@@ -1074,9 +1062,10 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
if (typeof completed !== "number") return ""
|
||||
const ms = completed - message.time.created
|
||||
if (!(ms >= 0)) return ""
|
||||
if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`
|
||||
const minutes = Math.floor(ms / 60_000)
|
||||
const seconds = Math.round((ms - minutes * 60_000) / 1000)
|
||||
const total = Math.round(ms / 1000)
|
||||
if (total < 60) return `${total}s`
|
||||
const minutes = Math.floor(total / 60)
|
||||
const seconds = total % 60
|
||||
return `${minutes}m ${seconds}s`
|
||||
})
|
||||
|
||||
@@ -1085,7 +1074,6 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
||||
const agent = (props.message as AssistantMessage).agent
|
||||
const items = [
|
||||
agent ? agent[0]?.toUpperCase() + agent.slice(1) : "",
|
||||
provider(),
|
||||
model(),
|
||||
duration(),
|
||||
interrupted() ? i18n.t("ui.message.interrupted") : "",
|
||||
@@ -1482,6 +1470,7 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="code-lines"
|
||||
defer
|
||||
trigger={
|
||||
<div data-component="edit-trigger">
|
||||
<div data-slot="message-part-title-area">
|
||||
@@ -1542,6 +1531,7 @@ ToolRegistry.register({
|
||||
<BasicTool
|
||||
{...props}
|
||||
icon="code-lines"
|
||||
defer
|
||||
trigger={
|
||||
<div data-component="write-trigger">
|
||||
<div data-slot="message-part-title-area">
|
||||
@@ -1602,6 +1592,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 +1613,92 @@ 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">
|
||||
<div data-slot="apply-patch-file-info">
|
||||
<FileIcon node={{ path: file.relativePath, type: "file" }} />
|
||||
<div data-slot="apply-patch-file-name-container">
|
||||
<Show when={file.relativePath.includes("/")}>
|
||||
<span data-slot="apply-patch-directory">{`\u202A${getDirectory(file.relativePath)}\u202C`}</span>
|
||||
</Show>
|
||||
<span data-slot="apply-patch-filename">{getFilename(file.relativePath)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div data-slot="apply-patch-trigger-actions">
|
||||
<Switch>
|
||||
<Match when={file.type === "add"}>
|
||||
<span data-slot="apply-patch-change" data-type="added">
|
||||
{i18n.t("ui.patch.action.created")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={file.type === "delete"}>
|
||||
<span data-slot="apply-patch-change" data-type="removed">
|
||||
{i18n.t("ui.patch.action.deleted")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={file.type === "move"}>
|
||||
<span data-slot="apply-patch-change" data-type="modified">
|
||||
{i18n.t("ui.patch.action.moved")}
|
||||
</span>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<DiffChanges changes={{ additions: file.additions, deletions: file.deletions }} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</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