diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx index 4c6aab48..6a684689 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 { 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() ? ( -
- ) : null} - - ) + return
} export default CodeBlock diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index fd828629..f1a1f1aa 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) { (preEl = el)} code={local.error || ""} /> @@ -518,7 +519,6 @@ function TerminalPart(props: TerminalPartProps) { (preEl = el)} code={local.result || ""} /> @@ -596,7 +596,6 @@ export default function Share(props: { messages: Record }) { 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(() => { - const raw = window.location.hash.slice(1) - const [id] = raw.split("-") - return id - }) - const [store, setStore] = createStore<{ info?: Session.Info messages: Record @@ -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={

Waiting for messages...

} >
- - {(msg, msgIndex) => ( - - {(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 + + + {(msg, msgIndex) => ( + + + {(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 ( - - {/* User text */} - { + const hash = window.location.hash.slice(1) + if (hash !== "" && hash === anchor()) { + scrollToAnchor(hash) } - > - {(part) => ( -
+ {/* User text */} + -
- - - -
-
-
- -
-
- )} -
- {/* AI text */} - - {(part) => ( -
-
- - - -
-
-
- - - - {DateTime.fromMillis( - data().completed || 0, - ).toLocaleString(DateTime.DATETIME_MED)} - - -
-
- )} -
- {/* AI model */} - - {(assistant) => { - return ( -
-
- - ( +
+
+ + + +
+
+
+ - -
+
-
-
-
- - {assistant().providerID} + )} + + {/* AI text */} + + {(part) => ( +
+
+ + + +
+
+
+ + + + {DateTime.fromMillis( + data().completed || 0, + ).toLocaleString(DateTime.DATETIME_MED)} -
- - {assistant().modelID} - +
-
- ) - }} - - - {/* Grep tool */} - - {(_part) => { - const matches = () => toolData()?.metadata?.matches - const splitArgs = () => { - const { pattern, ...rest } = toolData()?.args - return { pattern, rest } - } - - return ( -
-
- - - -
-
-
-
-
- Grep - “{splitArgs().pattern}” + )} + + {/* AI model */} + + {(assistant) => { + return ( +
+
+ + + +
- 0 - } - > -
- - {([name, value]) => ( - <> -
-
{name}
-
{value}
- - )} -
+
+
+
+ + {assistant().providerID} + +
+ + {assistant().modelID} +
- - - 0}> -
- - setShowResults((e) => !e) - } - /> - - - -
-
- -
- -
-
-
-
- -
-
- ) - }} -
- {/* Glob tool */} - - {(_part) => { - const count = () => toolData()?.metadata?.count - const pattern = () => toolData()?.args.pattern - - return ( -
-
- - - -
-
-
-
-
- Glob - “{pattern()}”
- - 0}> -
- - setShowResults((e) => !e) - } - /> - - - -
-
- -
- -
-
-
- -
-
- ) - }} -
- {/* LS tool */} - - {(_part) => { - const path = createMemo(() => - toolData()?.args?.path !== data().rootDir - ? stripWorkingDirectory( - toolData()?.args?.path, - data().rootDir, ) - : toolData()?.args?.path, - ) + }} + - return ( -
-
- - - -
-
-
-
-
- LS - - {path()} - -
- - -
- - setShowResults((e) => !e) - } - /> - - - -
-
-
-
- -
-
- ) - }} - - {/* Read tool */} - - {(_part) => { - const filePath = createMemo(() => - stripWorkingDirectory( - toolData()?.args?.filePath, - data().rootDir, - ), - ) - const hasError = () => toolData()?.metadata?.error - const preview = () => toolData()?.metadata?.preview + {/* Grep tool */} + + {(_part) => { + const matches = () => toolData()?.metadata?.matches + const splitArgs = () => { + const { pattern, ...rest } = toolData()?.args + return { pattern, rest } + } - return ( -
-
- - - -
-
-
-
-
- Read - - {filePath()} - + return ( +
+
+ + + +
- - -
- - {formatErrorString( - toolData()?.result, - )} - +
+
+
+ Grep + “{splitArgs().pattern}”
- - {/* Always try to show CodeBlock if preview is available (even if empty string) */} - -
- - setShowResults((e) => !e) - } - /> - -
- 0 + } + > +
+ + {([name, value]) => ( + <> +
+
{name}
+
{value}
+ + )} +
+
+ + + 0}> +
+ + setShowResults((e) => !e) + } /> + + +
- -
- - {/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */} - -
- - setShowResults((e) => !e) - } - /> - - - -
-
- -
- -
-
- ) - }} - - {/* Write tool */} - - {(_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 ( -
-
- - - -
-
-
-
-
- Write - - {filePath()} - -
- 0}> - {diagnostics()} - - - -
- - {formatErrorString( - toolData()?.result - )} - -
-
- -
- - setShowResults((e) => !e) - } - /> - -
- -
-
-
-
-
-
- -
-
- ) - }} -
- {/* Edit tool */} - - {(_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 ( -
-
- - - -
-
-
-
-
- Edit - - {filePath()} - -
- - -
- - {formatErrorString(message())} - -
-
- -
- -
-
-
- 0}> - {diagnostics()} - -
- -
-
- ) - }} -
- {/* Bash tool */} - - {(_part) => { - const command = () => toolData()?.metadata?.title - const desc = () => toolData()?.metadata?.description - const result = () => toolData()?.metadata?.stdout - const error = () => toolData()?.metadata?.stderr - - return ( -
-
- - - -
-
-
- {command() && ( -
- -
- )} - -
-
- ) - }} -
- {/* Todo write */} - - {(_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 ( -
-
- - - -
-
-
-
-
- - - - Creating plan - - Completing plan + +
+ +
-
+
+
- 0}> -
    - - {(todo) => ( -
  • - - {todo.content} -
  • - )} -
    -
-
- -
-
- ) - }} -
- {/* Fetch tool */} - - {(_part) => { - const url = () => toolData()?.args.url - const format = () => toolData()?.args.format - const hasError = () => toolData()?.metadata?.error + ) + }} + + {/* Glob tool */} + + {(_part) => { + const count = () => toolData()?.metadata?.count + const pattern = () => toolData()?.args.pattern - return ( -
-
- - - -
-
-
-
-
- Fetch - {url()} + return ( +
+
+ + + +
- - -
- - {formatErrorString( - toolData()?.result, - )} - +
+
+
+ Glob + “{pattern()}”
- - -
- - setShowResults((e) => !e) - } - /> - -
- + 0}> +
+ + setShowResults((e) => !e) + } + /> + + + +
+
+ +
+
- -
- - + + +
+ +
- -
-
- ) - }} - - {/* Tool call */} - - {(part) => { - return ( + ) + }} + + {/* LS tool */} + + {(_part) => { + const path = createMemo(() => + toolData()?.args?.path !== data().rootDir + ? stripWorkingDirectory( + toolData()?.args?.path, + data().rootDir, + ) + : toolData()?.args?.path, + ) + + return ( +
+
+ + + +
+
+
+
+
+ LS + + {path()} + +
+ + +
+ + setShowResults((e) => !e) + } + /> + + + +
+
+
+
+ +
+
+ ) + }} +
+ {/* Read tool */} + + {(_part) => { + const filePath = createMemo(() => + stripWorkingDirectory( + toolData()?.args?.filePath, + data().rootDir, + ), + ) + const hasError = () => toolData()?.metadata?.error + const preview = () => toolData()?.metadata?.preview + + return ( +
+
+ + + +
+
+
+
+
+ Read + + {filePath()} + +
+ + +
+ + {formatErrorString( + toolData()?.result, + )} + +
+
+ {/* Always try to show CodeBlock if preview is available (even if empty string) */} + +
+ + setShowResults((e) => !e) + } + /> + +
+ +
+
+
+
+ {/* Fallback to TextPart if preview is not a string (e.g. undefined) AND result exists */} + +
+ + setShowResults((e) => !e) + } + /> + + + +
+
+
+
+ +
+
+ ) + }} +
+ {/* Write tool */} + + {(_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 ( +
+
+ + + +
+
+
+
+
+ Write + + {filePath()} + +
+ 0}> + {diagnostics()} + + + +
+ + {formatErrorString( + toolData()?.result + )} + +
+
+ +
+ + setShowResults((e) => !e) + } + /> + +
+ +
+
+
+
+
+
+ +
+
+ ) + }} +
+ {/* Edit tool */} + + {(_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 ( +
+
+ + + +
+
+
+
+
+ Edit + + {filePath()} + +
+ + +
+ + {formatErrorString(message())} + +
+
+ +
+ +
+
+
+ 0}> + {diagnostics()} + +
+ +
+
+ ) + }} +
+ {/* Bash tool */} + + {(_part) => { + const command = () => toolData()?.metadata?.title + const desc = () => toolData()?.metadata?.description + const result = () => toolData()?.metadata?.stdout + const error = () => toolData()?.metadata?.stderr + + return ( +
+
+ + + +
+
+
+ {command() && ( +
+ +
+ )} + +
+
+ ) + }} +
+ {/* Todo write */} + + {(_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 ( +
+
+ + + +
+
+
+
+
+ + + + Creating plan + + + Completing plan + + + +
+ 0}> +
    + + {(todo) => ( +
  • + + {todo.content} +
  • + )} +
    +
+
+
+ +
+
+ ) + }} +
+ {/* Fetch tool */} + + {(_part) => { + const url = () => toolData()?.args.url + const format = () => toolData()?.args.format + const hasError = () => toolData()?.metadata?.error + + return ( +
+
+ + + +
+
+
+
+
+ Fetch + {url()} +
+ + +
+ + {formatErrorString( + toolData()?.result, + )} + +
+
+ +
+ + setShowResults((e) => !e) + } + /> + +
+ +
+
+
+
+
+
+ +
+
+ ) + }} +
+ {/* Tool call */} + + {(part) => { + return ( +
+
+ + + +
+
+
+
+
+ {part().toolInvocation.toolName} +
+
+ + {(arg) => ( + <> +
+
{arg[0]}
+
{arg[1]}
+ + )} +
+
+ + +
+ + setShowResults((e) => !e) + } + /> + + + +
+
+ + + +
+
+ +
+
+ ) + }} +
+ {/* Fallback */} +
- + + } + > + + + + + + + +
- {part().toolInvocation.toolName} + {part.type}
-
- - {(arg) => ( - <> -
-
{arg[0]}
-
{arg[1]}
- - )} -
-
- - -
- - setShowResults((e) => !e) - } - /> - - - -
-
- - - -
-
- -
-
- ) - }} -
- {/* Fallback */} - -
-
- - - } - > - - - - - - - - - -
-
-
-
-
- {part.type} +
-
-
-
- - - ) - }} - - )} - + + + ) + }} + + + )} + +