diff options
| author | Jay V <[email protected]> | 2025-07-03 13:15:18 -0400 |
|---|---|---|
| committer | Jay V <[email protected]> | 2025-07-03 13:15:21 -0400 |
| commit | 3e2a0c7281d449452d6808fdff80e2a1d8cf998d (patch) | |
| tree | ed48f85259cee6e41ea110895a0bf8d47487d603 | |
| parent | 5a0910ea79b3f219c64f922fc775636b2bfdf07c (diff) | |
| download | opencode-3e2a0c7281d449452d6808fdff80e2a1d8cf998d.tar.gz opencode-3e2a0c7281d449452d6808fdff80e2a1d8cf998d.zip | |
docs: share handle slow loading pages
| -rw-r--r-- | packages/web/src/components/CodeBlock.tsx | 28 | ||||
| -rw-r--r-- | packages/web/src/components/Share.tsx | 1890 |
2 files changed, 943 insertions, 975 deletions
diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx index 4c6aab48e..6a6846891 100644 --- a/packages/web/src/components/CodeBlock.tsx +++ b/packages/web/src/components/CodeBlock.tsx @@ -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 diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index fd828629e..f1a1f1aa4 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -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 - - 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() - - return { metadata, args, result, duration } - }) - return ( - <Switch> - {/* User text */} - <Match - when={ - msg.role === "user" && part.type === "text" && part - } - > - {(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()} - /> - </div> - </div> - )} - </Match> - {/* AI text */} - <Match - when={ - msg.role === "assistant" && - part.type === "text" && - part + <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 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 } + }) + + onMount(() => { + const hash = window.location.hash.slice(1) + if (hash !== "" && hash === anchor()) { + scrollToAnchor(hash) } - > - {(part) => ( - <div - id={anchor()} - data-section="part" - data-part-type="ai-text" + }) + + return ( + <Switch> + {/* User text */} + <Match + when={ + msg.role === "user" && part.type === "text" && part + } > - <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 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> </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} + )} + </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)} /> - </AnchorIcon> - <div></div> + <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> - <div data-section="content"> - <div data-part-tool-body> - <div data-part-title> - <span data-element-label>Grep</span> - <b>“{splitArgs().pattern}”</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> - </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 data-section="content"> + <div data-part-tool-body> + <div data-part-title> + <span data-element-label> + {assistant().providerID} + </span> </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> + <span data-part-model> + {assistant().modelID} + </span> + </div> + </div> </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>“{pattern()}”</b> + ) + }} + </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> - <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 data-section="content"> + <div data-part-tool-body> + <div data-part-title> + <span data-element-label>Grep</span> + <b>“{splitArgs().pattern}”</b> </div> - </Match> - <Match when={toolData()?.result}> - <div data-part-tool-result> - <TextPart - expand - text={toolData()?.result} - data-size="sm" - data-color="dimmed" - /> + <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> + </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>“{pattern()}”</b> </div> - </Match> - </Switch> + <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> - <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> + {/* 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> + 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> - <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 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> - </Match> - </Switch> + <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> - <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> + ) + }} + </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> - <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>Read</span> + <b title={toolData()?.args?.filePath}> + {filePath()} + </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()} + <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> - </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> + </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> - </Match> - </Switch> + <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> - <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> + ) + }} + </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> - <Show when={diagnostics().length > 0}> - <ErrorPart>{diagnostics()}</ErrorPart> - </Show> - <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>Edit</span> + <b title={toolData()?.args?.filePath}> + {filePath()} + </b> </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 + <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())} - code={toolData()?.args?.content} /> </div> - </Show> - </div> - </Match> - </Switch> + </Match> + </Switch> + <Show when={diagnostics().length > 0}> + <ErrorPart>{diagnostics()}</ErrorPart> + </Show> + </div> + <ToolFooter + time={toolData()?.duration || 0} + /> + </div> </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> + ) + }} + </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> - <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 data-section="content"> + {command() && ( + <div data-part-tool-body> + <TerminalPart + desc={desc()} + data-size="sm" + command={command()!} + result={result()} + error={error()} /> </div> - </Match> - </Switch> - <Show when={diagnostics().length > 0}> - <ErrorPart>{diagnostics()}</ErrorPart> - </Show> + )} + <ToolFooter + time={toolData()?.duration || 0} + /> + </div> </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()} + ) + }} + </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> - )} - <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 + </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={finished()}> - Completing plan + <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> - </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 - - 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> + ) + }} + </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> - <Switch> - <Match when={hasError()}> - <div data-part-tool-result> - <ErrorPart> - {formatErrorString( - toolData()?.result, + <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> + </> )} - </ErrorPart> + </For> </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={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> - </Show> - </div> - </Match> - </Switch> + </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> - <ToolFooter - time={toolData()?.duration || 0} - /> - </div> - </div> - ) - }} - </Match> - {/* Tool call */} - <Match - when={ - msg.role === "assistant" && - part.type === "tool-invocation" && - part - } - > - {(part) => { - return ( + ) + }} + </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} - /> - </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> + <Switch + fallback={ + <IconWrenchScrewdriver + width={16} + height={16} + /> + } + > <Match when={ - part().toolInvocation.state === "call" + msg.role === "assistant" && + part.type !== "tool-invocation" } > - <TextPart - data-size="sm" - data-color="dimmed" - text="Calling..." - /> + <IconSparkles width={18} height={18} /> + </Match> + + <Match when={msg.role === "user"}> + <IconUserCircle width={18} height={18} /> </Match> </Switch> - </div> - <ToolFooter - time={toolData()?.duration || 0} - /> + </AnchorIcon> + <div></div> </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} + <div data-section="content"> + <div data-part-tool-body> + <div data-part-title> + <span data-element-label>{part.type}</span> + </div> + <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> |
