diff options
| author | Adam <[email protected]> | 2025-12-19 13:07:46 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-19 13:07:53 -0600 |
| commit | 1689281c357e8e0785825dfe838a1bdafccb00c1 (patch) | |
| tree | 1ff21778494c0ebeeaa30f7a9b33c3bdb68c1505 | |
| parent | cdbb59fae845d0be529e5c577afd43b721be1739 (diff) | |
| download | opencode-1689281c357e8e0785825dfe838a1bdafccb00c1.tar.gz opencode-1689281c357e8e0785825dfe838a1bdafccb00c1.zip | |
fix(desktop): auto-scroll and session perf
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 7 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 707 |
2 files changed, 437 insertions, 277 deletions
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index ef85dd9ce..2fa9cf474 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -502,6 +502,7 @@ ToolRegistry.register({ render(props) { const diffComponent = useDiffComponent() const diagnostics = createMemo(() => getDiagnostics(props.metadata.diagnostics, props.input.filePath)) + console.log(props) return ( <BasicTool {...props} @@ -531,12 +532,14 @@ ToolRegistry.register({ <Dynamic component={diffComponent} before={{ - name: getFilename(props.metadata.filediff.path), + name: props.metadata.filediff.path, contents: props.metadata.filediff.before, + cacheKey: checksum(props.metadata.filediff.before), }} after={{ - name: getFilename(props.metadata.filediff.path), + name: props.metadata.filediff.path, contents: props.metadata.filediff.after, + cacheKey: checksum(props.metadata.filediff.after), }} /> </div> diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index 12725b983..f101a3aa4 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -1,4 +1,4 @@ -import { AssistantMessage, ToolPart } from "@opencode-ai/sdk/v2/client" +import { AssistantMessage, Part as PartType, TextPart, ToolPart } from "@opencode-ai/sdk/v2/client" import { useData } from "../context" import { useDiffComponent } from "../context/diff" import { getDirectory, getFilename } from "@opencode-ai/util/path" @@ -20,6 +20,45 @@ import { Spinner } from "./spinner" import { createStore } from "solid-js/store" import { DateTime, DurationUnit, Interval } from "luxon" +function computeStatusFromPart(part: PartType | undefined): string | undefined { + if (!part) return undefined + + if (part.type === "tool") { + switch (part.tool) { + case "task": + return "Delegating work" + case "todowrite": + case "todoread": + return "Planning next steps" + case "read": + return "Gathering context" + case "list": + case "grep": + case "glob": + return "Searching the codebase" + case "webfetch": + return "Searching the web" + case "edit": + case "write": + return "Making edits" + case "bash": + return "Running commands" + default: + return undefined + } + } + if (part.type === "reasoning") { + const text = part.text ?? "" + const match = text.trimStart().match(/^\*\*(.+?)\*\*/) + if (match) return `Thinking · ${match[1].trim()}` + return "Thinking" + } + if (part.type === "text") { + return "Gathering thoughts" + } + return undefined +} + export function SessionTurn( props: ParentProps<{ sessionID: string @@ -36,119 +75,152 @@ export function SessionTurn( ) { const data = useData() const diffComponent = useDiffComponent() - const messages = createMemo(() => data.store.message[props.sessionID] ?? []) - const userMessages = createMemo(() => - messages() - .filter((m) => m.role === "user") - .sort((a, b) => a.id.localeCompare(b.id)), - ) - const lastUserMessage = createMemo(() => userMessages().at(-1)!) - const message = createMemo(() => userMessages().find((m) => m.id === props.messageID)!) + + const derived = createMemo(() => { + const allMessages = data.store.message[props.sessionID] ?? [] + const userMessages = allMessages.filter((m) => m.role === "user").sort((a, b) => a.id.localeCompare(b.id)) + const lastUserMessage = userMessages.at(-1) + const message = userMessages.find((m) => m.id === props.messageID) + + if (!message) { + return { + message: undefined, + parts: [] as PartType[], + assistantMessages: [] as AssistantMessage[], + assistantParts: [] as PartType[], + lastAssistantMessage: undefined as AssistantMessage | undefined, + lastTextPart: undefined as PartType | undefined, + error: undefined, + hasSteps: false, + isShellMode: false, + rawStatus: undefined as string | undefined, + isLastUserMessage: false, + } + } + + const parts = data.store.part[message.id] ?? [] + const assistantMessages = allMessages.filter( + (m) => m.role === "assistant" && m.parentID === message.id, + ) as AssistantMessage[] + + const assistantParts: PartType[] = [] + for (const m of assistantMessages) { + const msgParts = data.store.part[m.id] + if (msgParts) { + for (const p of msgParts) { + if (p) assistantParts.push(p) + } + } + } + + const lastAssistantMessage = assistantMessages.at(-1) + const error = assistantMessages.find((m) => m.error)?.error + + let lastTextPart: PartType | undefined + for (let i = assistantParts.length - 1; i >= 0; i--) { + if (assistantParts[i]?.type === "text") { + lastTextPart = assistantParts[i] + break + } + } + + const hasSteps = assistantParts.some((p) => p?.type === "tool") + + let isShellMode = false + if (parts.every((p) => p?.type === "text" && p?.synthetic) && assistantParts.length === 1) { + const assistantPart = assistantParts[0] + if (assistantPart?.type === "tool" && assistantPart?.tool === "bash") { + isShellMode = true + } + } + + let resolvedParts = assistantParts + const currentTask = assistantParts.findLast( + (p) => + p && + p.type === "tool" && + p.tool === "task" && + p.state && + "metadata" in p.state && + p.state.metadata && + p.state.metadata.sessionId && + p.state.status === "running", + ) as ToolPart | undefined + + if (currentTask?.state && "metadata" in currentTask.state && currentTask.state.metadata?.sessionId) { + const taskMessages = data.store.message[currentTask.state.metadata.sessionId as string]?.filter( + (m) => m.role === "assistant", + ) + if (taskMessages) { + const taskParts: PartType[] = [] + for (const m of taskMessages) { + const msgParts = data.store.part[m.id] + if (msgParts) { + for (const p of msgParts) { + if (p) taskParts.push(p) + } + } + } + if (taskParts.length > 0) { + resolvedParts = taskParts + } + } + } + + const lastPart = resolvedParts.at(-1) + const rawStatus = computeStatusFromPart(lastPart) + + return { + message, + parts, + assistantMessages, + assistantParts, + lastAssistantMessage, + lastTextPart, + error, + hasSteps, + isShellMode, + rawStatus, + isLastUserMessage: message.id === lastUserMessage?.id, + } + }) + + const message = () => derived().message + const parts = () => derived().parts + const assistantMessages = () => derived().assistantMessages + const assistantParts = () => derived().assistantParts + const lastAssistantMessage = () => derived().lastAssistantMessage + const lastTextPart = () => derived().lastTextPart + const error = () => derived().error + const hasSteps = () => derived().hasSteps + const isShellMode = () => derived().isShellMode + const rawStatus = () => derived().rawStatus + const status = createMemo( () => data.store.session_status[props.sessionID] ?? { type: "idle", }, ) - const working = createMemo(() => status().type !== "idle" && message().id === lastUserMessage().id) + const working = createMemo(() => status().type !== "idle" && derived().isLastUserMessage) const retry = createMemo(() => { const s = status() if (s.type !== "retry") return return s }) - const assistantMessages = createMemo(() => { - return messages().filter((m) => m.role === "assistant" && m.parentID == message().id) as AssistantMessage[] - }) - const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]) ?? []) - const lastAssistantMessage = createMemo(() => assistantMessages().at(-1)) - const error = createMemo(() => assistantMessages().find((m) => m.error)?.error) - const parts = createMemo(() => data.store.part[message().id] ?? []) - const lastTextPart = createMemo(() => - assistantParts() - .filter((p) => p?.type === "text") - .at(-1), - ) - const summary = createMemo(() => message().summary?.body) - const response = createMemo(() => lastTextPart()?.text) - const hasSteps = createMemo(() => assistantParts().some((p) => p?.type === "tool")) - - const currentTask = createMemo( - () => - assistantParts().findLast( - (p) => - p && - p.type === "tool" && - p.tool === "task" && - p.state && - "metadata" in p.state && - p.state.metadata && - p.state.metadata.sessionId && - p.state.status === "running", - ) as ToolPart, - ) - const resolvedParts = createMemo(() => { - let resolved = assistantParts() - const task = currentTask() - if (task && task.state && "metadata" in task.state && task.state.metadata?.sessionId) { - const messages = data.store.message[task.state.metadata.sessionId as string]?.filter( - (m) => m.role === "assistant", - ) - resolved = messages?.flatMap((m) => data.store.part[m.id]) ?? assistantParts() - } - return resolved - }) - const lastPart = createMemo(() => resolvedParts().slice(-1)?.at(0)) - const rawStatus = createMemo(() => { - const last = lastPart() - if (!last) return undefined - - if (last.type === "tool") { - switch (last.tool) { - case "task": - return "Delegating work" - case "todowrite": - case "todoread": - return "Planning next steps" - case "read": - return "Gathering context" - case "list": - case "grep": - case "glob": - return "Searching the codebase" - case "webfetch": - return "Searching the web" - case "edit": - case "write": - return "Making edits" - case "bash": - return "Running commands" - default: - break - } - } else if (last.type === "reasoning") { - const text = last.text ?? "" - const match = text.trimStart().match(/^\*\*(.+?)\*\*/) - if (match) return `Thinking · ${match[1].trim()}` - return "Thinking" - } else if (last.type === "text") { - return "Gathering thoughts" - } - return undefined - }) - const hasDiffs = createMemo(() => message().summary?.diffs?.length) - const isShellMode = createMemo(() => { - if (parts().some((p) => p?.type !== "text" || !p?.synthetic)) return false - if (assistantParts().length !== 1) return false - const assistantPart = assistantParts()[0] - if (assistantPart?.type !== "tool") return false - if (assistantPart?.tool !== "bash") return false - return true - }) + const summary = () => message()?.summary?.body + const response = () => { + const part = lastTextPart() + return part?.type === "text" ? (part as TextPart).text : undefined + } + const hasDiffs = () => message()?.summary?.diffs?.length function duration() { + const msg = message() + if (!msg) return "" const completed = lastAssistantMessage()?.time.completed - const from = DateTime.fromMillis(message().time.created) + const from = DateTime.fromMillis(msg.time.created) const to = completed ? DateTime.fromMillis(completed) : DateTime.now() const interval = Interval.fromDateTimes(from, to) const unit: DurationUnit[] = interval.length("seconds") > 60 ? ["minutes", "seconds"] : ["seconds"] @@ -167,8 +239,11 @@ export function SessionTurn( stickyTitleRef: undefined as HTMLDivElement | undefined, stickyTriggerRef: undefined as HTMLDivElement | undefined, lastScrollTop: 0, + lastScrollHeight: 0, + lastContainerWidth: 0, autoScrolled: false, userScrolled: false, + reflowing: false, stickyHeaderHeight: 0, retrySeconds: 0, status: rawStatus(), @@ -192,22 +267,61 @@ export function SessionTurn( function handleScroll() { if (!scrollRef || store.autoScrolled) return + const scrollTop = scrollRef.scrollTop - console.log("scrollTop", scrollTop) - console.log("clientHeight", store.contentRef?.clientHeight) + const scrollHeight = scrollRef.scrollHeight + + // If we're in a reflow state (width just changed), don't interpret as user scroll + if (store.reflowing) { + batch(() => { + setStore("lastScrollTop", scrollTop) + setStore("lastScrollHeight", scrollHeight) + }) + return + } + + // Check if this looks like a reflow-induced scroll adjustment + // When width changes, scrollHeight changes and browser adjusts scrollTop proportionally + const scrollHeightChanged = Math.abs(scrollHeight - store.lastScrollHeight) > 10 + const scrollTopDelta = scrollTop - store.lastScrollTop + + // If scrollHeight decreased (content got shorter due to wider width), + // and scrollTop decreased proportionally, this is reflow, not user scroll + if (scrollHeightChanged && scrollTopDelta < 0) { + const heightRatio = store.lastScrollHeight > 0 ? scrollHeight / store.lastScrollHeight : 1 + const expectedScrollTop = store.lastScrollTop * heightRatio + const tolerance = 100 // Allow some tolerance for the adjustment + if (Math.abs(scrollTop - expectedScrollTop) < tolerance) { + // This is a proportional adjustment from reflow, not user scrolling + batch(() => { + setStore("lastScrollTop", scrollTop) + setStore("lastScrollHeight", scrollHeight) + }) + return + } + } + const reset = scrollTop <= 0 && store.lastScrollTop > 0 && working() && !store.userScrolled if (reset) { - setStore("lastScrollTop", scrollTop) + batch(() => { + setStore("lastScrollTop", scrollTop) + setStore("lastScrollHeight", scrollHeight) + }) requestAnimationFrame(scrollToBottom) return } - const scrolledUp = scrollTop < store.lastScrollTop - 50 + + // Only count as user scroll if scrollTop decreased without a corresponding scrollHeight change + const scrolledUp = scrollTop < store.lastScrollTop - 50 && !scrollHeightChanged if (scrolledUp && working()) { - console.log("scrolled up") setStore("userScrolled", true) props.onUserInteracted?.() } - setStore("lastScrollTop", scrollTop) + + batch(() => { + setStore("lastScrollTop", scrollTop) + setStore("lastScrollHeight", scrollHeight) + }) } function handleInteraction() { @@ -225,13 +339,39 @@ export function SessionTurn( requestAnimationFrame(() => { batch(() => { setStore("lastScrollTop", scrollRef?.scrollTop ?? 0) + setStore("lastScrollHeight", scrollRef?.scrollHeight ?? 0) setStore("autoScrolled", false) }) }) }) } - createResizeObserver(() => store.contentRef, scrollToBottom) + // Track width changes to detect reflow situations + createResizeObserver( + () => store.contentRef, + ({ width }) => { + const widthChanged = Math.abs(width - store.lastContainerWidth) > 5 + if (widthChanged && store.lastContainerWidth > 0) { + // Width changed - mark as reflowing to ignore scroll adjustments + setStore("reflowing", true) + // Clear reflow state after browser has had time to adjust + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setStore("reflowing", false) + // Restore auto-scroll if we're still working + if (working() && !store.userScrolled) { + scrollToBottom() + } + }) + }) + } else { + if (!store.reflowing) { + scrollToBottom() + } + } + setStore("lastContainerWidth", width) + }, + ) createEffect(() => { if (!working()) setStore("userScrolled", false) @@ -288,184 +428,201 @@ export function SessionTurn( <div data-component="session-turn" class={props.classes?.root}> <div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}> <div onClick={handleInteraction}> - <div - ref={(el) => setStore("contentRef", el)} - data-message={message().id} - data-slot="session-turn-message-container" - class={props.classes?.container} - style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }} - > - <Switch> - <Match when={isShellMode()}> - <Part part={assistantParts()[0]} message={message()} defaultOpen /> - </Match> - <Match when={true}> - {/* Title (sticky) */} - <div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> - <div data-slot="session-turn-message-header"> - <div data-slot="session-turn-message-title"> - <Switch> - <Match when={working()}> - <Typewriter as="h1" text={message().summary?.title} data-slot="session-turn-typewriter" /> - </Match> - <Match when={true}> - <h1>{message().summary?.title}</h1> - </Match> - </Switch> - </div> - </div> - </div> - {/* User Message */} - <div data-slot="session-turn-message-content"> - <Message message={message()} parts={parts()} /> - </div> - {/* Trigger (sticky) */} - <Show when={working() || hasSteps()}> - <div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger"> - <Button - data-expandable={assistantMessages().length > 0} - data-slot="session-turn-collapsible-trigger-content" - variant="ghost" - size="small" - onClick={props.onStepsExpandedToggle ?? (() => {})} - > - <Show when={working()}> - <Spinner /> - </Show> - <Switch> - <Match when={retry()}> - <span data-slot="session-turn-retry-message"> - {(() => { - const r = retry() - if (!r) return "" - return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message - })()} - </span> - <span data-slot="session-turn-retry-seconds"> - · retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""} - </span> - <span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span> - </Match> - <Match when={working()}>{store.status ?? "Considering next steps"}</Match> - <Match when={props.stepsExpanded}>Hide steps</Match> - <Match when={!props.stepsExpanded}>Show steps</Match> - </Switch> - <span>·</span> - <span>{store.duration}</span> - <Show when={assistantMessages().length > 0}> - <Icon name="chevron-grabber-vertical" size="small" /> - </Show> - </Button> - </div> - </Show> - {/* Response */} - <Show when={props.stepsExpanded && assistantMessages().length > 0}> - <div data-slot="session-turn-collapsible-content-inner"> - <For each={assistantMessages()}> - {(assistantMessage) => { - const parts = createMemo(() => data.store.part[assistantMessage.id] ?? []) - const last = createMemo(() => - parts() - .filter((p) => p?.type === "text") - .at(-1), - ) - return ( + <Show when={message()}> + {(msg) => ( + <div + ref={(el) => setStore("contentRef", el)} + data-message={msg().id} + data-slot="session-turn-message-container" + class={props.classes?.container} + style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }} + > + <Switch> + <Match when={isShellMode()}> + <Part part={assistantParts()[0]} message={msg()} defaultOpen /> + </Match> + <Match when={true}> + {/* Title (sticky) */} + <div ref={(el) => setStore("stickyTitleRef", el)} data-slot="session-turn-sticky-title"> + <div data-slot="session-turn-message-header"> + <div data-slot="session-turn-message-title"> <Switch> - <Match when={response() && lastTextPart()?.id === last()?.id}> - <Message message={assistantMessage} parts={parts().filter((p) => p?.id !== last()?.id)} /> + <Match when={working()}> + <Typewriter as="h1" text={msg().summary?.title} data-slot="session-turn-typewriter" /> </Match> <Match when={true}> - <Message message={assistantMessage} parts={parts()} /> + <h1>{msg().summary?.title}</h1> + </Match> + </Switch> + </div> + </div> + </div> + {/* User Message */} + <div data-slot="session-turn-message-content"> + <Message message={msg()} parts={parts()} /> + </div> + {/* Trigger (sticky) */} + <Show when={working() || hasSteps()}> + <div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger"> + <Button + data-expandable={assistantMessages().length > 0} + data-slot="session-turn-collapsible-trigger-content" + variant="ghost" + size="small" + onClick={props.onStepsExpandedToggle ?? (() => {})} + > + <Show when={working()}> + <Spinner /> + </Show> + <Switch> + <Match when={retry()}> + <span data-slot="session-turn-retry-message"> + {(() => { + const r = retry() + if (!r) return "" + return r.message.length > 60 ? r.message.slice(0, 60) + "..." : r.message + })()} + </span> + <span data-slot="session-turn-retry-seconds"> + · retrying {store.retrySeconds > 0 ? `in ${store.retrySeconds}s ` : ""} + </span> + <span data-slot="session-turn-retry-attempt">(#{retry()?.attempt})</span> </Match> + <Match when={working()}>{store.status ?? "Considering next steps"}</Match> + <Match when={props.stepsExpanded}>Hide steps</Match> + <Match when={!props.stepsExpanded}>Show steps</Match> </Switch> - ) - }} - </For> - <Show when={error()}> + <span>·</span> + <span>{store.duration}</span> + <Show when={assistantMessages().length > 0}> + <Icon name="chevron-grabber-vertical" size="small" /> + </Show> + </Button> + </div> + </Show> + {/* Response */} + <Show when={props.stepsExpanded && assistantMessages().length > 0}> + <div data-slot="session-turn-collapsible-content-inner"> + <For each={assistantMessages()}> + {(assistantMessage) => { + const parts = createMemo(() => data.store.part[assistantMessage.id] ?? []) + const last = createMemo(() => + parts() + .filter((p) => p?.type === "text") + .at(-1), + ) + return ( + <Switch> + <Match when={response() && lastTextPart()?.id === last()?.id}> + <Message + message={assistantMessage} + parts={parts().filter((p) => p?.id !== last()?.id)} + /> + </Match> + <Match when={true}> + <Message message={assistantMessage} parts={parts()} /> + </Match> + </Switch> + ) + }} + </For> + <Show when={error()}> + <Card variant="error" class="error-card"> + {error()?.data?.message as string} + </Card> + </Show> + </div> + </Show> + {/* Summary */} + <Show when={!working()}> + <div data-slot="session-turn-summary-section"> + <div data-slot="session-turn-summary-header"> + <Switch> + <Match when={summary()}> + {(summary) => ( + <> + <h2 data-slot="session-turn-summary-title">Summary</h2> + <Markdown + data-slot="session-turn-markdown" + data-diffs={hasDiffs()} + text={summary()} + /> + </> + )} + </Match> + <Match when={response()}> + {(response) => ( + <> + <h2 data-slot="session-turn-summary-title">Response</h2> + <Markdown + data-slot="session-turn-markdown" + data-diffs={hasDiffs()} + text={response()} + /> + </> + )} + </Match> + </Switch> + </div> + <Accordion data-slot="session-turn-accordion" multiple> + <For each={msg().summary?.diffs ?? []}> + {(diff) => ( + <Accordion.Item value={diff.file}> + <StickyAccordionHeader> + <Accordion.Trigger> + <div data-slot="session-turn-accordion-trigger-content"> + <div data-slot="session-turn-file-info"> + <FileIcon + node={{ path: diff.file, type: "file" }} + data-slot="session-turn-file-icon" + /> + <div data-slot="session-turn-file-path"> + <Show when={diff.file.includes("/")}> + <span data-slot="session-turn-directory"> + {getDirectory(diff.file)}‎ + </span> + </Show> + <span data-slot="session-turn-filename">{getFilename(diff.file)}</span> + </div> + </div> + <div data-slot="session-turn-accordion-actions"> + <DiffChanges changes={diff} /> + <Icon name="chevron-grabber-vertical" size="small" /> + </div> + </div> + </Accordion.Trigger> + </StickyAccordionHeader> + <Accordion.Content data-slot="session-turn-accordion-content"> + <Dynamic + component={diffComponent} + before={{ + name: diff.file!, + contents: diff.before!, + cacheKey: checksum(diff.before!), + }} + after={{ + name: diff.file!, + contents: diff.after!, + cacheKey: checksum(diff.after!), + }} + /> + </Accordion.Content> + </Accordion.Item> + )} + </For> + </Accordion> + </div> + </Show> + <Show when={error() && !props.stepsExpanded}> <Card variant="error" class="error-card"> {error()?.data?.message as string} </Card> </Show> - </div> - </Show> - {/* Summary */} - <Show when={!working()}> - <div data-slot="session-turn-summary-section"> - <div data-slot="session-turn-summary-header"> - <Switch> - <Match when={summary()}> - {(summary) => ( - <> - <h2 data-slot="session-turn-summary-title">Summary</h2> - <Markdown data-slot="session-turn-markdown" data-diffs={hasDiffs()} text={summary()} /> - </> - )} - </Match> - <Match when={response()}> - {(response) => ( - <> - <h2 data-slot="session-turn-summary-title">Response</h2> - <Markdown data-slot="session-turn-markdown" data-diffs={hasDiffs()} text={response()} /> - </> - )} - </Match> - </Switch> - </div> - <Accordion data-slot="session-turn-accordion" multiple> - <For each={message().summary?.diffs ?? []}> - {(diff) => ( - <Accordion.Item value={diff.file}> - <StickyAccordionHeader> - <Accordion.Trigger> - <div data-slot="session-turn-accordion-trigger-content"> - <div data-slot="session-turn-file-info"> - <FileIcon - node={{ path: diff.file, type: "file" }} - data-slot="session-turn-file-icon" - /> - <div data-slot="session-turn-file-path"> - <Show when={diff.file.includes("/")}> - <span data-slot="session-turn-directory">{getDirectory(diff.file)}‎</span> - </Show> - <span data-slot="session-turn-filename">{getFilename(diff.file)}</span> - </div> - </div> - <div data-slot="session-turn-accordion-actions"> - <DiffChanges changes={diff} /> - <Icon name="chevron-grabber-vertical" size="small" /> - </div> - </div> - </Accordion.Trigger> - </StickyAccordionHeader> - <Accordion.Content data-slot="session-turn-accordion-content"> - <Dynamic - component={diffComponent} - before={{ - name: diff.file!, - contents: diff.before!, - cacheKey: checksum(diff.before!), - }} - after={{ - name: diff.file!, - contents: diff.after!, - cacheKey: checksum(diff.after!), - }} - /> - </Accordion.Content> - </Accordion.Item> - )} - </For> - </Accordion> - </div> - </Show> - <Show when={error() && !props.stepsExpanded}> - <Card variant="error" class="error-card"> - {error()?.data?.message as string} - </Card> - </Show> - </Match> - </Switch> - </div> + </Match> + </Switch> + </div> + )} + </Show> {props.children} </div> </div> |
