mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
wip(desktop): session turn state consolidation
This commit is contained in:
parent
ece3bfd93d
commit
d81d63045a
1 changed files with 306 additions and 320 deletions
|
|
@ -40,6 +40,9 @@ export function SessionTurn(
|
|||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
)
|
||||
const message = createMemo(() => userMessages()?.find((m) => m.id === props.messageID))
|
||||
|
||||
if (!message()) return null
|
||||
|
||||
const status = createMemo(
|
||||
() =>
|
||||
data.store.session_status[props.sessionID] ?? {
|
||||
|
|
@ -49,379 +52,362 @@ export function SessionTurn(
|
|||
const working = createMemo(() => status()?.type !== "idle")
|
||||
|
||||
let scrollRef: HTMLDivElement | undefined
|
||||
const [state, setState] = createStore({
|
||||
|
||||
const assistantMessages = createMemo(() => {
|
||||
return messages()?.filter((m) => m.role === "assistant" && m.parentID == message()!.id) as AssistantMessage[]
|
||||
})
|
||||
const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
|
||||
const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
|
||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
||||
const parts = createMemo(() => data.store.part[message()!.id])
|
||||
const lastTextPart = createMemo(() =>
|
||||
assistantMessageParts()
|
||||
.filter((p) => p?.type === "text")
|
||||
?.at(-1),
|
||||
)
|
||||
const summary = createMemo(() => message()!.summary?.body ?? lastTextPart()?.text)
|
||||
const lastTextPartShown = createMemo(() => !message()!.summary?.body && (lastTextPart()?.text?.length ?? 0) > 0)
|
||||
|
||||
const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
|
||||
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 msgs = data.store.message[task.state.metadata.sessionId as string]?.filter((m) => m.role === "assistant")
|
||||
resolved = msgs?.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
|
||||
})
|
||||
|
||||
function duration() {
|
||||
const completed = lastAssistantMessage()?.time.completed
|
||||
const from = DateTime.fromMillis(message()!.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"]
|
||||
|
||||
return interval.toDuration(unit).normalize().toHuman({
|
||||
notation: "compact",
|
||||
unitDisplay: "narrow",
|
||||
compactDisplay: "short",
|
||||
showZeros: false,
|
||||
})
|
||||
}
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
stickyTitleRef: undefined as HTMLDivElement | undefined,
|
||||
stickyTriggerRef: undefined as HTMLDivElement | undefined,
|
||||
userScrolled: false,
|
||||
stickyHeaderHeight: 0,
|
||||
scrollY: 0,
|
||||
autoScrolling: false,
|
||||
status: rawStatus(),
|
||||
stepsExpanded: true,
|
||||
duration: duration(),
|
||||
lastStatusChange: Date.now(),
|
||||
statusTimeout: undefined as number | undefined,
|
||||
})
|
||||
|
||||
function handleScroll() {
|
||||
if (!scrollRef) return
|
||||
// prevents scroll loops
|
||||
if (working() && scrollRef.scrollTop < 100) return
|
||||
setState("scrollY", scrollRef.scrollTop)
|
||||
if (state.autoScrolling) return
|
||||
setStore("scrollY", scrollRef.scrollTop)
|
||||
if (store.autoScrolling) return
|
||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef
|
||||
const atBottom = scrollHeight - scrollTop - clientHeight < 50
|
||||
if (!atBottom && working()) {
|
||||
setState("userScrolled", true)
|
||||
setStore("userScrolled", true)
|
||||
}
|
||||
}
|
||||
|
||||
function handleInteraction() {
|
||||
if (working()) {
|
||||
setState("userScrolled", true)
|
||||
setStore("userScrolled", true)
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
if (!scrollRef || state.userScrolled || !working() || state.autoScrolling) return
|
||||
setState("autoScrolling", true)
|
||||
if (!scrollRef || store.userScrolled || !working() || store.autoScrolling) return
|
||||
setStore("autoScrolling", true)
|
||||
requestAnimationFrame(() => {
|
||||
scrollRef?.scrollTo({ top: scrollRef.scrollHeight, behavior: "instant" })
|
||||
requestAnimationFrame(() => {
|
||||
setState("autoScrolling", false)
|
||||
setStore("autoScrolling", false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (!working()) {
|
||||
setState("userScrolled", false)
|
||||
setStore("userScrolled", false)
|
||||
}
|
||||
})
|
||||
|
||||
createResizeObserver(
|
||||
() => state.stickyTitleRef,
|
||||
() => store.stickyTitleRef,
|
||||
({ height }) => {
|
||||
const triggerHeight = state.stickyTriggerRef?.offsetHeight ?? 0
|
||||
setState("stickyHeaderHeight", height + triggerHeight + 8)
|
||||
const triggerHeight = store.stickyTriggerRef?.offsetHeight ?? 0
|
||||
setStore("stickyHeaderHeight", height + triggerHeight + 8)
|
||||
},
|
||||
)
|
||||
|
||||
createResizeObserver(
|
||||
() => state.stickyTriggerRef,
|
||||
() => store.stickyTriggerRef,
|
||||
({ height }) => {
|
||||
const titleHeight = state.stickyTitleRef?.offsetHeight ?? 0
|
||||
setState("stickyHeaderHeight", titleHeight + height + 8)
|
||||
const titleHeight = store.stickyTitleRef?.offsetHeight ?? 0
|
||||
setStore("stickyHeaderHeight", titleHeight + height + 8)
|
||||
},
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
lastPart()
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setStore("duration", duration())
|
||||
}, 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const newStatus = rawStatus()
|
||||
if (newStatus === store.status || !newStatus) return
|
||||
|
||||
const timeSinceLastChange = Date.now() - store.lastStatusChange
|
||||
|
||||
if (timeSinceLastChange >= 2500) {
|
||||
setStore("status", newStatus)
|
||||
setStore("lastStatusChange", Date.now())
|
||||
if (store.statusTimeout) {
|
||||
clearTimeout(store.statusTimeout)
|
||||
setStore("statusTimeout", undefined)
|
||||
}
|
||||
} else {
|
||||
if (store.statusTimeout) clearTimeout(store.statusTimeout)
|
||||
setStore(
|
||||
"statusTimeout",
|
||||
setTimeout(() => {
|
||||
setStore("status", rawStatus())
|
||||
setStore("lastStatusChange", Date.now())
|
||||
setStore("statusTimeout", undefined)
|
||||
}, 2500 - timeSinceLastChange) as unknown as number,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect((prev) => {
|
||||
const isWorking = working()
|
||||
if (prev && !isWorking && !store.userScrolled) {
|
||||
setStore("stepsExpanded", false)
|
||||
}
|
||||
return isWorking
|
||||
}, working())
|
||||
|
||||
return (
|
||||
<div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${state.scrollY}px` }}>
|
||||
<div data-component="session-turn" class={props.classes?.root} style={{ "--scroll-y": `${store.scrollY}px` }}>
|
||||
<div ref={scrollRef} onScroll={handleScroll} data-slot="session-turn-content" class={props.classes?.content}>
|
||||
<div onClick={handleInteraction}>
|
||||
<Show when={message()}>
|
||||
{(message) => {
|
||||
const assistantMessages = createMemo(() => {
|
||||
return messages()?.filter(
|
||||
(m) => m.role === "assistant" && m.parentID == message().id,
|
||||
) as AssistantMessage[]
|
||||
})
|
||||
const lastAssistantMessage = createMemo(() => assistantMessages()?.at(-1))
|
||||
const assistantMessageParts = createMemo(() => assistantMessages()?.flatMap((m) => data.store.part[m.id]))
|
||||
const error = createMemo(() => assistantMessages().find((m) => m?.error)?.error)
|
||||
const parts = createMemo(() => data.store.part[message().id])
|
||||
const lastTextPart = createMemo(() =>
|
||||
assistantMessageParts()
|
||||
.filter((p) => p?.type === "text")
|
||||
?.at(-1),
|
||||
)
|
||||
const summary = createMemo(() => message().summary?.body ?? lastTextPart()?.text)
|
||||
const lastTextPartShown = createMemo(
|
||||
() => !message().summary?.body && (lastTextPart()?.text?.length ?? 0) > 0,
|
||||
)
|
||||
|
||||
const assistantParts = createMemo(() => assistantMessages().flatMap((m) => data.store.part[m.id]))
|
||||
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
|
||||
})
|
||||
|
||||
function duration() {
|
||||
const completed = lastAssistantMessage()?.time.completed
|
||||
const from = DateTime.fromMillis(message()!.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"]
|
||||
|
||||
return interval.toDuration(unit).normalize().toHuman({
|
||||
notation: "compact",
|
||||
unitDisplay: "narrow",
|
||||
compactDisplay: "short",
|
||||
showZeros: false,
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
lastPart()
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
const [store, setStore] = createStore({
|
||||
status: rawStatus(),
|
||||
stepsExpanded: true,
|
||||
duration: duration(),
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setStore("duration", duration())
|
||||
}, 1000)
|
||||
onCleanup(() => clearInterval(timer))
|
||||
})
|
||||
|
||||
let lastStatusChange = Date.now()
|
||||
let statusTimeout: number | undefined
|
||||
createEffect(() => {
|
||||
const newStatus = rawStatus()
|
||||
if (newStatus === store.status || !newStatus) return
|
||||
|
||||
const timeSinceLastChange = Date.now() - lastStatusChange
|
||||
|
||||
if (timeSinceLastChange >= 2500) {
|
||||
setStore("status", newStatus)
|
||||
lastStatusChange = Date.now()
|
||||
if (statusTimeout) {
|
||||
clearTimeout(statusTimeout)
|
||||
statusTimeout = undefined
|
||||
}
|
||||
} else {
|
||||
if (statusTimeout) clearTimeout(statusTimeout)
|
||||
statusTimeout = setTimeout(() => {
|
||||
setStore("status", rawStatus())
|
||||
lastStatusChange = Date.now()
|
||||
statusTimeout = undefined
|
||||
}, 2500 - timeSinceLastChange) as unknown as number
|
||||
}
|
||||
})
|
||||
|
||||
createEffect((prev) => {
|
||||
const isWorking = working()
|
||||
if (prev && !isWorking && !state.userScrolled) {
|
||||
setStore("stepsExpanded", false)
|
||||
}
|
||||
return isWorking
|
||||
}, working())
|
||||
|
||||
return (
|
||||
<div
|
||||
data-message={message().id}
|
||||
data-slot="session-turn-message-container"
|
||||
class={props.classes?.container}
|
||||
style={{ "--sticky-header-height": `${state.stickyHeaderHeight}px` }}
|
||||
>
|
||||
{/* Title (sticky) */}
|
||||
<div ref={(el) => setState("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) */}
|
||||
<div ref={(el) => setState("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||
>
|
||||
<Show when={working()}>
|
||||
<Spinner />
|
||||
</Show>
|
||||
<div
|
||||
data-message={message()!.id}
|
||||
data-slot="session-turn-message-container"
|
||||
class={props.classes?.container}
|
||||
style={{ "--sticky-header-height": `${store.stickyHeaderHeight}px` }}
|
||||
>
|
||||
{/* 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) */}
|
||||
<div ref={(el) => setStore("stickyTriggerRef", el)} data-slot="session-turn-response-trigger">
|
||||
<Button
|
||||
data-slot="session-turn-collapsible-trigger-content"
|
||||
variant="ghost"
|
||||
size="small"
|
||||
onClick={() => setStore("stepsExpanded", !store.stepsExpanded)}
|
||||
>
|
||||
<Show when={working()}>
|
||||
<Spinner />
|
||||
</Show>
|
||||
<Switch>
|
||||
<Match when={working()}>{store.status ?? "Considering next steps"}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
</Switch>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Response */}
|
||||
<Show when={store.stepsExpanded}>
|
||||
<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={working()}>{store.status ?? "Considering next steps"}</Match>
|
||||
<Match when={store.stepsExpanded}>Hide steps</Match>
|
||||
<Match when={!store.stepsExpanded}>Show steps</Match>
|
||||
<Match when={lastTextPartShown() && 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>
|
||||
<span>·</span>
|
||||
<span>{store.duration}</span>
|
||||
<Icon name="chevron-grabber-vertical" size="small" />
|
||||
</Button>
|
||||
</div>
|
||||
{/* Response */}
|
||||
<Show when={store.stepsExpanded}>
|
||||
<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={lastTextPartShown() && 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">
|
||||
<h2 data-slot="session-turn-summary-title">
|
||||
<Switch>
|
||||
<Match when={message().summary?.diffs?.length}>Summary</Match>
|
||||
<Match when={true}>Response</Match>
|
||||
</Switch>
|
||||
</h2>
|
||||
<Show when={summary()}>
|
||||
{(summary) => (
|
||||
<Markdown
|
||||
data-slot="session-turn-markdown"
|
||||
data-diffs={!!message().summary?.diffs?.length}
|
||||
text={summary()}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</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() && !store.stepsExpanded}>
|
||||
<Card variant="error" class="error-card">
|
||||
{error()?.data?.message as string}
|
||||
</Card>
|
||||
)
|
||||
}}
|
||||
</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">
|
||||
<h2 data-slot="session-turn-summary-title">
|
||||
<Switch>
|
||||
<Match when={message()!.summary?.diffs?.length}>Summary</Match>
|
||||
<Match when={true}>Response</Match>
|
||||
</Switch>
|
||||
</h2>
|
||||
<Show when={summary()}>
|
||||
{(summary) => (
|
||||
<Markdown
|
||||
data-slot="session-turn-markdown"
|
||||
data-diffs={!!message()!.summary?.diffs?.length}
|
||||
text={summary()}
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
<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() && !store.stepsExpanded}>
|
||||
<Card variant="error" class="error-card">
|
||||
{error()?.data?.message as string}
|
||||
</Card>
|
||||
</Show>
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue