docs: share handle slow loading pages

This commit is contained in:
Jay V
2025-07-03 13:15:18 -04:00
parent 5a0910ea79
commit 3e2a0c7281
2 changed files with 959 additions and 991 deletions
+4 -24
View File
@@ -1,8 +1,6 @@
import {
type JSX,
onCleanup,
splitProps,
createEffect,
createResource,
} from "solid-js"
import { codeToHtml } from "shiki"
@@ -12,15 +10,15 @@ import { transformerNotationDiff } from "@shikijs/transformers"
interface CodeBlockProps extends JSX.HTMLAttributes<HTMLDivElement> {
code: string
lang?: string
onRendered?: () => void
}
function CodeBlock(props: CodeBlockProps) {
const [local, rest] = splitProps(props, ["code", "lang", "onRendered"])
let containerRef!: HTMLDivElement
const [local, rest] = splitProps(props, ["code", "lang"])
const [html] = createResource(
() => [local.code, local.lang],
async ([code, lang]) => {
// TODO: For testing delays
// await new Promise((resolve) => setTimeout(resolve, 3000))
return (await codeToHtml(code || "", {
lang: lang || "text",
themes: {
@@ -32,25 +30,7 @@ function CodeBlock(props: CodeBlockProps) {
},
)
onCleanup(() => {
if (containerRef) containerRef.innerHTML = ""
})
createEffect(() => {
if (html() && containerRef) {
containerRef.innerHTML = html() as string
local.onRendered?.()
}
})
return (
<>
{html() ? (
<div ref={containerRef} class={styles.codeblock} {...rest}></div>
) : null}
</>
)
return <div innerHTML={html()} class={styles.codeblock} {...rest}></div >
}
export default CodeBlock
+955 -967
View File
@@ -5,11 +5,13 @@ import {
Match,
Switch,
onMount,
Suspense,
onCleanup,
splitProps,
createMemo,
createEffect,
createSignal,
SuspenseList,
} from "solid-js"
import map from "lang-map"
import { DateTime } from "luxon"
@@ -22,7 +24,6 @@ import {
IconAnthropic,
} from "./icons/custom"
import {
IconFolder,
IconHashtag,
IconSparkles,
IconGlobeAlt,
@@ -486,6 +487,7 @@ function TerminalPart(props: TerminalPartProps) {
}
onMount(() => {
checkOverflow()
window.addEventListener("resize", checkOverflow)
})
@@ -510,7 +512,6 @@ function TerminalPart(props: TerminalPartProps) {
<CodeBlock
data-section="error"
lang="text"
onRendered={checkOverflow}
ref={(el) => (preEl = el)}
code={local.error || ""}
/>
@@ -518,7 +519,6 @@ function TerminalPart(props: TerminalPartProps) {
<Match when={local.result}>
<CodeBlock
lang="console"
onRendered={checkOverflow}
ref={(el) => (preEl = el)}
code={local.result || ""}
/>
@@ -596,7 +596,6 @@ export default function Share(props: {
messages: Record<string, Message.Info>
}) {
let lastScrollY = 0
let hasScrolled = false
let scrollTimeout: number | undefined
const id = props.id
@@ -606,12 +605,6 @@ export default function Share(props: {
const [showScrollButton, setShowScrollButton] = createSignal(false)
const [isButtonHovered, setIsButtonHovered] = createSignal(false)
const anchorId = createMemo<string | null>(() => {
const raw = window.location.hash.slice(1)
const [id] = raw.split("-")
return id
})
const [store, setStore] = createStore<{
info?: Session.Info
messages: Record<string, Message.Info>
@@ -677,11 +670,6 @@ export default function Share(props: {
if (type === "message") {
const [, messageID] = splits
setStore("messages", messageID, reconcile(d.content))
if (!hasScrolled && messageID === anchorId()) {
scrollToAnchor(window.location.hash.slice(1))
hasScrolled = true
}
}
} catch (error) {
console.error("Error parsing WebSocket message:", error)
@@ -789,20 +777,8 @@ export default function Share(props: {
for (let i = 0; i < messages().length; i++) {
const msg = messages()[i]
// TODO: Cleanup
// const system = result.messages.length === 0 && msg.role === "system"
const assistant = msg.metadata?.assistant
// if (system) {
// for (const part of msg.parts) {
// if (part.type === "text") {
// result.system.push(part.text)
// }
// }
// result.created = msg.metadata?.time.created
// continue
// }
result.messages.push(msg)
if (assistant) {
@@ -889,994 +865,1006 @@ export default function Share(props: {
fallback={<p>Waiting for messages...</p>}
>
<div class={styles.parts}>
<For each={data().messages}>
{(msg, msgIndex) => (
<For each={msg.parts}>
{(part, partIndex) => {
if (
(part.type === "step-start" &&
(partIndex() > 0 || !msg.metadata?.assistant)) ||
(msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "todoread")
)
return null
<SuspenseList>
<For each={data().messages}>
{(msg, msgIndex) => (
<Suspense>
<For each={msg.parts}>
{(part, partIndex) => {
if (
(part.type === "step-start" &&
(partIndex() > 0 || !msg.metadata?.assistant)) ||
(msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "todoread")
)
return null
const anchor = createMemo(() => `${msg.id}-${partIndex()}`)
const [showResults, setShowResults] = createSignal(false)
const isLastPart = createMemo(
() =>
data().messages.length === msgIndex() + 1 &&
msg.parts.length === partIndex() + 1,
)
const toolData = createMemo(() => {
if (
msg.role !== "assistant" ||
part.type !== "tool-invocation"
)
return {}
const anchor = createMemo(() => `${msg.id}-${partIndex()}`)
const [showResults, setShowResults] = createSignal(false)
const isLastPart = createMemo(
() =>
data().messages.length === msgIndex() + 1 &&
msg.parts.length === partIndex() + 1,
)
const toolData = createMemo(() => {
if (
msg.role !== "assistant" ||
part.type !== "tool-invocation"
)
return {}
const metadata =
msg.metadata?.tool[part.toolInvocation.toolCallId]
const args = part.toolInvocation.args
const result =
part.toolInvocation.state === "result" &&
part.toolInvocation.result
const duration = DateTime.fromMillis(
metadata?.time.end || 0,
)
.diff(DateTime.fromMillis(metadata?.time.start || 0))
.toMillis()
const metadata =
msg.metadata?.tool[part.toolInvocation.toolCallId]
const args = part.toolInvocation.args
const result =
part.toolInvocation.state === "result" &&
part.toolInvocation.result
const duration = DateTime.fromMillis(
metadata?.time.end || 0,
)
.diff(DateTime.fromMillis(metadata?.time.start || 0))
.toMillis()
return { metadata, args, result, duration }
})
return (
<Switch>
{/* User text */}
<Match
when={
msg.role === "user" && part.type === "text" && part
return { metadata, args, result, duration }
})
onMount(() => {
const hash = window.location.hash.slice(1)
if (hash !== "" && hash === anchor()) {
scrollToAnchor(hash)
}
>
{(part) => (
<div
id={anchor()}
data-section="part"
data-part-type="user-text"
})
return (
<Switch>
{/* User text */}
<Match
when={
msg.role === "user" && part.type === "text" && part
}
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconUserCircle width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<TextPart
invert
text={part().text}
expand={isLastPart()}
/>
</div>
</div>
)}
</Match>
{/* AI text */}
<Match
when={
msg.role === "assistant" &&
part.type === "text" &&
part
}
>
{(part) => (
<div
id={anchor()}
data-section="part"
data-part-type="ai-text"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconSparkles width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<MarkdownPart
highlight
expand={isLastPart()}
text={stripEnclosingTag(part().text)}
/>
<Show when={isLastPart() && data().completed}>
<span
data-part-footer
title={DateTime.fromMillis(
data().completed || 0,
).toLocaleString(
DateTime.DATETIME_FULL_WITH_SECONDS,
)}
>
{DateTime.fromMillis(
data().completed || 0,
).toLocaleString(DateTime.DATETIME_MED)}
</span>
</Show>
</div>
</div>
)}
</Match>
{/* AI model */}
<Match
when={
msg.role === "assistant" &&
part.type === "step-start" &&
msg.metadata?.assistant
}
>
{(assistant) => {
return (
<div
id={anchor()}
data-section="part"
data-part-type="ai-model"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<ProviderIcon
size={18}
provider={assistant().providerID}
{(part) => (
<div
id={anchor()}
data-section="part"
data-part-type="user-text"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconUserCircle width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<TextPart
invert
text={part().text}
expand={isLastPart()}
/>
</AnchorIcon>
<div></div>
</div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>
{assistant().providerID}
)}
</Match>
{/* AI text */}
<Match
when={
msg.role === "assistant" &&
part.type === "text" &&
part
}
>
{(part) => (
<div
id={anchor()}
data-section="part"
data-part-type="ai-text"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconSparkles width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<MarkdownPart
highlight
expand={isLastPart()}
text={stripEnclosingTag(part().text)}
/>
<Show when={isLastPart() && data().completed}>
<span
data-part-footer
title={DateTime.fromMillis(
data().completed || 0,
).toLocaleString(
DateTime.DATETIME_FULL_WITH_SECONDS,
)}
>
{DateTime.fromMillis(
data().completed || 0,
).toLocaleString(DateTime.DATETIME_MED)}
</span>
</div>
<span data-part-model>
{assistant().modelID}
</span>
</Show>
</div>
</div>
</div>
)
}}
</Match>
{/* Grep tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "grep" &&
part
}
>
{(_part) => {
const matches = () => toolData()?.metadata?.matches
const splitArgs = () => {
const { pattern, ...rest } = toolData()?.args
return { pattern, rest }
}
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-grep"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconDocumentMagnifyingGlass
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Grep</span>
<b>&ldquo;{splitArgs().pattern}&rdquo;</b>
)}
</Match>
{/* AI model */}
<Match
when={
msg.role === "assistant" &&
part.type === "step-start" &&
msg.metadata?.assistant
}
>
{(assistant) => {
return (
<div
id={anchor()}
data-section="part"
data-part-type="ai-model"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<ProviderIcon
size={18}
provider={assistant().providerID}
/>
</AnchorIcon>
<div></div>
</div>
<Show
when={
Object.keys(splitArgs().rest).length > 0
}
>
<div data-part-tool-args>
<For
each={flattenToolArgs(
splitArgs().rest,
)}
>
{([name, value]) => (
<>
<div></div>
<div>{name}</div>
<div>{value}</div>
</>
)}
</For>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>
{assistant().providerID}
</span>
</div>
<span data-part-model>
{assistant().modelID}
</span>
</div>
</Show>
<Switch>
<Match when={matches() > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={
matches() === 1
? "1 match"
: `${matches()} matches`
}
hideCopy="Hide matches"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</Show>
</div>
</Match>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Glob tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "glob" &&
part
}
>
{(_part) => {
const count = () => toolData()?.metadata?.count
const pattern = () => toolData()?.args.pattern
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-glob"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconMagnifyingGlass
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Glob</span>
<b>&ldquo;{pattern()}&rdquo;</b>
</div>
<Switch>
<Match when={count() > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={
count() === 1
? "1 result"
: `${count()} results`
}
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</Show>
</div>
</Match>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* LS tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "list" &&
part
}
>
{(_part) => {
const path = createMemo(() =>
toolData()?.args?.path !== data().rootDir
? stripWorkingDirectory(
toolData()?.args?.path,
data().rootDir,
)
: toolData()?.args?.path,
)
}}
</Match>
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-list"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconRectangleStack
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>LS</span>
<b title={toolData()?.args?.path}>
{path()}
</b>
</div>
<Switch>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Read tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "read" &&
part
}
>
{(_part) => {
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args?.filePath,
data().rootDir,
),
)
const hasError = () => toolData()?.metadata?.error
const preview = () => toolData()?.metadata?.preview
{/* Grep tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "grep" &&
part
}
>
{(_part) => {
const matches = () => toolData()?.metadata?.matches
const splitArgs = () => {
const { pattern, ...rest } = toolData()?.args
return { pattern, rest }
}
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-read"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconDocument width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Read</span>
<b title={toolData()?.args?.filePath}>
{filePath()}
</b>
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-grep"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconDocumentMagnifyingGlass
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result,
)}
</ErrorPart>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Grep</span>
<b>&ldquo;{splitArgs().pattern}&rdquo;</b>
</div>
</Match>
{/* Always try to show CodeBlock if preview is available (even if empty string) */}
<Match when={typeof preview() === 'string'}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show preview"
hideCopy="Hide preview"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getShikiLang(filePath())}
code={preview()}
<Show
when={
Object.keys(splitArgs().rest).length > 0
}
>
<div data-part-tool-args>
<For
each={flattenToolArgs(
splitArgs().rest,
)}
>
{([name, value]) => (
<>
<div></div>
<div>{name}</div>
<div>{value}</div>
</>
)}
</For>
</div>
</Show>
<Switch>
<Match when={matches() > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={
matches() === 1
? "1 match"
: `${matches()} matches`
}
hideCopy="Hide matches"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</Show>
</div>
</Show>
</div>
</Match>
{/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */}
<Match when={typeof preview() !== 'string' && toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Write tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "write" &&
part
}
>
{(_part) => {
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args?.filePath,
data().rootDir,
),
)
const hasError = () => toolData()?.metadata?.error
const content = () => toolData()?.args?.content
const diagnostics = createMemo(() =>
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath,
),
)
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-write"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconDocumentPlus width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Write</span>
<b title={toolData()?.args?.filePath}>
{filePath()}
</b>
</div>
<Show when={diagnostics().length > 0}>
<ErrorPart>{diagnostics()}</ErrorPart>
</Show>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result
)}
</ErrorPart>
</div>
</Match>
<Match when={content()}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show contents"
hideCopy="Hide contents"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getShikiLang(filePath())}
code={toolData()?.args?.content}
/>
</div>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Edit tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "edit" &&
part
}
>
{(_part) => {
const diff = () => toolData()?.metadata?.diff
const message = () => toolData()?.metadata?.message
const hasError = () => toolData()?.metadata?.error
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args.filePath,
data().rootDir,
),
)
const diagnostics = createMemo(() =>
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath,
),
)
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-edit"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconPencilSquare width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Edit</span>
<b title={toolData()?.args?.filePath}>
{filePath()}
</b>
</div>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(message())}
</ErrorPart>
</div>
</Match>
<Match when={diff()}>
<div data-part-tool-edit>
<DiffView
class={styles["diff-code-block"]}
diff={diff()}
lang={getShikiLang(filePath())}
/>
</div>
</Match>
</Switch>
<Show when={diagnostics().length > 0}>
<ErrorPart>{diagnostics()}</ErrorPart>
</Show>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Bash tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "bash" &&
part
}
>
{(_part) => {
const command = () => toolData()?.metadata?.title
const desc = () => toolData()?.metadata?.description
const result = () => toolData()?.metadata?.stdout
const error = () => toolData()?.metadata?.stderr
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-bash"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconCommandLine width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
{command() && (
<div data-part-tool-body>
<TerminalPart
desc={desc()}
data-size="sm"
command={command()!}
result={result()}
error={error()}
/>
</div>
)}
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Todo write */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "todowrite" &&
part
}
>
{(_part) => {
const todos = createMemo(() =>
sortTodosByStatus(toolData()?.args?.todos ?? []),
)
const starting = () =>
todos().every((t) => t.status === "pending")
const finished = () =>
todos().every((t) => t.status === "completed")
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-todo"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconQueueList width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>
<Switch fallback="Updating plan">
<Match when={starting()}>
Creating plan
</Match>
<Match when={finished()}>
Completing plan
<Match when={toolData()?.result}>
<div data-part-tool-result>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</div>
</Match>
</Switch>
</span>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
<Show when={todos().length > 0}>
<ul class={styles.todos}>
<For each={todos()}>
{(todo) => (
<li data-status={todo.status}>
<span></span>
{todo.content}
</li>
)}
</For>
</ul>
</Show>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Fetch tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "webfetch" &&
part
}
>
{(_part) => {
const url = () => toolData()?.args.url
const format = () => toolData()?.args.format
const hasError = () => toolData()?.metadata?.error
)
}}
</Match>
{/* Glob tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "glob" &&
part
}
>
{(_part) => {
const count = () => toolData()?.metadata?.count
const pattern = () => toolData()?.args.pattern
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-fetch"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconGlobeAlt width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Fetch</span>
<b>{url()}</b>
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-glob"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconMagnifyingGlass
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result,
)}
</ErrorPart>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Glob</span>
<b>&ldquo;{pattern()}&rdquo;</b>
</div>
</Match>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={format() || "text"}
code={toolData()?.result}
<Switch>
<Match when={count() > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={
count() === 1
? "1 result"
: `${count()} results`
}
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</Show>
</div>
</Match>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</div>
</Show>
</div>
</Match>
</Switch>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Tool call */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part
}
>
{(part) => {
return (
)
}}
</Match>
{/* LS tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "list" &&
part
}
>
{(_part) => {
const path = createMemo(() =>
toolData()?.args?.path !== data().rootDir
? stripWorkingDirectory(
toolData()?.args?.path,
data().rootDir,
)
: toolData()?.args?.path,
)
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-list"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconRectangleStack
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>LS</span>
<b title={toolData()?.args?.path}>
{path()}
</b>
</div>
<Switch>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Read tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "read" &&
part
}
>
{(_part) => {
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args?.filePath,
data().rootDir,
),
)
const hasError = () => toolData()?.metadata?.error
const preview = () => toolData()?.metadata?.preview
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-read"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconDocument width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Read</span>
<b title={toolData()?.args?.filePath}>
{filePath()}
</b>
</div>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result,
)}
</ErrorPart>
</div>
</Match>
{/* Always try to show CodeBlock if preview is available (even if empty string) */}
<Match when={typeof preview() === 'string'}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show preview"
hideCopy="Hide preview"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getShikiLang(filePath())}
code={preview()}
/>
</div>
</Show>
</div>
</Match>
{/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */}
<Match when={typeof preview() !== 'string' && toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Write tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "write" &&
part
}
>
{(_part) => {
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args?.filePath,
data().rootDir,
),
)
const hasError = () => toolData()?.metadata?.error
const content = () => toolData()?.args?.content
const diagnostics = createMemo(() =>
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath,
),
)
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-write"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconDocumentPlus width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Write</span>
<b title={toolData()?.args?.filePath}>
{filePath()}
</b>
</div>
<Show when={diagnostics().length > 0}>
<ErrorPart>{diagnostics()}</ErrorPart>
</Show>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result
)}
</ErrorPart>
</div>
</Match>
<Match when={content()}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show contents"
hideCopy="Hide contents"
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getShikiLang(filePath())}
code={toolData()?.args?.content}
/>
</div>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Edit tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "edit" &&
part
}
>
{(_part) => {
const diff = () => toolData()?.metadata?.diff
const message = () => toolData()?.metadata?.message
const hasError = () => toolData()?.metadata?.error
const filePath = createMemo(() =>
stripWorkingDirectory(
toolData()?.args.filePath,
data().rootDir,
),
)
const diagnostics = createMemo(() =>
getDiagnostics(
toolData()?.metadata?.diagnostics,
toolData()?.args.filePath,
),
)
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-edit"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconPencilSquare width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Edit</span>
<b title={toolData()?.args?.filePath}>
{filePath()}
</b>
</div>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(message())}
</ErrorPart>
</div>
</Match>
<Match when={diff()}>
<div data-part-tool-edit>
<DiffView
class={styles["diff-code-block"]}
diff={diff()}
lang={getShikiLang(filePath())}
/>
</div>
</Match>
</Switch>
<Show when={diagnostics().length > 0}>
<ErrorPart>{diagnostics()}</ErrorPart>
</Show>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Bash tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "bash" &&
part
}
>
{(_part) => {
const command = () => toolData()?.metadata?.title
const desc = () => toolData()?.metadata?.description
const result = () => toolData()?.metadata?.stdout
const error = () => toolData()?.metadata?.stderr
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-bash"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconCommandLine width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
{command() && (
<div data-part-tool-body>
<TerminalPart
desc={desc()}
data-size="sm"
command={command()!}
result={result()}
error={error()}
/>
</div>
)}
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Todo write */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "todowrite" &&
part
}
>
{(_part) => {
const todos = createMemo(() =>
sortTodosByStatus(toolData()?.args?.todos ?? []),
)
const starting = () =>
todos().every((t) => t.status === "pending")
const finished = () =>
todos().every((t) => t.status === "completed")
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-todo"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconQueueList width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>
<Switch fallback="Updating plan">
<Match when={starting()}>
Creating plan
</Match>
<Match when={finished()}>
Completing plan
</Match>
</Switch>
</span>
</div>
<Show when={todos().length > 0}>
<ul class={styles.todos}>
<For each={todos()}>
{(todo) => (
<li data-status={todo.status}>
<span></span>
{todo.content}
</li>
)}
</For>
</ul>
</Show>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Fetch tool */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part.toolInvocation.toolName === "webfetch" &&
part
}
>
{(_part) => {
const url = () => toolData()?.args.url
const format = () => toolData()?.args.format
const hasError = () => toolData()?.metadata?.error
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-fetch"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconGlobeAlt width={18} height={18} />
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>Fetch</span>
<b>{url()}</b>
</div>
<Switch>
<Match when={hasError()}>
<div data-part-tool-result>
<ErrorPart>
{formatErrorString(
toolData()?.result,
)}
</ErrorPart>
</div>
</Match>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={format() || "text"}
code={toolData()?.result}
/>
</div>
</Show>
</div>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Tool call */}
<Match
when={
msg.role === "assistant" &&
part.type === "tool-invocation" &&
part
}
>
{(part) => {
return (
<div
id={anchor()}
data-section="part"
data-part-type="tool-fallback"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconWrenchScrewdriver
width={18}
height={18}
/>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
{part().toolInvocation.toolName}
</div>
<div data-part-tool-args>
<For
each={flattenToolArgs(
part().toolInvocation.args,
)}
>
{(arg) => (
<>
<div></div>
<div>{arg[0]}</div>
<div>{arg[1]}</div>
</>
)}
</For>
</div>
<Switch>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</Show>
</div>
</Match>
<Match
when={
part().toolInvocation.state === "call"
}
>
<TextPart
data-size="sm"
data-color="dimmed"
text="Calling..."
/>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Fallback */}
<Match when={true}>
<div
id={anchor()}
data-section="part"
data-part-type="tool-fallback"
data-part-type="fallback"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<IconWrenchScrewdriver
width={18}
height={18}
/>
<Switch
fallback={
<IconWrenchScrewdriver
width={16}
height={16}
/>
}
>
<Match
when={
msg.role === "assistant" &&
part.type !== "tool-invocation"
}
>
<IconSparkles width={18} height={18} />
</Match>
<Match when={msg.role === "user"}>
<IconUserCircle width={18} height={18} />
</Match>
</Switch>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
{part().toolInvocation.toolName}
<span data-element-label>{part.type}</span>
</div>
<div data-part-tool-args>
<For
each={flattenToolArgs(
part().toolInvocation.args,
)}
>
{(arg) => (
<>
<div></div>
<div>{arg[0]}</div>
<div>{arg[1]}</div>
</>
)}
</For>
</div>
<Switch>
<Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
results={showResults()}
onClick={() =>
setShowResults((e) => !e)
}
/>
<Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
text={toolData()?.result}
/>
</Show>
</div>
</Match>
<Match
when={
part().toolInvocation.state === "call"
}
>
<TextPart
data-size="sm"
data-color="dimmed"
text="Calling..."
/>
</Match>
</Switch>
</div>
<ToolFooter
time={toolData()?.duration || 0}
/>
</div>
</div>
)
}}
</Match>
{/* Fallback */}
<Match when={true}>
<div
id={anchor()}
data-section="part"
data-part-type="fallback"
>
<div data-section="decoration">
<AnchorIcon id={anchor()}>
<Switch
fallback={
<IconWrenchScrewdriver
width={16}
height={16}
<TextPart
text={JSON.stringify(part, null, 2)}
/>
}
>
<Match
when={
msg.role === "assistant" &&
part.type !== "tool-invocation"
}
>
<IconSparkles width={18} height={18} />
</Match>
<Match when={msg.role === "user"}>
<IconUserCircle width={18} height={18} />
</Match>
</Switch>
</AnchorIcon>
<div></div>
</div>
<div data-section="content">
<div data-part-tool-body>
<div data-part-title>
<span data-element-label>{part.type}</span>
</div>
</div>
<TextPart
text={JSON.stringify(part, null, 2)}
/>
</div>
</div>
</div>
</Match>
</Switch>
)
}}
</For>
)}
</For>
</Match>
</Switch>
)
}}
</For>
</Suspense>
)}
</For>
</SuspenseList>
<div data-section="part" data-part-type="summary">
<div data-section="decoration">
<span data-status={connectionStatus()[0]}></span>