From f884766445bbf1fbce11f1db4bc6174e72d9baa5 Mon Sep 17 00:00:00 2001 From: Dax Date: Mon, 7 Jul 2025 15:53:43 -0400 Subject: v2 message format and upgrade to ai sdk v5 (#743) Co-authored-by: GitHub Action Co-authored-by: Liang-Shih Lin Co-authored-by: Dominik Engelhardt Co-authored-by: Jay V Co-authored-by: adamdottv <2363879+adamdottv@users.noreply.github.com> --- packages/web/src/components/CodeBlock.tsx | 8 +- packages/web/src/components/DiffView.tsx | 246 -- packages/web/src/components/MarkdownView.tsx | 39 - packages/web/src/components/Share.tsx | 1784 ++------------- packages/web/src/components/codeblock.module.css | 1 - packages/web/src/components/diffview.module.css | 121 - packages/web/src/components/icons/custom.tsx | 7 +- packages/web/src/components/icons/index.tsx | 2395 +++----------------- .../web/src/components/markdownview.module.css | 106 - packages/web/src/components/share.module.css | 184 +- packages/web/src/components/share/common.tsx | 60 + .../src/components/share/content-code.module.css | 25 + packages/web/src/components/share/content-code.tsx | 32 + .../src/components/share/content-diff.module.css | 125 + packages/web/src/components/share/content-diff.tsx | 231 ++ .../components/share/content-markdown.module.css | 140 ++ .../web/src/components/share/content-markdown.tsx | 65 + .../src/components/share/content-text.module.css | 57 + packages/web/src/components/share/content-text.tsx | 35 + packages/web/src/components/share/part.module.css | 375 +++ packages/web/src/components/share/part.tsx | 664 ++++++ packages/web/src/content/docs/docs/cli.mdx | 22 +- packages/web/src/content/docs/docs/config.mdx | 7 +- packages/web/src/content/docs/docs/index.mdx | 26 +- packages/web/src/content/docs/docs/rules.mdx | 3 + packages/web/src/content/docs/docs/themes.mdx | 26 +- packages/web/src/types/lang-map.d.ts | 12 +- 27 files changed, 2511 insertions(+), 4285 deletions(-) delete mode 100644 packages/web/src/components/DiffView.tsx delete mode 100644 packages/web/src/components/MarkdownView.tsx delete mode 100644 packages/web/src/components/diffview.module.css delete mode 100644 packages/web/src/components/markdownview.module.css create mode 100644 packages/web/src/components/share/common.tsx create mode 100644 packages/web/src/components/share/content-code.module.css create mode 100644 packages/web/src/components/share/content-code.tsx create mode 100644 packages/web/src/components/share/content-diff.module.css create mode 100644 packages/web/src/components/share/content-diff.tsx create mode 100644 packages/web/src/components/share/content-markdown.module.css create mode 100644 packages/web/src/components/share/content-markdown.tsx create mode 100644 packages/web/src/components/share/content-text.module.css create mode 100644 packages/web/src/components/share/content-text.tsx create mode 100644 packages/web/src/components/share/part.module.css create mode 100644 packages/web/src/components/share/part.tsx (limited to 'packages/web/src') diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx index 6a6846891..3702044b9 100644 --- a/packages/web/src/components/CodeBlock.tsx +++ b/packages/web/src/components/CodeBlock.tsx @@ -1,8 +1,4 @@ -import { - type JSX, - splitProps, - createResource, -} from "solid-js" +import { type JSX, splitProps, createResource } from "solid-js" import { codeToHtml } from "shiki" import styles from "./codeblock.module.css" import { transformerNotationDiff } from "@shikijs/transformers" @@ -30,7 +26,7 @@ function CodeBlock(props: CodeBlockProps) { }, ) - return
+ return
} export default CodeBlock diff --git a/packages/web/src/components/DiffView.tsx b/packages/web/src/components/DiffView.tsx deleted file mode 100644 index 66dd7f0fc..000000000 --- a/packages/web/src/components/DiffView.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { type Component, createMemo } from "solid-js" -import { parsePatch } from "diff" -import CodeBlock from "./CodeBlock" -import styles from "./diffview.module.css" - -type DiffRow = { - left: string - right: string - type: "added" | "removed" | "unchanged" | "modified" -} - -interface DiffViewProps { - diff: string - lang?: string - class?: string -} - -const DiffView: Component = (props) => { - - const rows = createMemo(() => { - const diffRows: DiffRow[] = [] - - try { - const patches = parsePatch(props.diff) - - for (const patch of patches) { - for (const hunk of patch.hunks) { - const lines = hunk.lines - let i = 0 - - while (i < lines.length) { - const line = lines[i] - const content = line.slice(1) - const prefix = line[0] - - if (prefix === '-') { - // Look ahead for consecutive additions to pair with removals - const removals: string[] = [content] - let j = i + 1 - - // Collect all consecutive removals - while (j < lines.length && lines[j][0] === '-') { - removals.push(lines[j].slice(1)) - j++ - } - - // Collect all consecutive additions that follow - const additions: string[] = [] - while (j < lines.length && lines[j][0] === '+') { - additions.push(lines[j].slice(1)) - j++ - } - - // Pair removals with additions - const maxLength = Math.max(removals.length, additions.length) - for (let k = 0; k < maxLength; k++) { - const hasLeft = k < removals.length - const hasRight = k < additions.length - - if (hasLeft && hasRight) { - // Replacement - left is removed, right is added - diffRows.push({ - left: removals[k], - right: additions[k], - type: "modified" - }) - } else if (hasLeft) { - // Pure removal - diffRows.push({ - left: removals[k], - right: "", - type: "removed" - }) - } else if (hasRight) { - // Pure addition - only create if we actually have content - diffRows.push({ - left: "", - right: additions[k], - type: "added" - }) - } - } - - i = j - } else if (prefix === '+') { - // Standalone addition (not paired with removal) - diffRows.push({ - left: "", - right: content, - type: "added" - }) - i++ - } else if (prefix === ' ') { - diffRows.push({ - left: content, - right: content, - type: "unchanged" - }) - i++ - } else { - i++ - } - } - } - } - } catch (error) { - console.error("Failed to parse patch:", error) - return [] - } - - return diffRows - }) - - const mobileRows = createMemo(() => { - const mobileBlocks: { type: 'removed' | 'added' | 'unchanged', lines: string[] }[] = [] - const currentRows = rows() - - let i = 0 - while (i < currentRows.length) { - const removedLines: string[] = [] - const addedLines: string[] = [] - - // Collect consecutive modified/removed/added rows - while (i < currentRows.length && - (currentRows[i].type === 'modified' || - currentRows[i].type === 'removed' || - currentRows[i].type === 'added')) { - const row = currentRows[i] - if (row.left && (row.type === 'removed' || row.type === 'modified')) { - removedLines.push(row.left) - } - if (row.right && (row.type === 'added' || row.type === 'modified')) { - addedLines.push(row.right) - } - i++ - } - - // Add grouped blocks - if (removedLines.length > 0) { - mobileBlocks.push({ type: 'removed', lines: removedLines }) - } - if (addedLines.length > 0) { - mobileBlocks.push({ type: 'added', lines: addedLines }) - } - - // Add unchanged rows as-is - if (i < currentRows.length && currentRows[i].type === 'unchanged') { - mobileBlocks.push({ - type: 'unchanged', - lines: [currentRows[i].left] - }) - i++ - } - } - - return mobileBlocks - }) - - return ( -
-
- {rows().map((r) => ( -
-
- -
-
- -
-
- ))} -
- -
- {mobileRows().map((block) => ( -
- {block.lines.map((line) => ( - - ))} -
- ))} -
-
- ) -} - -export default DiffView - -// const testDiff = `--- combined_before.txt 2025-06-24 16:38:08 -// +++ combined_after.txt 2025-06-24 16:38:12 -// @@ -1,21 +1,25 @@ -// unchanged line -// -deleted line -// -old content -// +added line -// +new content -// -// -removed empty line below -// +added empty line above -// -// - tab indented -// -trailing spaces -// -very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view -// -unicode content: πŸš€ ✨ δΈ­ζ–‡ -// -mixed content with tabs and spaces -// + space indented -// +no trailing spaces -// +short line -// +very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals -// +different unicode: πŸŽ‰ πŸ’» ζ—₯本θͺž -// +normalized content with consistent spacing -// +newline to content -// -// -content to remove -// -whitespace only: -// -multiple -// -consecutive -// -deletions -// -single deletion -// + -// +single addition -// +first addition -// +second addition -// +third addition -// line before addition -// +first added line -// + -// +third added line -// line after addition -// final unchanged line` diff --git a/packages/web/src/components/MarkdownView.tsx b/packages/web/src/components/MarkdownView.tsx deleted file mode 100644 index 7a63bc0cb..000000000 --- a/packages/web/src/components/MarkdownView.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { type JSX, splitProps, createResource } from "solid-js" -import { marked } from "marked" -import markedShiki from "marked-shiki" -import { codeToHtml } from "shiki" -import { transformerNotationDiff } from "@shikijs/transformers" -import styles from "./markdownview.module.css" - -interface MarkdownViewProps extends JSX.HTMLAttributes { - markdown: string -} - -const markedWithShiki = marked.use( - markedShiki({ - highlight(code, lang) { - return codeToHtml(code, { - lang: lang || "text", - themes: { - light: "github-light", - dark: "github-dark", - }, - transformers: [transformerNotationDiff()], - }) - }, - }), -) - -function MarkdownView(props: MarkdownViewProps) { - const [local, rest] = splitProps(props, ["markdown"]) - const [html] = createResource( - () => local.markdown, - async (markdown) => { - return markedWithShiki.parse(markdown) - }, - ) - - return
-} - -export default MarkdownView diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index ed889790d..40e2763d1 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -1,4 +1,3 @@ -import { type JSX } from "solid-js" import { For, Show, @@ -7,77 +6,21 @@ import { onMount, Suspense, onCleanup, - splitProps, createMemo, - createEffect, createSignal, SuspenseList, } from "solid-js" -import map from "lang-map" import { DateTime } from "luxon" import { createStore, reconcile } from "solid-js/store" -import type { Diagnostic } from "vscode-languageserver-types" -import { - IconOpenAI, - IconGemini, - IconOpencode, - IconAnthropic, -} from "./icons/custom" -import { - IconHashtag, - IconSparkles, - IconGlobeAlt, - IconDocument, - IconQueueList, - IconUserCircle, - IconCheckCircle, - IconChevronDown, - IconCommandLine, - IconChevronRight, - IconDocumentPlus, - IconPencilSquare, - IconRectangleStack, - IconMagnifyingGlass, - IconWrenchScrewdriver, - IconDocumentMagnifyingGlass, - IconArrowDown, -} from "./icons" -import DiffView from "./DiffView" -import CodeBlock from "./CodeBlock" -import MarkdownView from "./MarkdownView" +import { IconOpenAI, IconGemini, IconOpencode, IconAnthropic } from "./icons/custom" +import { IconSparkles, IconArrowDown } from "./icons" import styles from "./share.module.css" +import type { MessageV2 } from "opencode/session/message-v2" import type { Message } from "opencode/session/message" import type { Session } from "opencode/session/index" +import { Part } from "./share/part" -const MIN_DURATION = 2 - -type Status = - | "disconnected" - | "connecting" - | "connected" - | "error" - | "reconnecting" - -type TodoStatus = "pending" | "in_progress" | "completed" - -interface Todo { - id: string - content: string - status: TodoStatus - priority: "low" | "medium" | "high" -} - -function sortTodosByStatus(todos: Todo[]) { - const statusPriority: Record = { - in_progress: 0, - pending: 1, - completed: 2, - } - - return todos - .slice() - .sort((a, b) => statusPriority[a.status] - statusPriority[b.status]) -} +type Status = "disconnected" | "connecting" | "connected" | "error" | "reconnecting" function scrollToAnchor(id: string) { const el = document.getElementById(id) @@ -86,146 +29,6 @@ function scrollToAnchor(id: string) { el.scrollIntoView({ behavior: "smooth" }) } -function stripWorkingDirectory(filePath?: string, workingDir?: string) { - if (filePath === undefined || workingDir === undefined) return filePath - - const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/" - - if (filePath === workingDir) { - return "" - } - - if (filePath.startsWith(prefix)) { - return filePath.slice(prefix.length) - } - - return filePath -} - -function getShikiLang(filename: string) { - const ext = filename.split(".").pop()?.toLowerCase() ?? "" - - // map.languages(ext) returns an array of matching Linguist language names (e.g. ['TypeScript']) - const langs = map.languages(ext) - const type = langs?.[0]?.toLowerCase() - - // Overrride any specific language mappings - const overrides: Record = { - conf: "shellscript", - } - - return type ? (overrides[type] ?? type) : "plaintext" -} - -function formatDuration(ms: number): string { - const ONE_SECOND = 1000 - const ONE_MINUTE = 60 * ONE_SECOND - - if (ms >= ONE_MINUTE) { - const minutes = Math.floor(ms / ONE_MINUTE) - return minutes === 1 ? `1min` : `${minutes}mins` - } - - if (ms >= ONE_SECOND) { - const seconds = Math.floor(ms / ONE_SECOND) - return `${seconds}s` - } - - return `${ms}ms` -} - -// Converts nested objects/arrays into [path, value] pairs. -// E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]] -function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { - const entries: Array<[string, any]> = [] - - for (const [key, value] of Object.entries(obj)) { - const path = prefix ? `${prefix}.${key}` : key - - if (value !== null && typeof value === "object") { - if (Array.isArray(value)) { - value.forEach((item, index) => { - const arrayPath = `${path}[${index}]` - if (item !== null && typeof item === "object") { - entries.push(...flattenToolArgs(item, arrayPath)) - } else { - entries.push([arrayPath, item]) - } - }) - } else { - entries.push(...flattenToolArgs(value, path)) - } - } else { - entries.push([path, value]) - } - } - - return entries -} - -function formatErrorString(error: string): JSX.Element { - const errorMarker = "Error: " - const startsWithError = error.startsWith(errorMarker) - - return startsWithError ? ( -
-      
-        Error
-      
-      {error.slice(errorMarker.length)}
-    
- ) : ( -
-      {error}
-    
- ) -} - -function getDiagnostics( - diagnosticsByFile: Record, - currentFile: string, -): JSX.Element[] { - // Return a flat array of error diagnostics, in the format: - // "Error [65:20] Property 'x' does not exist on type 'Y'" - const result: JSX.Element[] = [] - - if ( - diagnosticsByFile === undefined || - diagnosticsByFile[currentFile] === undefined - ) - return result - - for (const diags of Object.values(diagnosticsByFile)) { - for (const d of diags) { - // Only keep diagnostics explicitly marked as Error (severity === 1) - if (d.severity !== 1) continue - - const line = d.range.start.line + 1 // 1-based - const column = d.range.start.character + 1 // 1-based - - result.push( -
-          
-            Error
-          
-          
-            [{line}:{column}]
-          
-          {d.message}
-        
, - ) - } - } - - return result -} - -function stripEnclosingTag(text: string): string { - const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ - const match = text.match(wrappedRe) - return match ? match[2] : text -} - function getStatusText(status: [Status, string?]): string { switch (status[0]) { case "connected": @@ -243,44 +46,6 @@ function getStatusText(status: [Status, string?]): string { } } -function checkOverflow(getEl: () => HTMLElement | undefined, watch?: () => any) { - const [needsToggle, setNeedsToggle] = createSignal(false) - - function measure() { - const el = getEl() - if (!el) return - setNeedsToggle(el.scrollHeight > el.clientHeight + 1) - } - - onMount(() => { - let raf = 0 - - function probe() { - const el = getEl() - if (el && el.offsetParent !== null && el.getBoundingClientRect().height) { - measure() - } - else { - raf = requestAnimationFrame(probe) - } - } - raf = requestAnimationFrame(probe) - - const ro = new ResizeObserver(measure) - const el = getEl() - if (el) ro.observe(el) - - onCleanup(() => { - cancelAnimationFrame(raf) - ro.disconnect() - }) - }) - - if (watch) createEffect(measure) - - return needsToggle -} - function ProviderIcon(props: { provider: string; size?: number }) { const size = props.size || 16 return ( @@ -298,262 +63,11 @@ function ProviderIcon(props: { provider: string; size?: number }) { ) } -interface ResultsButtonProps extends JSX.HTMLAttributes { - showCopy?: string - hideCopy?: string - results: boolean -} -function ResultsButton(props: ResultsButtonProps) { - const [local, rest] = splitProps(props, ["results", "showCopy", "hideCopy"]) - return ( - - ) -} - -interface TextPartProps extends JSX.HTMLAttributes { - text: string - expand?: boolean -} -function TextPart(props: TextPartProps) { - let preEl: HTMLPreElement | undefined - - const [local, rest] = splitProps(props, ["text", "expand"]) - const [expanded, setExpanded] = createSignal(false) - const overflowed = checkOverflow(() => preEl, () => local.expand) - - return ( -
-
{local.text}
- {((!local.expand && overflowed()) || expanded()) && ( - - )} -
- ) -} - -interface ErrorPartProps extends JSX.HTMLAttributes { - expand?: boolean -} -function ErrorPart(props: ErrorPartProps) { - let preEl: HTMLDivElement | undefined - - const [local, rest] = splitProps(props, ["expand", "children"]) - const [expanded, setExpanded] = createSignal(false) - const overflowed = checkOverflow(() => preEl, () => local.expand) - - return ( -
-
- {local.children} -
- {((!local.expand && overflowed()) || expanded()) && ( - - )} -
- ) -} - -interface MarkdownPartProps extends JSX.HTMLAttributes { - text: string - expand?: boolean - highlight?: boolean -} -function MarkdownPart(props: MarkdownPartProps) { - let divEl: HTMLDivElement | undefined - - const [local, rest] = splitProps(props, ["text", "expand", "highlight"]) - const [expanded, setExpanded] = createSignal(false) - const overflowed = checkOverflow(() => divEl, () => local.expand) - - return ( -
- (divEl = el)} - /> - {((!local.expand && overflowed()) || expanded()) && ( - - )} -
- ) -} - -interface TerminalPartProps extends JSX.HTMLAttributes { - command: string - error?: string - result?: string - desc?: string - expand?: boolean -} -function TerminalPart(props: TerminalPartProps) { - const [local, rest] = splitProps(props, [ - "command", - "error", - "result", - "desc", - "expand", - ]) - let preEl: HTMLDivElement | undefined - - const [expanded, setExpanded] = createSignal(false) - const overflowed = checkOverflow( - () => { - if (!preEl) return - return preEl.getElementsByTagName("pre")[0] - }, - () => local.expand - ) - - return ( -
-
-
- {local.desc} -
-
- - - - - - - - - -
-
- {((!local.expand && overflowed()) || expanded()) && ( - - )} -
- ) -} - -function ToolFooter(props: { time: number }) { - return props.time > MIN_DURATION ? ( - - {formatDuration(props.time)} - - ) : ( -
- ) -} - -interface AnchorProps extends JSX.HTMLAttributes { - id: string -} -function AnchorIcon(props: AnchorProps) { - const [local, rest] = splitProps(props, ["id", "children"]) - const [copied, setCopied] = createSignal(false) - - return ( - - ) -} - export default function Share(props: { id: string api: string info: Session.Info - messages: Record + messages: Record }) { let lastScrollY = 0 let hasScrolledToAnchor = false @@ -571,14 +85,10 @@ export default function Share(props: { const [store, setStore] = createStore<{ info?: Session.Info - messages: Record + messages: Record }>({ info: props.info, messages: props.messages }) - const messages = createMemo(() => - Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)), - ) - const [connectionStatus, setConnectionStatus] = createSignal< - [Status, string?] - >(["disconnected", "Disconnected"]) + const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id))) + const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"]) onMount(() => { const apiUrl = props.api @@ -653,10 +163,7 @@ export default function Share(props: { // Try to reconnect after 2 seconds clearTimeout(reconnectTimer) - reconnectTimer = window.setTimeout( - setupWebSocket, - 2000, - ) as unknown as number + reconnectTimer = window.setTimeout(setupWebSocket, 2000) as unknown as number } } @@ -754,7 +261,7 @@ export default function Share(props: { rootDir: undefined as string | undefined, created: undefined as number | undefined, completed: undefined as number | undefined, - messages: [] as Message.Info[], + messages: [] as MessageV2.Info[], models: {} as Record, cost: 0, tokens: { @@ -766,46 +273,41 @@ export default function Share(props: { result.created = props.info.time.created - for (let i = 0; i < messages().length; i++) { - const msg = messages()[i] - - const assistant = msg.metadata?.assistant + const msgs = messages() + for (let i = 0; i < msgs.length; i++) { + const msg = "metadata" in msgs[i] ? fromV1(msgs[i] as Message.Info) : (msgs[i] as MessageV2.Info) result.messages.push(msg) - if (assistant) { - result.cost += assistant.cost - result.tokens.input += assistant.tokens.input - result.tokens.output += assistant.tokens.output - result.tokens.reasoning += assistant.tokens.reasoning + if (msg.role === "assistant") { + result.cost += msg.cost + result.tokens.input += msg.tokens.input + result.tokens.output += msg.tokens.output + result.tokens.reasoning += msg.tokens.reasoning - result.models[`${assistant.providerID} ${assistant.modelID}`] = [ - assistant.providerID, - assistant.modelID, - ] + result.models[`${msg.providerID} ${msg.modelID}`] = [msg.providerID, msg.modelID] - if (assistant.path?.root) { - result.rootDir = assistant.path.root + if (msg.path.root) { + result.rootDir = msg.path.root } - if (msg.metadata?.time.completed) { - result.completed = msg.metadata?.time.completed + if (msg.time.completed) { + result.completed = msg.time.completed } } } + console.log(result.messages) return result }) return ( -
-
-
-

{store.info?.title}

-
-
-
    -
  • -
    +
    +
    +

    {store.info?.title}

    +
    +
      +
    • +
      @@ -815,11 +317,11 @@ export default function Share(props: { {Object.values(data().models).length > 0 ? ( {([provider, model]) => ( -
    • -
      +
    • +
      - {model} + {model}
    • )} @@ -830,1086 +332,52 @@ export default function Share(props: { )}
    -
    - {data().created ? ( - - {DateTime.fromMillis(data().created || 0).toLocaleString( - DateTime.DATETIME_MED, - )} - - ) : ( - - Started at — - - )} +
    + {DateTime.fromMillis(data().created || 0).toLocaleString(DateTime.DATETIME_MED)}
    - 0} - fallback={

    Waiting for messages...

    } - > + 0} fallback={

    Waiting for messages...

    }>
    {(msg, msgIndex) => ( - + { + if (x.type === "step-start" && index > 0) return false + if (x.type === "tool" && x.tool === "todoread") return false + if (x.type === "text" && !x.text) return false + if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running")) + return false + return true + })} + > {(part, partIndex) => { - if ( - (part.type === "step-start" && - (partIndex() > 0 || !msg.metadata?.assistant)) || - (msg.role === "assistant" && - part.type === "tool-invocation" && - part.toolInvocation.toolName === "todoread") + const last = createMemo( + () => data().messages.length === msgIndex() + 1 && msg.parts.length === partIndex() + 1, ) - 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) // Wait till all parts are loaded if ( - hash !== "" - && !hasScrolledToAnchor - && msg.parts.length === partIndex() + 1 - && data().messages.length === msgIndex() + 1 + hash !== "" && + !hasScrolledToAnchor && + msg.parts.length === partIndex() + 1 && + data().messages.length === msgIndex() + 1 ) { hasScrolledToAnchor = true scrollToAnchor(hash) } }) - return ( - - {/* User text */} - - {(part) => ( -
    -
    - - - -
    -
    -
    - -
    -
    - )} -
    - {/* AI text */} - - {(part) => ( -
    -
    - - - -
    -
    -
    - - - - {DateTime.fromMillis( - data().completed || 0, - ).toLocaleString(DateTime.DATETIME_MED)} - - -
    -
    - )} -
    - {/* AI model */} - - {(assistant) => { - return ( -
    -
    - - - -
    -
    -
    -
    -
    - - {assistant().providerID} - -
    - - {assistant().modelID} - -
    -
    -
    - ) - }} -
    - - {/* Grep tool */} - - {(_part) => { - const matches = () => - toolData()?.metadata?.matches - const splitArgs = () => { - const { pattern, ...rest } = toolData()?.args - return { pattern, rest } - } - - return ( -
    -
    - - - -
    -
    -
    -
    -
    - Grep - - “{splitArgs().pattern}” - -
    - 0 - } - > -
    - - {([name, value]) => ( - <> -
    -
    {name}
    -
    {value}
    - - )} -
    -
    -
    - - 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 - - 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.type} - -
    - -
    -
    -
    -
    -
    - ) + return }}
    @@ -1934,19 +402,11 @@ export default function Share(props: {
  • Input Tokens - {data().tokens.input ? ( - {data().tokens.input} - ) : ( - - )} + {data().tokens.input ? {data().tokens.input} : }
  • Output Tokens - {data().tokens.output ? ( - {data().tokens.output} - ) : ( - - )} + {data().tokens.output ? {data().tokens.output} : }
  • Reasoning Tokens @@ -1972,10 +432,7 @@ export default function Share(props: { "overflow-y": "auto", }} > - 0} - fallback={

    Waiting for messages...

    } - > + 0} fallback={

    Waiting for messages...

    }>
      {(msg) => ( @@ -2003,9 +460,7 @@ export default function Share(props: {
) } + +export function fromV1(v1: Message.Info): MessageV2.Info { + if (v1.role === "assistant") { + const result: MessageV2.Assistant = { + id: v1.id, + sessionID: v1.metadata.sessionID, + role: "assistant", + time: { + created: v1.metadata.time.created, + completed: v1.metadata.time.completed, + }, + cost: v1.metadata.assistant!.cost, + path: v1.metadata.assistant!.path, + summary: v1.metadata.assistant!.summary, + tokens: v1.metadata.assistant!.tokens, + modelID: v1.metadata.assistant!.modelID, + providerID: v1.metadata.assistant!.providerID, + system: v1.metadata.assistant!.system, + error: v1.metadata.error, + parts: v1.parts.flatMap((part): MessageV2.AssistantPart[] => { + if (part.type === "text") { + return [ + { + type: "text", + text: part.text, + }, + ] + } + if (part.type === "step-start") { + return [ + { + type: "step-start", + }, + ] + } + if (part.type === "tool-invocation") { + return [ + { + type: "tool", + id: part.toolInvocation.toolCallId, + tool: part.toolInvocation.toolName, + state: (() => { + if (part.toolInvocation.state === "partial-call") { + return { + status: "pending", + } + } + + const { title, time, ...metadata } = v1.metadata.tool[part.toolInvocation.toolCallId] + if (part.toolInvocation.state === "call") { + return { + status: "running", + input: part.toolInvocation.args, + time: { + start: time.start, + }, + } + } + + if (part.toolInvocation.state === "result") { + return { + status: "completed", + input: part.toolInvocation.args, + output: part.toolInvocation.result, + title, + time, + metadata, + } + } + throw new Error("unknown tool invocation state") + })(), + }, + ] + } + return [] + }), + } + return result + } + + if (v1.role === "user") { + const result: MessageV2.User = { + id: v1.id, + sessionID: v1.metadata.sessionID, + role: "user", + time: { + created: v1.metadata.time.created, + }, + parts: v1.parts.flatMap((part): MessageV2.UserPart[] => { + if (part.type === "text") { + return [ + { + type: "text", + text: part.text, + }, + ] + } + if (part.type === "file") { + return [ + { + type: "file", + mime: part.mediaType, + filename: part.filename, + url: part.url, + }, + ] + } + return [] + }), + } + return result + } + + throw new Error("unknown message type") +} diff --git a/packages/web/src/components/codeblock.module.css b/packages/web/src/components/codeblock.module.css index ddd88ef18..531201201 100644 --- a/packages/web/src/components/codeblock.module.css +++ b/packages/web/src/components/codeblock.module.css @@ -8,4 +8,3 @@ } } } - diff --git a/packages/web/src/components/diffview.module.css b/packages/web/src/components/diffview.module.css deleted file mode 100644 index a748c5d02..000000000 --- a/packages/web/src/components/diffview.module.css +++ /dev/null @@ -1,121 +0,0 @@ -.diff { - display: flex; - flex-direction: column; - border: 1px solid var(--sl-color-divider); - background-color: var(--sl-color-bg-surface); - border-radius: 0.25rem; -} - -.desktopView { - display: block; -} - -.mobileView { - display: none; -} - -.mobileBlock { - display: flex; - flex-direction: column; -} - -.row { - display: grid; - grid-template-columns: 1fr 1fr; - align-items: stretch; -} - -.beforeColumn, -.afterColumn { - display: flex; - flex-direction: column; - overflow-x: visible; - min-width: 0; - align-items: stretch; -} - -.beforeColumn { - border-right: 1px solid var(--sl-color-divider); -} - -.diff > .row:first-child [data-section="cell"]:first-child { - padding-top: 0.5rem; -} - -.diff > .row:last-child [data-section="cell"]:last-child { - padding-bottom: 0.5rem; -} - -[data-section="cell"] { - position: relative; - flex: 1; - display: flex; - flex-direction: column; - - width: 100%; - padding: 0.1875rem 0.5rem 0.1875rem 2.2ch; - margin: 0; - - &[data-display-mobile="true"] { - display: none; - } - - pre { - --shiki-dark-bg: var(--sl-color-bg-surface) !important; - background-color: var(--sl-color-bg-surface) !important; - - white-space: pre-wrap; - word-break: break-word; - - code > span:empty::before { - content: "\00a0"; - white-space: pre; - display: inline-block; - width: 0; - } - } -} - -[data-diff-type="removed"] { - background-color: var(--sl-color-red-low); - - pre { - --shiki-dark-bg: var(--sl-color-red-low) !important; - background-color: var(--sl-color-red-low) !important; - } - - &::before { - content: "-"; - position: absolute; - left: 0.5ch; - user-select: none; - color: var(--sl-color-red-high); - } -} - -[data-diff-type="added"] { - background-color: var(--sl-color-green-low); - - pre { - --shiki-dark-bg: var(--sl-color-green-low) !important; - background-color: var(--sl-color-green-low) !important; - } - - &::before { - content: "+"; - position: absolute; - left: 0.6ch; - user-select: none; - color: var(--sl-color-green-high); - } -} - -@media (max-width: 40rem) { - .desktopView { - display: none; - } - - .mobileView { - display: block; - } -} diff --git a/packages/web/src/components/icons/custom.tsx b/packages/web/src/components/icons/custom.tsx index b4e32d0cf..be1e2b4db 100644 --- a/packages/web/src/components/icons/custom.tsx +++ b/packages/web/src/components/icons/custom.tsx @@ -39,7 +39,12 @@ export function IconGemini(props: JSX.SvgSVGAttributes) { export function IconOpencode(props: JSX.SvgSVGAttributes) { return ( - + ) diff --git a/packages/web/src/components/icons/index.tsx b/packages/web/src/components/icons/index.tsx index a788d8f47..62445611f 100644 --- a/packages/web/src/components/icons/index.tsx +++ b/packages/web/src/components/icons/index.tsx @@ -3,12 +3,7 @@ import { type JSX } from "solid-js" export function IconAcademicCap(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconAdjustmentsHorizontal( - props: JSX.SvgSVGAttributes, -) { +export function IconAdjustmentsHorizontal(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconAdjustmentsVertical( - props: JSX.SvgSVGAttributes, -) { +export function IconAdjustmentsVertical(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArchiveBoxArrowDown( - props: JSX.SvgSVGAttributes, -) { +export function IconArchiveBoxArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArchiveBoxXMark( - props: JSX.SvgSVGAttributes, -) { +export function IconArchiveBoxXMark(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconArrowDownCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowDownCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconArrowDownOnSquareStack( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowDownOnSquareStack(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowDownOnSquare( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowDownOnSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconArrowDownTray(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowLeftCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowLeftCircle(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowLeftOnRectangle( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowLeftOnRectangle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconArrowLongDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowLongLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowLongRight(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowLongUp(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowPathRoundedSquare( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowPathRoundedSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconArrowRightCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowRightCircle(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowRightOnRectangle( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowRightOnRectangle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconArrowSmallDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowSmallLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowSmallRight( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowSmallRight(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconArrowTopRightOnSquare( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowTopRightOnSquare(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowTrendingDown( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowTrendingDown(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowTrendingUp( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowTrendingUp(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconArrowUpLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowUpOnSquareStack( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowUpOnSquareStack(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowUpOnSquare( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowUpOnSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconArrowUpTray(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUturnDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconArrowUturnLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconArrowUturnRight( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowUturnRight(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconArrowsPointingIn( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowsPointingIn(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowsPointingOut( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowsPointingOut(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconArrowsRightLeft( - props: JSX.SvgSVGAttributes, -) { +export function IconArrowsRightLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconAtSymbol(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBackspace(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBackward(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBanknotes(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBars2(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconBars3BottomLeft( - props: JSX.SvgSVGAttributes, -) { +export function IconBars3BottomLeft(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconBars3BottomRight( - props: JSX.SvgSVGAttributes, -) { +export function IconBars3BottomRight(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconBars3CenterLeft( - props: JSX.SvgSVGAttributes, -) { +export function IconBars3CenterLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconBars4(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBarsArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBarsArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBattery0(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBattery100(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBattery50(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBeaker(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBellAlert(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBellSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBellSnooze(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBell(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBoltSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBolt(props: JSX.SvgSVGAttributes) { return ( - + ) { export function IconBoltSolid(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookmarkSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookmarkSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBookmark(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBriefcase(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconBugAnt(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconBuildingLibrary( - props: JSX.SvgSVGAttributes, -) { +export function IconBuildingLibrary(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconBuildingOffice2( - props: JSX.SvgSVGAttributes, -) { +export function IconBuildingOffice2(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconBuildingStorefront( - props: JSX.SvgSVGAttributes, -) { +export function IconBuildingStorefront(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconCalculator(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCalendarDays(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCalendar(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCamera(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChartBarSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChartBar(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChartPie(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconChatBubbleBottomCenterText( - props: JSX.SvgSVGAttributes, -) { +export function IconChatBubbleBottomCenterText(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChatBubbleBottomCenter( - props: JSX.SvgSVGAttributes, -) { +export function IconChatBubbleBottomCenter(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChatBubbleLeftEllipsis( - props: JSX.SvgSVGAttributes, -) { +export function IconChatBubbleLeftEllipsis(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChatBubbleLeftRight( - props: JSX.SvgSVGAttributes, -) { +export function IconChatBubbleLeftRight(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconChatBubbleOvalLeftEllipsis( - props: JSX.SvgSVGAttributes, -) { +export function IconChatBubbleOvalLeftEllipsis(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChatBubbleOvalLeft( - props: JSX.SvgSVGAttributes, -) { +export function IconChatBubbleOvalLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconCheckCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCheck(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconChevronDoubleDown( - props: JSX.SvgSVGAttributes, -) { +export function IconChevronDoubleDown(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChevronDoubleLeft( - props: JSX.SvgSVGAttributes, -) { +export function IconChevronDoubleLeft(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChevronDoubleRight( - props: JSX.SvgSVGAttributes, -) { +export function IconChevronDoubleRight(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconChevronDoubleUp( - props: JSX.SvgSVGAttributes, -) { +export function IconChevronDoubleUp(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconChevronLeft(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronRight(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronUpDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconChevronUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCircleStack(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconClipboardDocumentCheck( - props: JSX.SvgSVGAttributes, -) { +export function IconClipboardDocumentCheck(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconClipboardDocumentList( - props: JSX.SvgSVGAttributes, -) { +export function IconClipboardDocumentList(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconClipboardDocument( - props: JSX.SvgSVGAttributes, -) { +export function IconClipboardDocument(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconClock(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCloudArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCloudArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCloud(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCodeBracketSquare( - props: JSX.SvgSVGAttributes, -) { +export function IconCodeBracketSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconCog6Tooth(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCog8Tooth(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCog(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCommandLine(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconComputerDesktop( - props: JSX.SvgSVGAttributes, -) { +export function IconComputerDesktop(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconCreditCard(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCubeTransparent( - props: JSX.SvgSVGAttributes, -) { +export function IconCubeTransparent(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconCurrencyBangladeshi( - props: JSX.SvgSVGAttributes, -) { +export function IconCurrencyBangladeshi(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconCurrencyEuro(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCurrencyPound(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCurrencyRupee(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconCurrencyYen(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconCursorArrowRays( - props: JSX.SvgSVGAttributes, -) { +export function IconCursorArrowRays(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconCursorArrowRipple( - props: JSX.SvgSVGAttributes, -) { +export function IconCursorArrowRipple(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconDevicePhoneMobile( - props: JSX.SvgSVGAttributes, -) { +export function IconDevicePhoneMobile(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconDocumentArrowDown( - props: JSX.SvgSVGAttributes, -) { +export function IconDocumentArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconDocumentArrowUp( - props: JSX.SvgSVGAttributes, -) { +export function IconDocumentArrowUp(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconDocumentChartBar( - props: JSX.SvgSVGAttributes, -) { +export function IconDocumentChartBar(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { ) } -export function IconDocumentDuplicate( - props: JSX.SvgSVGAttributes, -) { +export function IconDocumentDuplicate(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconDocumentMagnifyingGlass( - props: JSX.SvgSVGAttributes, -) { +export function IconDocumentMagnifyingGlass(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconDocumentPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconDocumentText(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconDocument(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconEllipsisHorizontalCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconEllipsisHorizontalCircle(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconEllipsisHorizontal( - props: JSX.SvgSVGAttributes, -) { +export function IconEllipsisHorizontal(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconEllipsisVertical( - props: JSX.SvgSVGAttributes, -) { +export function IconEllipsisVertical(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconEnvelope(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconEnvelopeSolid(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconExclamationCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconExclamationCircle(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconExclamationTriangle( - props: JSX.SvgSVGAttributes, -) { +export function IconExclamationTriangle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconEyeSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconEye(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFaceFrown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFaceSmile(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFilm(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFingerPrint(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFire(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFlag(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconFolderArrowDown( - props: JSX.SvgSVGAttributes, -) { +export function IconFolderArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconFolderOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFolderPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFolder(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconForward(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconFunnel(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGif(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGiftTop(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGift(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGlobeAlt(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconGlobeAmericas(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconGlobeAsiaAustralia( - props: JSX.SvgSVGAttributes, -) { +export function IconGlobeAsiaAustralia(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconGlobeEuropeAfrica( - props: JSX.SvgSVGAttributes, -) { +export function IconGlobeEuropeAfrica(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconHandThumbDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHandThumbUp(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHashtag(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHeart(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHomeModern(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconHome(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconIdentification(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconInboxArrowDown(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconInboxStack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconInbox(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconInformationCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconInformationCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconLanguage(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLifebuoy(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLightBulb(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLink(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconListBullet(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLockClosed(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLockOpen(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconMagnifyingGlassCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconMagnifyingGlassCircle(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconMagnifyingGlassMinus( - props: JSX.SvgSVGAttributes, -) { +export function IconMagnifyingGlassMinus(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconMagnifyingGlassPlus( - props: JSX.SvgSVGAttributes, -) { +export function IconMagnifyingGlassPlus(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconMagnifyingGlass( - props: JSX.SvgSVGAttributes, -) { +export function IconMagnifyingGlass(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconMap(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMegaphone(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMicrophone(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMinusCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMinusSmall(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMinus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMoon(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMusicalNote(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconNewspaper(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconNoSymbol(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPaintBrush(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPaperAirplane(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPaperClip(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPauseCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPause(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPencilSquare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPencil(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconPhoneArrowDownLeft( - props: JSX.SvgSVGAttributes, -) { +export function IconPhoneArrowDownLeft(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconPhoneArrowUpRight( - props: JSX.SvgSVGAttributes, -) { +export function IconPhoneArrowUpRight(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconPhone(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPhoto(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlayCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlayPause(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlay(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlusCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlusSmall(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconPower(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconPresentationChartBar( - props: JSX.SvgSVGAttributes, -) { +export function IconPresentationChartBar(props: JSX.SvgSVGAttributes) { return ( - + ) } -export function IconPresentationChartLine( - props: JSX.SvgSVGAttributes, -) { +export function IconPresentationChartLine(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconPuzzlePiece(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconQrCode(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconQuestionMarkCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconQuestionMarkCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconRadio(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconReceiptPercent(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconReceiptRefund(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRectangleGroup(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRectangleStack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRocketLaunch(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconRss(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconScale(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconScissors(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconServerStack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconServer(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconShare(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconShieldCheck(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconShieldExclamation( - props: JSX.SvgSVGAttributes, -) { +export function IconShieldExclamation(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconShoppingCart(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSignalSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSignal(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSparkles(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSpeakerWave(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSpeakerXMark(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquare2Stack(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquare3Stack3d(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquares2x2(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSquaresPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconStar(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconStopCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconStop(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSun(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSwatch(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTableCells(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTag(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTicket(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTrash(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTrophy(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTruck(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconTv(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserGroup(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserMinus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUserPlus(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUser(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconUsers(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconVariable(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconVideoCameraSlash( - props: JSX.SvgSVGAttributes, -) { +export function IconVideoCameraSlash(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconViewColumns(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconViewfinderCircle( - props: JSX.SvgSVGAttributes, -) { +export function IconViewfinderCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconWifi(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconWindow(props: JSX.SvgSVGAttributes) { return ( - + ) { ) } -export function IconWrenchScrewdriver( - props: JSX.SvgSVGAttributes, -) { +export function IconWrenchScrewdriver(props: JSX.SvgSVGAttributes) { return ( - + ) { return ( - + ) { } export function IconXCircle(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconXMark(props: JSX.SvgSVGAttributes) { return ( - + ) { // index export function IconCommand(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconLetter(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconMultiSelect(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSettings(props: JSX.SvgSVGAttributes) { return ( - + ) { } export function IconSingleSelect(props: JSX.SvgSVGAttributes) { return ( - + *:last-child { - margin-bottom: 0; - } - - pre { - --shiki-dark-bg: var(--sl-color-bg-surface) !important; - background-color: var(--sl-color-bg-surface) !important; - padding: 0.5rem 0.75rem; - line-height: 1.6; - font-size: 0.75rem; - white-space: pre-wrap; - word-break: break-word; - - span { - white-space: break-spaces; - } - } - - code { - font-weight: 500; - - &:not(pre code) { - &::before { - content: "`"; - font-weight: 700; - } - &::after { - content: "`"; - font-weight: 700; - } - } - } - - table { - border-collapse: collapse; - width: 100%; - } - - th, - td { - border: 1px solid var(--sl-color-border); - padding: 0.5rem 0.75rem; - text-align: left; - } - - th { - border-bottom: 1px solid var(--sl-color-border); - } - - /* Remove outer borders */ - table tr:first-child th, - table tr:first-child td { - border-top: none; - } - - table tr:last-child td { - border-bottom: none; - } - - table th:first-child, - table td:first-child { - border-left: none; - } - - table th:last-child, - table td:last-child { - border-right: none; - } -} diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css index c339d2b80..14680736c 100644 --- a/packages/web/src/components/share.module.css +++ b/packages/web/src/components/share.module.css @@ -15,76 +15,42 @@ --lg-tool-width: 56rem; --term-icon: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%2060%2016'%20preserveAspectRatio%3D'xMidYMid%20meet'%3E%3Ccircle%20cx%3D'8'%20cy%3D'8'%20r%3D'8'%2F%3E%3Ccircle%20cx%3D'30'%20cy%3D'8'%20r%3D'8'%2F%3E%3Ccircle%20cx%3D'52'%20cy%3D'8'%20r%3D'8'%2F%3E%3C%2Fsvg%3E"); -} -[data-element-button-text] { - cursor: pointer; - appearance: none; - background-color: transparent; - border: none; - padding: 0; - color: var(--sl-color-text-secondary); - - &:hover { - color: var(--sl-color-text); - } - - &[data-element-button-more] { + [data-component="header"] { display: flex; - align-items: center; - gap: 0.125rem; + flex-direction: column; + gap: 1rem; - span[data-button-icon] { - line-height: 1; - opacity: 0.85; - svg { - display: block; - } + @media (max-width: 30rem) { + gap: 1rem; } } -} - -[data-element-label] { - text-transform: uppercase; - letter-spacing: -0.5px; - color: var(--sl-color-text-dimmed); -} -.header { - display: flex; - flex-direction: column; - gap: 1rem; + [data-component="header-title"] { + font-size: 2.75rem; + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.05em; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; - @media (max-width: 30rem) { - gap: 1rem; - } - - [data-section="title"] { - h1 { - font-size: 2.75rem; - font-weight: 500; - line-height: 1.2; - letter-spacing: -0.05em; - display: -webkit-box; - -webkit-box-orient: vertical; + @media (max-width: 30rem) { + font-size: 1.75rem; + line-height: 1.25; -webkit-line-clamp: 3; - overflow: hidden; - - @media (max-width: 30rem) { - font-size: 1.75rem; - line-height: 1.25; - -webkit-line-clamp: 3; - } } } - [data-section="row"] { + [data-component="header-details"] { display: flex; flex-direction: column; gap: 0.5rem; } - [data-section="stats"] { + [data-component="header-stats"] { list-style-type: none; padding: 0; margin: 0; @@ -92,41 +58,62 @@ gap: 0.5rem 0.875rem; flex-wrap: wrap; - li { + [data-slot="item"] { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.3125rem; font-size: 0.875rem; span[data-placeholder] { color: var(--sl-color-text-dimmed); } } + + [data-slot="icon"] { + flex: 0 0 auto; + color: var(--sl-color-text-dimmed); + opacity: 0.85; + + svg { + display: block; + } + } + + [data-slot="model"] { + color: var(--sl-color-text); + } } - [data-section="stats"] { - li { - gap: 0.3125rem; + [data-component="header-time"] { + color: var(--sl-color-text-dimmed); + font-size: 0.875rem; + } - [data-stat-icon] { - flex: 0 0 auto; - color: var(--sl-color-text-dimmed); + [data-component="text-button"] { + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + padding: 0; + color: var(--sl-color-text-secondary); + + &:hover { + color: var(--sl-color-text); + } + + &[data-element-button-more] { + display: flex; + align-items: center; + gap: 0.125rem; + + span[data-button-icon] { + line-height: 1; opacity: 0.85; + svg { display: block; } } - - span[data-stat-model] { - color: var(--sl-color-text); - } - } - } - - [data-section="time"] { - span { - color: var(--sl-color-text-dimmed); - font-size: 0.875rem; } } } @@ -170,10 +157,12 @@ svg:nth-child(3) { display: none; } + &:hover { svg:nth-child(1) { display: none; } + svg:nth-child(2) { display: block; } @@ -213,12 +202,14 @@ opacity: 1; visibility: visible; } + a, a:hover { svg:nth-child(1), svg:nth-child(2) { display: none; } + svg:nth-child(3) { display: block; } @@ -264,7 +255,7 @@ } b { - color: var(--sl-color-text); + color: var(--sl-color-text); word-break: break-all; font-weight: 500; } @@ -348,8 +339,7 @@ } [data-part-type="tool-grep"] { - &:not(:has([data-part-tool-args])) - > [data-section="content"] > [data-part-tool-body] { + &:not(:has([data-part-tool-args])) > [data-section="content"] > [data-part-tool-body] { gap: 0.5rem; } } @@ -374,6 +364,7 @@ } } } + [data-part-type="summary"] { & > [data-section="decoration"] { span:first-child { @@ -388,15 +379,19 @@ &[data-status="connected"] { background-color: var(--sl-color-green); } + &[data-status="connecting"] { background-color: var(--sl-color-orange); } + &[data-status="disconnected"] { background-color: var(--sl-color-divider); } + &[data-status="reconnecting"] { background-color: var(--sl-color-orange); } + &[data-status="error"] { background-color: var(--sl-color-red); } @@ -493,14 +488,20 @@ } } - &[data-background="none"] { background-color: transparent; } - &[data-background="blue"] { background-color: var(--sl-color-blue-low); } + &[data-background="none"] { + background-color: transparent; + } + + &[data-background="blue"] { + background-color: var(--sl-color-blue-low); + } &[data-expanded="true"] { pre { display: block; } } + &[data-expanded="false"] { pre { display: -webkit-box; @@ -536,20 +537,25 @@ span { margin-right: 0.25rem; + &:last-child { margin-right: 0; } } + span[data-color="red"] { color: var(--sl-color-red); } + span[data-color="dimmed"] { color: var(--sl-color-text-dimmed); } + span[data-marker="label"] { text-transform: uppercase; letter-spacing: -0.5px; } + span[data-separator] { margin-right: 0.375rem; } @@ -561,6 +567,7 @@ display: block; } } + &[data-expanded="false"] { [data-section="content"] { display: -webkit-box; @@ -575,7 +582,6 @@ padding: 2px 0; font-size: 0.75rem; } - } .message-terminal { @@ -611,7 +617,7 @@ } &::before { - content: ''; + content: ""; position: absolute; pointer-events: none; top: 8px; @@ -651,6 +657,7 @@ display: block; } } + &[data-expanded="false"] { pre { display: -webkit-box; @@ -693,6 +700,7 @@ display: block; } } + &[data-expanded="false"] { [data-element-markdown] { display: -webkit-box; @@ -750,10 +758,14 @@ &[data-status="pending"] { color: var(--sl-color-text); } + &[data-status="in_progress"] { color: var(--sl-color-text); - & > span { border-color: var(--sl-color-orange); } + & > span { + border-color: var(--sl-color-orange); + } + & > span::before { content: ""; position: absolute; @@ -764,10 +776,14 @@ box-shadow: inset 1rem 1rem var(--sl-color-orange-low); } } + &[data-status="completed"] { color: var(--sl-color-text-secondary); - & > span { border-color: var(--sl-color-green-low); } + & > span { + border-color: var(--sl-color-green-low); + } + & > span::before { content: ""; position: absolute; @@ -798,7 +814,9 @@ display: flex; align-items: center; justify-content: center; - transition: all 0.15s ease, opacity 0.5s ease; + transition: + all 0.15s ease, + opacity 0.5s ease; z-index: 100; appearance: none; opacity: 1; diff --git a/packages/web/src/components/share/common.tsx b/packages/web/src/components/share/common.tsx new file mode 100644 index 000000000..9f5221de9 --- /dev/null +++ b/packages/web/src/components/share/common.tsx @@ -0,0 +1,60 @@ +import { createSignal, onCleanup, splitProps } from "solid-js" +import type { JSX } from "solid-js/jsx-runtime" +import { IconCheckCircle, IconHashtag } from "../icons" + +interface AnchorProps extends JSX.HTMLAttributes { + id: string +} +export function AnchorIcon(props: AnchorProps) { + const [local, rest] = splitProps(props, ["id", "children"]) + const [copied, setCopied] = createSignal(false) + + return ( + + ) +} + +export function createOverflow() { + const [overflow, setOverflow] = createSignal(false) + return { + get status() { + return overflow() + }, + ref(el: HTMLElement) { + const ro = new ResizeObserver(() => { + if (el.scrollHeight > el.clientHeight + 1) { + setOverflow(true) + } + return + }) + ro.observe(el) + + onCleanup(() => { + ro.disconnect() + }) + }, + } +} diff --git a/packages/web/src/components/share/content-code.module.css b/packages/web/src/components/share/content-code.module.css new file mode 100644 index 000000000..b95f936da --- /dev/null +++ b/packages/web/src/components/share/content-code.module.css @@ -0,0 +1,25 @@ +.root { + max-width: var(--md-tool-width); + border: 1px solid var(--sl-color-divider); + background-color: var(--sl-color-bg-surface); + border-radius: 0.25rem; + padding: 0.5rem calc(0.5rem + 3px); + + &[data-flush="true"] { + border: none; + background-color: transparent; + padding: 0; + } + + pre { + --shiki-dark-bg: var(--sl-color-bg-surface) !important; + line-height: 1.6; + font-size: 0.75rem; + white-space: pre-wrap; + word-break: break-word; + + span { + white-space: break-spaces; + } + } +} diff --git a/packages/web/src/components/share/content-code.tsx b/packages/web/src/components/share/content-code.tsx new file mode 100644 index 000000000..b8c4f2ccd --- /dev/null +++ b/packages/web/src/components/share/content-code.tsx @@ -0,0 +1,32 @@ +import { type JSX, splitProps, createResource, Suspense } from "solid-js" +import { codeToHtml } from "shiki" +import style from "./content-code.module.css" +import { transformerNotationDiff } from "@shikijs/transformers" + +interface Props { + code: string + lang?: string + flush?: boolean +} +export function ContentCode(props: Props) { + const [html] = createResource( + () => [props.code, props.lang], + async ([code, lang]) => { + // TODO: For testing delays + // await new Promise((resolve) => setTimeout(resolve, 3000)) + return (await codeToHtml(code || "", { + lang: lang || "text", + themes: { + light: "github-light", + dark: "github-dark", + }, + transformers: [transformerNotationDiff()], + })) as string + }, + ) + return ( + +
+ + ) +} diff --git a/packages/web/src/components/share/content-diff.module.css b/packages/web/src/components/share/content-diff.module.css new file mode 100644 index 000000000..718ae3690 --- /dev/null +++ b/packages/web/src/components/share/content-diff.module.css @@ -0,0 +1,125 @@ +.root { + display: flex; + flex-direction: column; + border: 1px solid var(--sl-color-divider); + background-color: var(--sl-color-bg-surface); + border-radius: 0.25rem; + + [data-component="desktop"] { + display: block; + } + + [data-component="mobile"] { + display: none; + } + + [data-component="diff-block"] { + display: flex; + flex-direction: column; + } + + [data-component="diff-row"] { + display: grid; + grid-template-columns: 1fr 1fr; + align-items: stretch; + + [data-slot="before"], + [data-slot="after"] { + position: relative; + display: flex; + flex-direction: column; + overflow-x: visible; + min-width: 0; + align-items: stretch; + padding: 0 1rem; + + &[data-diff-type="removed"] { + background-color: var(--sl-color-red-low); + + pre { + --shiki-dark-bg: var(--sl-color-red-low) !important; + background-color: var(--sl-color-red-low) !important; + } + + &::before { + content: "-"; + position: absolute; + left: 0.5ch; + top: 1px; + user-select: none; + color: var(--sl-color-red-high); + } + } + + &[data-diff-type="added"] { + background-color: var(--sl-color-green-low); + + pre { + --shiki-dark-bg: var(--sl-color-green-low) !important; + background-color: var(--sl-color-green-low) !important; + } + + &::before { + content: "+"; + position: absolute; + user-select: none; + color: var(--sl-color-green-high); + left: 0.5ch; + top: 1px; + } + } + } + + [data-slot="before"] { + border-right: 1px solid var(--sl-color-divider); + } + } + + .diff > .row:first-child [data-section="cell"]:first-child { + padding-top: 0.5rem; + } + + .diff > .row:last-child [data-section="cell"]:last-child { + padding-bottom: 0.5rem; + } + + [data-section="cell"] { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + + width: 100%; + padding: 0.1875rem 0.5rem 0.1875rem 2.2ch; + margin: 0; + + &[data-display-mobile="true"] { + display: none; + } + + pre { + --shiki-dark-bg: var(--sl-color-bg-surface) !important; + background-color: var(--sl-color-bg-surface) !important; + + white-space: pre-wrap; + word-break: break-word; + + code > span:empty::before { + content: "\00a0"; + white-space: pre; + display: inline-block; + width: 0; + } + } + } + + @media (max-width: 40rem) { + [data-slot="desktop"] { + display: none; + } + + [data-slot="mobile"] { + display: block; + } + } +} diff --git a/packages/web/src/components/share/content-diff.tsx b/packages/web/src/components/share/content-diff.tsx new file mode 100644 index 000000000..894145c34 --- /dev/null +++ b/packages/web/src/components/share/content-diff.tsx @@ -0,0 +1,231 @@ +import { type Component, createMemo } from "solid-js" +import { parsePatch } from "diff" +import { ContentCode } from "./content-code" +import styles from "./content-diff.module.css" + +type DiffRow = { + left: string + right: string + type: "added" | "removed" | "unchanged" | "modified" +} + +interface Props { + diff: string + lang?: string +} + +export function ContentDiff(props: Props) { + const rows = createMemo(() => { + const diffRows: DiffRow[] = [] + + try { + const patches = parsePatch(props.diff) + + for (const patch of patches) { + for (const hunk of patch.hunks) { + const lines = hunk.lines + let i = 0 + + while (i < lines.length) { + const line = lines[i] + const content = line.slice(1) + const prefix = line[0] + + if (prefix === "-") { + // Look ahead for consecutive additions to pair with removals + const removals: string[] = [content] + let j = i + 1 + + // Collect all consecutive removals + while (j < lines.length && lines[j][0] === "-") { + removals.push(lines[j].slice(1)) + j++ + } + + // Collect all consecutive additions that follow + const additions: string[] = [] + while (j < lines.length && lines[j][0] === "+") { + additions.push(lines[j].slice(1)) + j++ + } + + // Pair removals with additions + const maxLength = Math.max(removals.length, additions.length) + for (let k = 0; k < maxLength; k++) { + const hasLeft = k < removals.length + const hasRight = k < additions.length + + if (hasLeft && hasRight) { + // Replacement - left is removed, right is added + diffRows.push({ + left: removals[k], + right: additions[k], + type: "modified", + }) + } else if (hasLeft) { + // Pure removal + diffRows.push({ + left: removals[k], + right: "", + type: "removed", + }) + } else if (hasRight) { + // Pure addition - only create if we actually have content + diffRows.push({ + left: "", + right: additions[k], + type: "added", + }) + } + } + + i = j + } else if (prefix === "+") { + // Standalone addition (not paired with removal) + diffRows.push({ + left: "", + right: content, + type: "added", + }) + i++ + } else if (prefix === " ") { + diffRows.push({ + left: content, + right: content, + type: "unchanged", + }) + i++ + } else { + i++ + } + } + } + } + } catch (error) { + console.error("Failed to parse patch:", error) + return [] + } + + return diffRows + }) + + const mobileRows = createMemo(() => { + const mobileBlocks: { type: "removed" | "added" | "unchanged"; lines: string[] }[] = [] + const currentRows = rows() + + let i = 0 + while (i < currentRows.length) { + const removedLines: string[] = [] + const addedLines: string[] = [] + + // Collect consecutive modified/removed/added rows + while ( + i < currentRows.length && + (currentRows[i].type === "modified" || currentRows[i].type === "removed" || currentRows[i].type === "added") + ) { + const row = currentRows[i] + if (row.left && (row.type === "removed" || row.type === "modified")) { + removedLines.push(row.left) + } + if (row.right && (row.type === "added" || row.type === "modified")) { + addedLines.push(row.right) + } + i++ + } + + // Add grouped blocks + if (removedLines.length > 0) { + mobileBlocks.push({ type: "removed", lines: removedLines }) + } + if (addedLines.length > 0) { + mobileBlocks.push({ type: "added", lines: addedLines }) + } + + // Add unchanged rows as-is + if (i < currentRows.length && currentRows[i].type === "unchanged") { + mobileBlocks.push({ + type: "unchanged", + lines: [currentRows[i].left], + }) + i++ + } + } + + return mobileBlocks + }) + + return ( +
+
+ {rows().map((r) => ( +
+
+ +
+
+ +
+
+ ))} +
+ +
+ {mobileRows().map((block) => ( +
+ {block.lines.map((line) => ( + + ))} +
+ ))} +
+
+ ) +} + +// const testDiff = `--- combined_before.txt 2025-06-24 16:38:08 +// +++ combined_after.txt 2025-06-24 16:38:12 +// @@ -1,21 +1,25 @@ +// unchanged line +// -deleted line +// -old content +// +added line +// +new content +// +// -removed empty line below +// +added empty line above +// +// - tab indented +// -trailing spaces +// -very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view +// -unicode content: πŸš€ ✨ δΈ­ζ–‡ +// -mixed content with tabs and spaces +// + space indented +// +no trailing spaces +// +short line +// +very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals +// +different unicode: πŸŽ‰ πŸ’» ζ—₯本θͺž +// +normalized content with consistent spacing +// +newline to content +// +// -content to remove +// -whitespace only: +// -multiple +// -consecutive +// -deletions +// -single deletion +// + +// +single addition +// +first addition +// +second addition +// +third addition +// line before addition +// +first added line +// + +// +third added line +// line after addition +// final unchanged line` diff --git a/packages/web/src/components/share/content-markdown.module.css b/packages/web/src/components/share/content-markdown.module.css new file mode 100644 index 000000000..da1aa1128 --- /dev/null +++ b/packages/web/src/components/share/content-markdown.module.css @@ -0,0 +1,140 @@ +.root { + border: 1px solid var(--sl-color-blue-high); + padding: 0.5rem calc(0.5rem + 3px); + border-radius: 0.25rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1rem; + align-self: flex-start; + max-width: var(--md-tool-width); + + &[data-highlight="true"] { + background-color: var(--sl-color-blue-low); + } + + [data-slot="expand-button"] { + flex: 0 0 auto; + padding: 2px 0; + font-size: 0.75rem; + } + + [data-slot="markdown"] { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; + + [data-expanded] & { + display: block; + } + + font-size: 0.875rem; + line-height: 1.5; + + p, + blockquote, + ul, + ol, + dl, + table, + pre { + margin-bottom: 1rem; + } + + strong { + font-weight: 600; + } + + ol { + list-style-position: inside; + padding-left: 0.75rem; + } + + ul { + padding-left: 1.5rem; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: 0.875rem; + font-weight: 600; + margin-bottom: 0.5rem; + } + + & > *:last-child { + margin-bottom: 0; + } + + pre { + --shiki-dark-bg: var(--sl-color-bg-surface) !important; + background-color: var(--sl-color-bg-surface) !important; + padding: 0.5rem 0.75rem; + line-height: 1.6; + font-size: 0.75rem; + white-space: pre-wrap; + word-break: break-word; + + span { + white-space: break-spaces; + } + } + + code { + font-weight: 500; + + &:not(pre code) { + &::before { + content: "`"; + font-weight: 700; + } + + &::after { + content: "`"; + font-weight: 700; + } + } + } + + table { + border-collapse: collapse; + width: 100%; + } + + th, + td { + border: 1px solid var(--sl-color-border); + padding: 0.5rem 0.75rem; + text-align: left; + } + + th { + border-bottom: 1px solid var(--sl-color-border); + } + + /* Remove outer borders */ + table tr:first-child th, + table tr:first-child td { + border-top: none; + } + + table tr:last-child td { + border-bottom: none; + } + + table th:first-child, + table td:first-child { + border-left: none; + } + + table th:last-child, + table td:last-child { + border-right: none; + } + } +} diff --git a/packages/web/src/components/share/content-markdown.tsx b/packages/web/src/components/share/content-markdown.tsx new file mode 100644 index 000000000..f79271296 --- /dev/null +++ b/packages/web/src/components/share/content-markdown.tsx @@ -0,0 +1,65 @@ +import style from "./content-markdown.module.css" +import { createResource, createSignal } from "solid-js" +import { createOverflow } from "./common" +import { transformerNotationDiff } from "@shikijs/transformers" +import { marked } from "marked" +import markedShiki from "marked-shiki" +import { codeToHtml } from "shiki" + +const markedWithShiki = marked.use( + markedShiki({ + highlight(code, lang) { + return codeToHtml(code, { + lang: lang || "text", + themes: { + light: "github-light", + dark: "github-dark", + }, + transformers: [transformerNotationDiff()], + }) + }, + }), +) + +interface Props { + text: string + expand?: boolean + highlight?: boolean +} +export function ContentMarkdown(props: Props) { + const [html] = createResource( + () => strip(props.text), + async (markdown) => { + return markedWithShiki.parse(markdown) + }, + ) + const [expanded, setExpanded] = createSignal(false) + const overflow = createOverflow() + + return ( +
+
+ + {!props.expand && overflow.status && ( + + )} +
+ ) +} + +function strip(text: string): string { + const wrappedRe = /^\s*<([A-Za-z]\w*)>\s*([\s\S]*?)\s*<\/\1>\s*$/ + const match = text.match(wrappedRe) + return match ? match[2] : text +} diff --git a/packages/web/src/components/share/content-text.module.css b/packages/web/src/components/share/content-text.module.css new file mode 100644 index 000000000..f8d0b0b93 --- /dev/null +++ b/packages/web/src/components/share/content-text.module.css @@ -0,0 +1,57 @@ +.root { + color: var(--sl-color-text); + background-color: var(--sl-color-bg-surface); + padding: 0.5rem calc(0.5rem + 3px); + border-radius: 0.25rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1rem; + align-self: flex-start; + max-width: var(--md-tool-width); + font-size: 0.875rem; + + &[data-compact] { + font-size: 0.75rem; + color: var(--sl-color-text-dimmed); + } + + [data-slot="text"] { + line-height: 1.5; + white-space: pre-wrap; + overflow-wrap: anywhere; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + line-clamp: 3; + overflow: hidden; + + [data-expanded] & { + display: block; + } + } + + [data-slot="expand-button"] { + flex: 0 0 auto; + padding: 2px 0; + font-size: 0.75rem; + } + + &[data-theme="invert"] { + background-color: var(--sl-color-blue-high); + color: var(--sl-color-text-invert); + + [data-slot="expand-button"] { + opacity: 0.85; + color: var(--sl-color-text-invert); + + &:hover { + opacity: 1; + } + } + } + + &[data-theme="blue"] { + background-color: var(--sl-color-blue-low); + } +} diff --git a/packages/web/src/components/share/content-text.tsx b/packages/web/src/components/share/content-text.tsx new file mode 100644 index 000000000..c52e0dfcc --- /dev/null +++ b/packages/web/src/components/share/content-text.tsx @@ -0,0 +1,35 @@ +import style from "./content-text.module.css" +import { createSignal } from "solid-js" +import { createOverflow } from "./common" + +interface Props { + text: string + expand?: boolean + compact?: boolean +} +export function ContentText(props: Props) { + const [expanded, setExpanded] = createSignal(false) + const overflow = createOverflow() + + return ( +
+
+        {props.text}
+      
+ {((!props.expand && overflow.status) || expanded()) && ( + + )} +
+ ) +} diff --git a/packages/web/src/components/share/part.module.css b/packages/web/src/components/share/part.module.css new file mode 100644 index 000000000..9145cddf7 --- /dev/null +++ b/packages/web/src/components/share/part.module.css @@ -0,0 +1,375 @@ +.root { + display: flex; + gap: 0.625rem; + + [data-component="decoration"] { + flex: 0 0 auto; + display: flex; + flex-direction: column; + gap: 0.625rem; + align-items: center; + justify-content: flex-start; + + [data-slot="anchor"] { + position: relative; + + a:first-child { + display: block; + flex: 0 0 auto; + width: 18px; + opacity: 0.65; + + svg { + color: var(--sl-color-text-secondary); + display: block; + + &:nth-child(3) { + color: var(--sl-color-green-high); + } + } + + svg:nth-child(2), + svg:nth-child(3) { + display: none; + } + + &:hover { + svg:nth-child(1) { + display: none; + } + + svg:nth-child(2) { + display: block; + } + } + } + + [data-copied] & { + a, + a:hover { + svg:nth-child(1), + svg:nth-child(2) { + display: none; + } + + svg:nth-child(3) { + display: block; + } + } + } + } + + [data-slot="bar"] { + width: 3px; + height: 100%; + border-radius: 1px; + background-color: var(--sl-color-hairline); + } + + [data-slot="tooltip"] { + position: absolute; + top: 50%; + left: calc(100% + 12px); + transform: translate(0, -50%); + line-height: 1.1; + padding: 0.375em 0.5em calc(0.375em + 2px); + background: var(--sl-color-white); + color: var(--sl-color-text-invert); + font-size: 0.6875rem; + border-radius: 7px; + white-space: nowrap; + + z-index: 1; + opacity: 0; + visibility: hidden; + + &::after { + content: ""; + position: absolute; + top: 50%; + left: -15px; + transform: translateY(-50%); + border: 8px solid transparent; + border-right-color: var(--sl-color-white); + } + + [data-copied] & { + opacity: 1; + visibility: visible; + } + } + } + + [data-component="content"] { + display: flex; + flex-direction: column; + gap: 1rem; + flex-grow: 1; + } + + [data-component="spacer"] { + height: 0rem; + } + + [data-component="content-footer"] { + align-self: flex-start; + font-size: 0.75rem; + color: var(--sl-color-text-dimmed); + } + + [data-component="step-start"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.375rem; + padding-bottom: 1rem; + + [data-slot="provider"] { + line-height: 18px; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: -0.5px; + color: var(--sl-color-text-secondary); + } + + [data-slot="model"] { + line-height: 1.5; + } + } + + [data-component="button-text"] { + cursor: pointer; + appearance: none; + background-color: transparent; + border: none; + padding: 0; + color: var(--sl-color-text-secondary); + font-size: 0.75rem; + + &:hover { + color: var(--sl-color-text); + } + + &[data-more] { + display: flex; + align-items: center; + gap: 0.125rem; + + span[data-slot="icon"] { + line-height: 1; + opacity: 0.85; + + svg { + display: block; + } + } + } + } + + [data-component="tool"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.375rem; + padding-bottom: 1rem; + } + + [data-component="tool-title"] { + line-height: 18px; + font-size: 0.875rem; + color: var(--sl-color-text-secondary); + max-width: var(--md-tool-width); + display: flex; + align-items: flex-start; + gap: 0.375rem; + + [data-slot="name"] { + text-transform: uppercase; + letter-spacing: -0.5px; + } + + [data-slot="target"] { + color: var(--sl-color-text); + word-break: break-all; + font-weight: 500; + } + } + + [data-component="tool-result"] { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + [data-component="todos"] { + list-style-type: none; + padding: 0; + margin: 0; + width: 100%; + max-width: var(--sm-tool-width); + border: 1px solid var(--sl-color-divider); + border-radius: 0.25rem; + + [data-slot="item"] { + margin: 0; + position: relative; + padding-left: 1.5rem; + font-size: 0.75rem; + padding: 0.375rem 0.625rem 0.375rem 1.75rem; + border-bottom: 1px solid var(--sl-color-divider); + line-height: 1.5; + word-break: break-word; + + &:last-child { + border-bottom: none; + } + + & > span { + position: absolute; + display: inline-block; + left: 0.5rem; + top: calc(0.5rem + 1px); + width: 0.75rem; + height: 0.75rem; + border: 1px solid var(--sl-color-divider); + border-radius: 0.15rem; + + &::before { + } + } + + &[data-status="pending"] { + color: var(--sl-color-text); + } + + &[data-status="in_progress"] { + color: var(--sl-color-text); + + & > span { + border-color: var(--sl-color-orange); + } + + & > span::before { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: calc(0.75rem - 2px - 4px); + height: calc(0.75rem - 2px - 4px); + box-shadow: inset 1rem 1rem var(--sl-color-orange-low); + } + } + + &[data-status="completed"] { + color: var(--sl-color-text-secondary); + + & > span { + border-color: var(--sl-color-green-low); + } + + & > span::before { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: calc(0.75rem - 2px - 4px); + height: calc(0.75rem - 2px - 4px); + box-shadow: inset 1rem 1rem var(--sl-color-green); + + transform-origin: bottom left; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); + } + } + } + } + + [data-component="terminal"] { + width: 100%; + max-width: var(--sm-tool-width); + + [data-slot="body"] { + display: flex; + flex-direction: column; + border: 1px solid var(--sl-color-divider); + border-radius: 0.25rem; + overflow: hidden; + } + + [data-slot="header"] { + position: relative; + border-bottom: 1px solid var(--sl-color-divider); + width: 100%; + height: 1.625rem; + text-align: center; + padding: 0 3.25rem; + + > span { + max-width: min(100%, 140ch); + display: inline-block; + white-space: nowrap; + overflow: hidden; + line-height: 1.625rem; + font-size: 0.75rem; + text-overflow: ellipsis; + color: var(--sl-color-text-dimmed); + } + + &::before { + content: ""; + position: absolute; + pointer-events: none; + top: 8px; + left: 10px; + width: 2rem; + height: 0.5rem; + line-height: 0; + background-color: var(--sl-color-hairline); + mask-image: var(--term-icon); + mask-repeat: no-repeat; + } + } + + [data-slot="content"] { + display: flex; + flex-direction: column; + padding: 0.5rem calc(0.5rem + 3px); + + pre { + --shiki-dark-bg: var(--sl-color-bg) !important; + background-color: var(--sl-color-bg) !important; + line-height: 1.6; + font-size: 0.75rem; + white-space: pre-wrap; + word-break: break-word; + } + } + } + + [data-component="tool-args"] { + display: inline-grid; + align-items: center; + grid-template-columns: max-content max-content minmax(0, 1fr); + max-width: var(--md-tool-width); + gap: 0.25rem 0.375rem; + + & > div:nth-child(3n + 1) { + width: 8px; + height: 2px; + border-radius: 1px; + background: var(--sl-color-divider); + } + + & > div:nth-child(3n + 2), + & > div:nth-child(3n + 3) { + font-size: 0.75rem; + line-height: 1.5; + } + + & > div:nth-child(3n + 3) { + padding-left: 0.125rem; + word-break: break-word; + color: var(--sl-color-text-secondary); + } + } +} diff --git a/packages/web/src/components/share/part.tsx b/packages/web/src/components/share/part.tsx new file mode 100644 index 000000000..3ee2c61a3 --- /dev/null +++ b/packages/web/src/components/share/part.tsx @@ -0,0 +1,664 @@ +import { createMemo, createSignal, For, Match, Show, Switch, type JSX, type ParentProps } from "solid-js" +import { + IconCheckCircle, + IconChevronDown, + IconChevronRight, + IconHashtag, + IconSparkles, + IconGlobeAlt, + IconDocument, + IconQueueList, + IconCommandLine, + IconDocumentPlus, + IconPencilSquare, + IconRectangleStack, + IconMagnifyingGlass, + IconDocumentMagnifyingGlass, +} from "../icons" +import styles from "./part.module.css" +import type { MessageV2 } from "opencode/session/message-v2" +import { ContentText } from "./content-text" +import { ContentMarkdown } from "./content-markdown" +import { DateTime } from "luxon" +import CodeBlock from "../CodeBlock" +import map from "lang-map" +import type { Diagnostic } from "vscode-languageserver-types" + +import { ContentCode } from "./content-code" +import { ContentDiff } from "./content-diff" + +export interface PartProps { + index: number + message: MessageV2.Info + part: MessageV2.AssistantPart | MessageV2.UserPart + last: boolean +} + +export function Part(props: PartProps) { + const [copied, setCopied] = createSignal(false) + const id = createMemo(() => props.message.id + "-" + props.index) + + return ( +
+ +
+ {props.message.role === "user" && props.part.type === "text" && ( + <> + + + )} + {props.message.role === "assistant" && props.part.type === "text" && ( + <> + + {props.last && props.message.role === "assistant" && props.message.time.completed && ( +
+ {DateTime.fromMillis(props.message.time.completed).toLocaleString(DateTime.DATETIME_MED)} +
+ )} + + + )} + {props.part.type === "step-start" && props.message.role === "assistant" && ( +
+
{props.message.providerID}
+
{props.message.modelID}
+
+ )} + {props.part.type === "tool" && + props.part.state.status === "completed" && + props.message.role === "assistant" && ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ )} +
+
+ ) +} + +type ToolProps = { + id: MessageV2.ToolPart["id"] + tool: MessageV2.ToolPart["tool"] + state: MessageV2.ToolStateCompleted + message: MessageV2.Assistant + isLastPart?: boolean +} + +interface Todo { + id: string + content: string + status: "pending" | "in_progress" | "completed" + priority: "low" | "medium" | "high" +} + +function stripWorkingDirectory(filePath?: string, workingDir?: string) { + if (filePath === undefined || workingDir === undefined) return filePath + + const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/" + + if (filePath === workingDir) { + return "" + } + + if (filePath.startsWith(prefix)) { + return filePath.slice(prefix.length) + } + + return filePath +} + +function getShikiLang(filename: string) { + const ext = filename.split(".").pop()?.toLowerCase() ?? "" + const langs = map.languages(ext) + const type = langs?.[0]?.toLowerCase() + + const overrides: Record = { + conf: "shellscript", + } + + return type ? (overrides[type] ?? type) : "plaintext" +} + +function getDiagnostics(diagnosticsByFile: Record, currentFile: string): JSX.Element[] { + const result: JSX.Element[] = [] + + if (diagnosticsByFile === undefined || diagnosticsByFile[currentFile] === undefined) return result + + for (const diags of Object.values(diagnosticsByFile)) { + for (const d of diags) { + if (d.severity !== 1) continue + + const line = d.range.start.line + 1 + const column = d.range.start.character + 1 + + result.push( +
+          
+            Error
+          
+          
+            [{line}:{column}]
+          
+          {d.message}
+        
, + ) + } + } + + return result +} + +function formatErrorString(error: string): JSX.Element { + const errorMarker = "Error: " + const startsWithError = error.startsWith(errorMarker) + + return startsWithError ? ( +
+      
+        Error
+      
+      {error.slice(errorMarker.length)}
+    
+ ) : ( +
+      {error}
+    
+ ) +} + +export function TodoWriteTool(props: ToolProps) { + const priority: Record = { + in_progress: 0, + pending: 1, + completed: 2, + } + const todos = createMemo(() => + ((props.state.input?.todos ?? []) as Todo[]).slice().sort((a, b) => priority[a.status] - priority[b.status]), + ) + const starting = () => todos().every((t: Todo) => t.status === "pending") + const finished = () => todos().every((t: Todo) => t.status === "completed") + + return ( + <> +
+ + + Creating plan + Completing plan + + +
+ 0}> +
    + + {(todo) => ( +
  • + + {todo.content} +
  • + )} +
    +
+
+ + ) +} + +export function GrepTool(props: ToolProps) { + return ( + <> +
+ Grep + “{props.state.input.pattern}” +
+
+ + 0}> + + + + + + + + +
+ + ) +} + +export function ListTool(props: ToolProps) { + const path = createMemo(() => + props.state.input?.path !== props.message.path.cwd + ? stripWorkingDirectory(props.state.input?.path, props.message.path.cwd) + : props.state.input?.path, + ) + + return ( + <> +
+ LS + + {path()} + +
+
+ + + + + + + +
+ + ) +} + +export function WebFetchTool(props: ToolProps) { + return ( + <> +
+ Fetch + {props.state.input.url} +
+
+ + +
{formatErrorString(props.state.output)}
+
+ + + + + +
+
+ + ) +} + +export function ReadTool(props: ToolProps) { + const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) + + return ( + <> +
+ Read + + {filePath()} + +
+
+ + +
{formatErrorString(props.state.output)}
+
+ + + + + + + + + + +
+
+ + ) +} + +export function WriteTool(props: ToolProps) { + const filePath = createMemo(() => stripWorkingDirectory(props.state.input?.filePath, props.message.path.cwd)) + const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) + + return ( + <> +
+ Write + + {filePath()} + +
+ 0}> +
{diagnostics()}
+
+
+ + +
{formatErrorString(props.state.output)}
+
+ + + + + +
+
+ + ) +} + +export function EditTool(props: ToolProps) { + const filePath = createMemo(() => stripWorkingDirectory(props.state.input.filePath, props.message.path.cwd)) + const diagnostics = createMemo(() => getDiagnostics(props.state.metadata?.diagnostics, props.state.input.filePath)) + + return ( + <> +
+ Edit + + {filePath()} + +
+
+ + +
{formatErrorString(props.state.metadata?.message || "")}
+
+ +
+ +
+
+
+
+ 0}> +
{diagnostics()}
+
+ + ) +} + +export function BashTool(props: ToolProps) { + return ( + <> +
+
+
+ {props.state.metadata.description} +
+
+ + +
+
+
+ + ) +} + +export function GlobTool(props: ToolProps) { + return ( + <> +
+ Glob + “{props.state.input.pattern}” +
+ + 0}> +
+ + + +
+
+ + + +
+ + ) +} + +interface ResultsButtonProps extends ParentProps { + showCopy?: string + hideCopy?: string +} +function ResultsButton(props: ResultsButtonProps) { + const [show, setShow] = createSignal(false) + + return ( + <> + + {props.children} + + ) +} + +export function Spacer() { + return
+} + +function Footer(props: ParentProps<{ title: string }>) { + return ( +
+ {props.children} +
+ ) +} + +export function FallbackTool(props: ToolProps) { + return ( + <> +
+ {props.tool} +
+
+ + {(arg) => ( + <> +
+
{arg[0]}
+
{arg[1]}
+ + )} +
+
+ + +
+ + + +
+
+
+ + ) +} + +// Converts nested objects/arrays into [path, value] pairs. +// E.g. {a:{b:{c:1}}, d:[{e:2}, 3]} => [["a.b.c",1], ["d[0].e",2], ["d[1]",3]] +function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> { + const entries: Array<[string, any]> = [] + + for (const [key, value] of Object.entries(obj)) { + const path = prefix ? `${prefix}.${key}` : key + + if (value !== null && typeof value === "object") { + if (Array.isArray(value)) { + value.forEach((item, index) => { + const arrayPath = `${path}[${index}]` + if (item !== null && typeof item === "object") { + entries.push(...flattenToolArgs(item, arrayPath)) + } else { + entries.push([arrayPath, item]) + } + }) + } else { + entries.push(...flattenToolArgs(value, path)) + } + } else { + entries.push([path, value]) + } + } + + return entries +} diff --git a/packages/web/src/content/docs/docs/cli.mdx b/packages/web/src/content/docs/docs/cli.mdx index 49d343bee..57e595212 100644 --- a/packages/web/src/content/docs/docs/cli.mdx +++ b/packages/web/src/content/docs/docs/cli.mdx @@ -39,12 +39,12 @@ opencode run Explain the use of context in Go #### Flags -| Flag | Short | Description | -| ----------------- | ----- | --------------------- | -| `--continue` | `-c` | Continue the last session | -| `--session` | `-s` | Session ID to continue | -| `--share` | | Share the session | -| `--model` | `-m` | Model to use in the form of provider/model | +| Flag | Short | Description | +| ------------ | ----- | ------------------------------------------ | +| `--continue` | `-c` | Continue the last session | +| `--session` | `-s` | Session ID to continue | +| `--share` | | Share the session | +| `--model` | `-m` | Model to use in the form of provider/model | --- @@ -122,8 +122,8 @@ opencode upgrade v0.1.48 The opencode CLI takes the following flags. -| Flag | Short | Description | -| ----------------- | ----- | --------------------- | -| `--help` | `-h` | Display help | -| `--version` | | Print version number | -| `--print-logs` | | Print logs to stderr | +| Flag | Short | Description | +| -------------- | ----- | -------------------- | +| `--help` | `-h` | Display help | +| `--version` | | Print version number | +| `--print-logs` | | Print logs to stderr | diff --git a/packages/web/src/content/docs/docs/config.mdx b/packages/web/src/content/docs/docs/config.mdx index d88749c62..40583ea0a 100644 --- a/packages/web/src/content/docs/docs/config.mdx +++ b/packages/web/src/content/docs/docs/config.mdx @@ -39,7 +39,7 @@ You can configure the providers and models you want to use in your opencode conf ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "provider": { }, + "provider": {}, "model": "" } ``` @@ -70,7 +70,7 @@ You can customize your keybinds through the `keybinds` option. ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "keybinds": { } + "keybinds": {} } ``` @@ -85,7 +85,7 @@ You can configure MCP servers you want to use through the `mcp` option. ```json title="opencode.json" { "$schema": "https://opencode.ai/config.json", - "mcp": { } + "mcp": {} } ``` @@ -105,6 +105,7 @@ You can disable providers that are loaded automatically through the `disabled_pr ``` The `disabled_providers` option accepts an array of provider IDs. When a provider is disabled: + - It won't be loaded even if environment variables are set - It won't be loaded even if API keys are configured through `opencode auth login` - The provider's models won't appear in the model selection list diff --git a/packages/web/src/content/docs/docs/index.mdx b/packages/web/src/content/docs/docs/index.mdx index b39ce452c..9ea95844a 100644 --- a/packages/web/src/content/docs/docs/index.mdx +++ b/packages/web/src/content/docs/docs/index.mdx @@ -3,7 +3,7 @@ title: Intro description: Get started with opencode. --- -import { Tabs, TabItem } from '@astrojs/starlight/components'; +import { Tabs, TabItem } from "@astrojs/starlight/components" [**opencode**](/) is an AI coding agent built for the terminal. It features: @@ -21,26 +21,10 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; ## Install - - ```bash - npm install -g opencode-ai - ``` - - - ```bash - bun install -g opencode-ai - ``` - - - ```bash - pnpm install -g opencode-ai - ``` - - - ```bash - yarn global add opencode-ai - ``` - + ```bash npm install -g opencode-ai ``` + ```bash bun install -g opencode-ai ``` + ```bash pnpm install -g opencode-ai ``` + ```bash yarn global add opencode-ai ``` You can also install the opencode binary through the following. diff --git a/packages/web/src/content/docs/docs/rules.mdx b/packages/web/src/content/docs/docs/rules.mdx index b7818d71b..b1b55b026 100644 --- a/packages/web/src/content/docs/docs/rules.mdx +++ b/packages/web/src/content/docs/docs/rules.mdx @@ -31,17 +31,20 @@ You can also just create this file manually. Here's an example of some things yo This is an SST v3 monorepo with TypeScript. The project uses bun workspaces for package management. ## Project Structure + - `packages/` - Contains all workspace packages (functions, core, web, etc.) - `infra/` - Infrastructure definitions split by service (storage.ts, api.ts, web.ts) - `sst.config.ts` - Main SST configuration with dynamic imports ## Code Standards + - Use TypeScript with strict mode enabled - Shared code goes in `packages/core/` with proper exports configuration - Functions go in `packages/functions/` - Infrastructure should be split into logical files in `infra/` ## Monorepo Conventions + - Import shared modules using workspace names: `@my-app/core/example` ``` diff --git a/packages/web/src/content/docs/docs/themes.mdx b/packages/web/src/content/docs/docs/themes.mdx index da612284e..125591532 100644 --- a/packages/web/src/content/docs/docs/themes.mdx +++ b/packages/web/src/content/docs/docs/themes.mdx @@ -13,18 +13,18 @@ By default, opencode uses our own `opencode` theme. opencode comes with several built-in themes. -| Name | Description | -| --- | --- | -| `system` | Adapts to your terminal's background color | -| `tokyonight` | Based on the Tokyonight theme | -| `everforest` | Based on the Everforest theme | -| `ayu` | Based on the Ayu dark theme | -| `catppuccin` | Based on the Catppuccin theme | -| `gruvbox` | Based on the Gruvbox theme | -| `kanagawa` | Based on the Kanagawa theme | -| `nord` | Based on the Nord theme | -| `matrix` | Hacker-style green on black theme | -| `one-dark` | Based on the Atom One Dark theme | +| Name | Description | +| ------------ | ------------------------------------------ | +| `system` | Adapts to your terminal's background color | +| `tokyonight` | Based on the Tokyonight theme | +| `everforest` | Based on the Everforest theme | +| `ayu` | Based on the Ayu dark theme | +| `catppuccin` | Based on the Catppuccin theme | +| `gruvbox` | Based on the Gruvbox theme | +| `kanagawa` | Based on the Kanagawa theme | +| `nord` | Based on the Nord theme | +| `matrix` | Hacker-style green on black theme | +| `one-dark` | Based on the Atom One Dark theme | And more, we are constantly adding new themes. @@ -61,7 +61,7 @@ You can select a theme by bringing up the theme select with the `/theme` command ## Custom themes -opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily. +opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily. --- diff --git a/packages/web/src/types/lang-map.d.ts b/packages/web/src/types/lang-map.d.ts index b21d2a005..6df26d6a0 100644 --- a/packages/web/src/types/lang-map.d.ts +++ b/packages/web/src/types/lang-map.d.ts @@ -2,9 +2,9 @@ declare module "lang-map" { /** Returned by calling `map()` */ export interface MapReturn { /** All extensions keyed by language name */ - extensions: Record; + extensions: Record /** All languages keyed by file-extension */ - languages: Record; + languages: Record } /** @@ -14,14 +14,14 @@ declare module "lang-map" { * const { extensions, languages } = map(); * ``` */ - function map(): MapReturn; + function map(): MapReturn /** Static method: get extensions for a given language */ namespace map { - function extensions(language: string): string[]; + function extensions(language: string): string[] /** Static method: get languages for a given extension */ - function languages(extension: string): string[]; + function languages(extension: string): string[] } - export = map; + export = map } -- cgit v1.2.3