summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-06-17 20:26:09 -0400
committerJay V <[email protected]>2025-06-17 20:26:12 -0400
commitd1f9311931b48f36bd538b5bc74decd8967bf0a2 (patch)
tree9911b28a4ff382942707e6f9275b0442de3df90e
parent1c58023df93d915959d89d9540f9be5522b2fb82 (diff)
downloadopencode-d1f9311931b48f36bd538b5bc74decd8967bf0a2.tar.gz
opencode-d1f9311931b48f36bd538b5bc74decd8967bf0a2.zip
ignore: share page polish
-rw-r--r--bun.lock4
-rw-r--r--packages/web/package.json2
-rw-r--r--packages/web/src/components/CodeBlock.tsx6
-rw-r--r--packages/web/src/components/Header.astro6
-rw-r--r--packages/web/src/components/Share.tsx808
-rw-r--r--packages/web/src/components/codeblock.module.css4
-rw-r--r--packages/web/src/components/markdownview.module.css4
-rw-r--r--packages/web/src/components/share.module.css119
8 files changed, 367 insertions, 586 deletions
diff --git a/bun.lock b/bun.lock
index 90191032f..068422f8b 100644
--- a/bun.lock
+++ b/bun.lock
@@ -75,7 +75,7 @@
"sharp": "0.32.5",
"shiki": "3.4.2",
"solid-js": "1.9.7",
- "toolbeam-docs-theme": "0.2.4",
+ "toolbeam-docs-theme": "0.3.0",
},
"devDependencies": {
"@types/node": "catalog:",
@@ -1494,7 +1494,7 @@
"token-types": ["[email protected]", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
- "toolbeam-docs-theme": ["[email protected]", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-W5mdbcgRpTBDFyEdcU81USs3MFZoXMInpSznc/AFZCwqz8atk4iBNDIlhvihpGHY54Nf5crKmZwJjxVojkHFvA=="],
+ "toolbeam-docs-theme": ["[email protected]", "", { "peerDependencies": { "@astrojs/starlight": "^0.34.3", "astro": "^5.7.13" } }, "sha512-qlBkKRp8HVYV7p7jaG9lT2lvQY7c8b9czZ0tnsJUrN2TBTtEyFJymCdkhhpZNC9U4oGZ7lLk0glRJHrndWvVsg=="],
"tr46": ["[email protected]", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
diff --git a/packages/web/package.json b/packages/web/package.json
index dc6654119..3e38da7b9 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -27,7 +27,7 @@
"sharp": "0.32.5",
"shiki": "3.4.2",
"solid-js": "1.9.7",
- "toolbeam-docs-theme": "0.2.4"
+ "toolbeam-docs-theme": "0.3.0"
},
"devDependencies": {
"@types/node": "catalog:",
diff --git a/packages/web/src/components/CodeBlock.tsx b/packages/web/src/components/CodeBlock.tsx
index a9bfae8be..03744550e 100644
--- a/packages/web/src/components/CodeBlock.tsx
+++ b/packages/web/src/components/CodeBlock.tsx
@@ -18,9 +18,9 @@ function CodeBlock(props: CodeBlockProps) {
const [local, rest] = splitProps(props, ["code", "lang", "onRendered"])
let containerRef!: HTMLDivElement
- const [html] = createResource(async () => {
- return (await codeToHtml(local.code, {
- lang: local.lang || "text",
+ const [html] = createResource(() => [local.code, local.lang], async ([code, lang]) => {
+ return (await codeToHtml(code || "", {
+ lang: lang || "text",
themes: {
light: "github-light",
dark: "github-dark",
diff --git a/packages/web/src/components/Header.astro b/packages/web/src/components/Header.astro
index a45899ff8..1b531db61 100644
--- a/packages/web/src/components/Header.astro
+++ b/packages/web/src/components/Header.astro
@@ -54,9 +54,3 @@ const links = config.social || [];
}
}
</style>
-<style is:global>
-body > div.page > header {
- border-color: var(--sl-color-divider);
-}
-</style>
-
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index 03fa89d1c..58953a656 100644
--- a/packages/web/src/components/Share.tsx
+++ b/packages/web/src/components/Share.tsx
@@ -108,6 +108,20 @@ function scrollToAnchor(id: string) {
el.scrollIntoView({ behavior: "smooth" })
}
+function stripWorkingDirectory(filePath: string, workingDir: string) {
+ const prefix = workingDir.endsWith('/') ? workingDir : workingDir + '/'
+
+ if (filePath === workingDir) {
+ return ''
+ }
+
+ if (filePath.startsWith(prefix)) {
+ return filePath.slice(prefix.length)
+ }
+
+ return filePath
+}
+
function getFileType(path: string) {
return path.split(".").pop()
}
@@ -158,13 +172,11 @@ function flattenToolArgs(obj: any, prefix: string = ""): Array<[string, any]> {
return entries
}
-/**
- * Return a flat array of error diagnostics, in the format:
- * "ERROR [65:20] Property 'x' does not exist on type 'Y'"
- */
export function getDiagnostics(
diagnosticsByFile: Record<string, Diagnostic[]>,
): string[] {
+ // Return a flat array of error diagnostics, in the format:
+ // "ERROR [65:20] Property 'x' does not exist on type 'Y'"
const result: string[] = []
if (diagnosticsByFile === undefined) return result
@@ -246,9 +258,9 @@ function ResultsButton(props: ResultsButtonProps) {
<span data-button-icon>
<Show
when={local.results}
- fallback={<IconChevronRight width={10} height={10} />}
+ fallback={<IconChevronRight width={11} height={11} />}
>
- <IconChevronDown width={10} height={10} />
+ <IconChevronDown width={11} height={11} />
</Show>
</span>
</button>
@@ -258,10 +270,11 @@ function ResultsButton(props: ResultsButtonProps) {
interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
text: string
expand?: boolean
+ invert?: boolean
highlight?: boolean
}
function TextPart(props: TextPartProps) {
- const [local, rest] = splitProps(props, ["text", "expand", "highlight"])
+ const [local, rest] = splitProps(props, ["text", "expand", "invert", "highlight"])
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLPreElement | undefined
@@ -289,6 +302,7 @@ function TextPart(props: TextPartProps) {
return (
<div
class={styles["message-text"]}
+ data-invert={local.invert}
data-highlight={local.highlight}
data-expanded={expanded() || local.expand === true}
{...rest}
@@ -310,9 +324,10 @@ function TextPart(props: TextPartProps) {
interface MarkdownPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
text: string
expand?: boolean
+ highlight?: boolean
}
function MarkdownPart(props: MarkdownPartProps) {
- const [local, rest] = splitProps(props, ["text", "expand"])
+ const [local, rest] = splitProps(props, ["text", "expand", "highlight"])
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let divEl: HTMLDivElement | undefined
@@ -340,6 +355,7 @@ function MarkdownPart(props: MarkdownPartProps) {
return (
<div
class={styles["message-markdown"]}
+ data-highlight={local.highlight}
data-expanded={expanded() || local.expand === true}
{...rest}
>
@@ -362,12 +378,13 @@ function MarkdownPart(props: MarkdownPartProps) {
}
interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
- text: string
+ command: string
+ result?: string
desc?: string
expand?: boolean
}
function TerminalPart(props: TerminalPartProps) {
- const [local, rest] = splitProps(props, ["text", "desc", "expand"])
+ const [local, rest] = splitProps(props, ["command", "result", "desc", "expand"])
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLElement | undefined
@@ -401,11 +418,12 @@ function TerminalPart(props: TerminalPartProps) {
<span>{local.desc}</span>
</div>
<div data-section="content">
+ <CodeBlock lang="bash" code={local.command} />
<CodeBlock
- lang="ansi"
+ lang="console"
onRendered={checkOverflow}
ref={(el) => (preEl = el)}
- code={`\x1b[90m>\x1b[0m ${local.text}`}
+ code={local.result || ""}
/>
</div>
</div>
@@ -440,7 +458,7 @@ export default function Share(props: {
}) {
const id = props.id
- const anchorId = createMemo<number | null>(() => {
+ const anchorId = createMemo<string | null>(() => {
const raw = window.location.hash.slice(1)
const [id] = raw.split("-")
return id
@@ -570,18 +588,19 @@ export default function Share(props: {
for (let i = 0; i < messages().length; i++) {
const msg = messages()[i]
- const system = result.messages.length === 0 && msg.role === "system"
+ // TODO: Cleaup
+ // const system = result.messages.length === 0 && msg.role === "system"
const assistant = msg.metadata?.assistant
- if (system) {
- for (const part of msg.parts) {
- if (part.type === "text") {
- result.system.push(part.text)
- }
- }
- result.created = msg.metadata?.time.created
- continue
- }
+ // if (system) {
+ // for (const part of msg.parts) {
+ // if (part.type === "text") {
+ // result.system.push(part.text)
+ // }
+ // }
+ // result.created = msg.metadata?.time.created
+ // continue
+ // }
result.messages.push(msg)
@@ -686,40 +705,6 @@ export default function Share(props: {
</li>
)}
</ul>
- <div data-section="system-prompt">
- <div data-section="icon">
- <IconCpuChip width={16} height={16} />
- </div>
- <div data-section="content">
- <button
- type="button"
- data-element-button-text
- data-element-button-more
- onClick={() => showSystemPrompt((e) => !e)}
- >
- <span>
- {showingSystemPrompt()
- ? "Hide system prompt"
- : "Show system prompt"}
- </span>
- <span data-button-icon>
- <Show
- when={showingSystemPrompt()}
- fallback={<IconChevronRight width={12} height={12} />}
- >
- <IconChevronDown width={12} height={12} />
- </Show>
- </span>
- </button>
- <Show when={showingSystemPrompt()}>
- <TextPart
- expand
- data-size="sm"
- text={data().system.join("\n\n").trim()}
- />
- </Show>
- </div>
- </div>
</div>
</div>
@@ -733,19 +718,39 @@ export default function Share(props: {
{(msg, msgIndex) => (
<For each={msg.parts}>
{(part, partIndex) => {
- if (
+ if ((
part.type === "step-start" &&
(partIndex() > 0 || !msg.metadata?.assistant)
- )
+ ) || (
+ msg.role === "assistant" &&
+ part.type === "tool-invocation" &&
+ part.toolInvocation.toolName === "opencode_todoread"
+ ))
return null
const anchor = createMemo(() => `${msg.id}-${partIndex()}`)
- const [results, showResults] = createSignal(false)
+ 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 root = msg.metadata?.assistant?.path?.root || ''
+ 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 { root, metadata, args, result, duration }
+ })
return (
<Switch>
{/* User text */}
@@ -768,7 +773,7 @@ export default function Share(props: {
</div>
<div data-section="content">
<TextPart
- highlight
+ invert
text={part().text}
expand={isLastPart()}
/>
@@ -798,6 +803,7 @@ export default function Share(props: {
</div>
<div data-section="content">
<MarkdownPart
+ highlight
expand={isLastPart()}
text={stripEnclosingTag(part().text)}
/>
@@ -813,37 +819,56 @@ export default function Share(props: {
msg.metadata?.assistant
}
>
- {(assistant) => (
- <div
- id={anchor()}
- data-section="part"
- data-part-type="ai-model"
- >
- <div data-section="decoration">
- <a href={`#${anchor()}`} title="Model">
- <ProviderIcon
- size={18}
- provider={assistant().providerID}
- />
- </a>
- <div></div>
- </div>
- <div data-section="content">
- <div data-part-tool-body>
- <span
- data-size="md"
- data-part-title
- data-element-label
- >
- {assistant().providerID}
- </span>
- <span data-part-model>
- {assistant().modelID}
- </span>
+ {(assistant) => {
+ const system = () => assistant().system || []
+ return (
+ <div
+ id={anchor()}
+ data-section="part"
+ data-part-type="ai-model"
+ >
+ <div data-section="decoration">
+ <a href={`#${anchor()}`} title="Model">
+ <ProviderIcon
+ size={18}
+ provider={assistant().providerID}
+ />
+ </a>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <div data-part-tool-body>
+ <div data-part-title>
+ <span data-element-label>
+ {assistant().providerID}
+ </span>
+ </div>
+ <span data-part-model>
+ {assistant().modelID}
+ </span>
+ <div data-part-tool-result>
+ <ResultsButton
+ showCopy="Show system prompt"
+ hideCopy="Hide system prompt"
+ results={showResults()}
+ onClick={() =>
+ setShowResults((e) => !e)
+ }
+ />
+ <Show when={showResults()}>
+ <TextPart
+ expand
+ data-size="sm"
+ data-color="dimmed"
+ text={system().join("\n\n").trim()}
+ />
+ </Show>
+ </div>
+ </div>
</div>
</div>
- </div>
- )}
+ )
+ }}
</Match>
{/* System text */}
<Match
@@ -867,9 +892,9 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-element-label data-part-title>
- System
- </span>
+ <div data-part-title>
+ <span data-element-label>System</span>
+ </div>
<TextPart
data-size="sm"
text={part().text}
@@ -889,30 +914,12 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
- const args = part().toolInvocation.args
- const result =
- part().toolInvocation.state === "result" &&
- part().toolInvocation.result
- const matches = metadata()?.matches
-
- const { pattern, ...rest } = args
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
- )
+ {(_part) => {
+ const matches = () => toolData()?.metadata?.matches
+ const splitArgs = () => {
+ const { pattern, ...rest } = toolData()?.args
+ return { pattern, rest }
+ }
return (
<div
@@ -931,13 +938,17 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>Grep</span>
- <b>&ldquo;{pattern}&rdquo;</b>
- </span>
- <Show when={Object.keys(rest).length > 0}>
+ <b>&ldquo;{splitArgs().pattern}&rdquo;</b>
+ </div>
+ <Show when={
+ Object.keys(splitArgs().rest).length > 0
+ }>
<div data-part-tool-args>
- <For each={flattenToolArgs(rest)}>
+ <For each={
+ flattenToolArgs(splitArgs().rest)
+ }>
{([name, value]) => (
<>
<div></div>
@@ -949,43 +960,43 @@ export default function Share(props: {
</div>
</Show>
<Switch>
- <Match when={matches > 0}>
+ <Match when={matches() > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={
- matches === 1
+ matches() === 1
? "1 match"
- : `${matches} matches`
+ : `${matches()} matches`
}
hideCopy="Hide matches"
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<TextPart
expand
- text={result}
data-size="sm"
data-color="dimmed"
+ text={toolData()?.result}
/>
</Show>
</div>
</Match>
- <Match when={result}>
+ <Match when={toolData()?.result}>
<div data-part-tool-result>
<TextPart
expand
- text={result}
data-size="sm"
data-color="dimmed"
+ text={toolData()?.result}
/>
</div>
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1000,29 +1011,9 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
- const args = part().toolInvocation.args
- const result =
- part().toolInvocation.state === "result" &&
- part().toolInvocation.result
- const count = metadata()?.count
- const pattern = args.pattern
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
- )
+ {(_part) => {
+ const count = () => toolData()?.metadata?.count
+ const pattern = () => toolData()?.args.pattern
return (
<div
@@ -1041,39 +1032,39 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>Glob</span>
- <b>&ldquo;{pattern}&rdquo;</b>
- </span>
+ <b>&ldquo;{pattern()}&rdquo;</b>
+ </div>
<Switch>
- <Match when={count > 0}>
+ <Match when={count() > 0}>
<div data-part-tool-result>
<ResultsButton
showCopy={
- count === 1
+ count() === 1
? "1 result"
- : `${count} results`
+ : `${count()} results`
}
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<TextPart
expand
- text={result}
+ text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</Show>
</div>
</Match>
- <Match when={result}>
+ <Match when={toolData()?.result}>
<div data-part-tool-result>
<TextPart
expand
- text={result}
+ text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
@@ -1081,7 +1072,7 @@ export default function Share(props: {
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1096,24 +1087,14 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
- const args = part().toolInvocation.args
- const path = args.path
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
+ {(_part) => {
+ const path = createMemo(
+ () => toolData()?.args.path === toolData()?.root
+ ? toolData()?.root
+ : stripWorkingDirectory(
+ toolData()?.args.path,
+ toolData()?.root
)
- .toMillis(),
)
return (
@@ -1133,40 +1114,32 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>LS</span>
- <b>{path}</b>
- </span>
+ <b>{path()}</b>
+ </div>
<Switch>
- <Match
- when={
- part().toolInvocation.state ===
- "result" &&
- part().toolInvocation.result
- }
- >
+ <Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
- text={
- part().toolInvocation.result
- }
+ text={toolData()?.result}
/>
</Show>
</div>
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1181,30 +1154,15 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
- const args = part().toolInvocation.args
- const filePath = args.filePath
- const hasError = metadata()?.error
- const preview = metadata()?.preview
- const result =
- part().toolInvocation.state === "result" &&
- part().toolInvocation.result
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
+ {(_part) => {
+ const filePath = createMemo(
+ () => stripWorkingDirectory(
+ toolData()?.args.filePath,
+ toolData()?.root
+ )
)
+ const hasError = () => toolData()?.metadata?.error
+ const preview = () => toolData()?.metadata?.preview
return (
<div
@@ -1220,53 +1178,53 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>Read</span>
- <b>{filePath}</b>
- </span>
+ <b>{filePath()}</b>
+ </div>
<Switch>
- <Match when={hasError}>
+ <Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
- text={result}
+ text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</div>
</Match>
- <Match when={preview}>
+ <Match when={preview()}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show preview"
hideCopy="Hide preview"
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
- lang={getFileType(filePath)}
- code={preview}
+ lang={getFileType(filePath())}
+ code={preview()}
/>
</div>
</Show>
</div>
</Match>
- <Match when={result}>
+ <Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<TextPart
expand
- text={result}
+ text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
@@ -1275,7 +1233,7 @@ export default function Share(props: {
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1290,32 +1248,17 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
+ {(_part) => {
+ const filePath = createMemo(
+ () => stripWorkingDirectory(
+ toolData()?.args.filePath,
+ toolData()?.root
+ )
)
- const args = part().toolInvocation.args
- const filePath = args.filePath
- const content = args.content
- const hasError = metadata()?.error
- const result =
- part().toolInvocation.state === "result" &&
- part().toolInvocation.result
+ const hasError = () => toolData()?.metadata?.error
+ const content = () => toolData()?.args?.content
const diagnostics = createMemo(() =>
- getDiagnostics(metadata()?.diagnostics),
- )
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
+ getDiagnostics(toolData()?.metadata?.diagnostics)
)
return (
@@ -1332,10 +1275,10 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>Write</span>
- <b>{filePath}</b>
- </span>
+ <b>{filePath()}</b>
+ </div>
<Show when={diagnostics().length > 0}>
<TextPart
data-size="sm"
@@ -1343,31 +1286,31 @@ export default function Share(props: {
/>
</Show>
<Switch>
- <Match when={hasError}>
+ <Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
- text={result}
+ text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</div>
</Match>
- <Match when={content}>
+ <Match when={content()}>
<div data-part-tool-result>
<ResultsButton
showCopy="Show contents"
hideCopy="Hide contents"
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
- lang={getFileType(filePath)}
- code={content}
+ lang={getFileType(filePath())}
+ code={content()}
/>
</div>
</Show>
@@ -1375,7 +1318,7 @@ export default function Share(props: {
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1390,28 +1333,18 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
+ {(_part) => {
+ const diff = () => toolData()?.metadata?.diff
+ const message = () => toolData()?.metadata?.message
+ const hasError = () => toolData()?.metadata?.error
+ const filePath = createMemo(
+ () => stripWorkingDirectory(
+ toolData()?.args.filePath,
+ toolData()?.root
+ )
)
- const hasError = metadata()?.error
- const args = part().toolInvocation.args
- const filePath = args.filePath
const diagnostics = createMemo(() =>
- getDiagnostics(metadata()?.diagnostics),
- )
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
+ getDiagnostics(toolData()?.metadata?.diagnostics)
)
return (
@@ -1428,27 +1361,27 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>Edit</span>
- <b>{filePath}</b>
- </span>
+ <b>{filePath()}</b>
+ </div>
<Switch>
- <Match when={hasError}>
+ <Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
data-size="sm"
data-color="dimmed"
- text={metadata()?.message}
+ text={message()}
/>
</div>
</Match>
- <Match when={metadata()?.diff}>
+ <Match when={diff()}>
<div data-part-tool-edit>
<DiffView
class={styles["diff-code-block"]}
- diff={metadata()?.diff}
- lang={getFileType(filePath)}
+ diff={diff()}
+ lang={getFileType(filePath())}
/>
</div>
</Match>
@@ -1460,7 +1393,7 @@ export default function Share(props: {
/>
</Show>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1475,30 +1408,9 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
-
- const command = part().toolInvocation.args.command
- const desc = part().toolInvocation.args.description
- const result = createMemo(() => {
- const invocation = part().toolInvocation
- return metadata()?.stdout || (invocation.state === "result" && invocation.result)
- })
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
- )
+ {(_part) => {
+ const command = () => toolData()?.args.command
+ const desc = () => toolData()?.args.description
return (
<div
@@ -1515,66 +1427,13 @@ export default function Share(props: {
<div data-section="content">
<div data-part-tool-body>
<TerminalPart
- desc={desc}
+ desc={desc()}
data-size="sm"
- text={
- command + (result() ? `\n${result()}` : "")
- }
+ command={command()}
+ result={toolData()?.result}
/>
</div>
- <ToolFooter time={duration()} />
- </div>
- </div>
- )
- }}
- </Match>
- {/* Todo read */}
- <Match
- when={
- msg.role === "assistant" &&
- part.type === "tool-invocation" &&
- part.toolInvocation.toolName ===
- "opencode_todoread" &&
- part
- }
- >
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
- )
-
- return (
- <div
- id={anchor()}
- data-section="part"
- data-part-type="tool-fallback"
- >
- <div data-section="decoration">
- <a href={`#${anchor()}`} title="Plan">
- <IconQueueList width={18} height={18} />
- </a>
- <div></div>
- </div>
- <div data-section="content">
- <div data-part-tool-body>
- <span data-part-title data-size="sm">
- Checking plan&hellip;
- </span>
- </div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1590,41 +1449,22 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
-
- const todos = createMemo(() =>
- sortTodosByStatus(
- part().toolInvocation.args.todos,
- ),
+ {(_part) => {
+ const todos = createMemo(
+ () => sortTodosByStatus(toolData()?.args.todos)
)
- const starting = todos().every(
- (t) => t.status === "pending",
+ const starting = () => todos().every(
+ (t) => t.status === "pending"
)
- const finished = todos().every(
- (t) => t.status === "completed",
- )
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
+ const finished = () => todos().every(
+ (t) => t.status === "completed"
)
return (
<div
id={anchor()}
data-section="part"
- data-part-type="tool-fallback"
+ data-part-type="tool-todo"
>
<div data-section="decoration">
<a href={`#${anchor()}`} title="Plan">
@@ -1634,30 +1474,32 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="sm">
- <Switch fallback="Updating the plan">
- <Match when={starting}>
- Creating a plan
- </Match>
- <Match when={finished}>
- Completing the plan
- </Match>
- </Switch>
- </span>
+ <div data-part-title>
+ <span data-element-label>
+ <Switch fallback="Updating plan">
+ <Match when={starting()}>
+ Creating plan
+ </Match>
+ <Match when={finished()}>
+ Completing plan
+ </Match>
+ </Switch>
+ </span>
+ </div>
<Show when={todos().length > 0}>
<ul class={styles.todos}>
<For each={todos()}>
- {({ status, content }) => (
- <li data-status={status}>
+ {(todo) => (
+ <li data-status={todo.status}>
<span></span>
- {content}
+ {todo.content}
</li>
)}
</For>
</ul>
</Show>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1673,31 +1515,10 @@ export default function Share(props: {
part
}
>
- {(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
- const args = part().toolInvocation.args
- const url = args.url
- const format = args.format
- const hasError = metadata()?.error
- const result = createMemo(() => {
- const invocation = part().toolInvocation
- return invocation.state === "result" && invocation.result
- })
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
- )
+ {(_part) => {
+ const url = () => toolData()?.args.url
+ const format = () => toolData()?.args.format
+ const hasError = () => toolData()?.metadata?.error
return (
<div
@@ -1713,34 +1534,34 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
<span data-element-label>Fetch</span>
- <b>{url}</b>
- </span>
+ <b>{url()}</b>
+ </div>
<Switch>
- <Match when={hasError}>
+ <Match when={hasError()}>
<div data-part-tool-result>
<TextPart
expand
- text={result()}
+ text={toolData()?.result}
data-size="sm"
data-color="dimmed"
/>
</div>
</Match>
- <Match when={result}>
+ <Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
- lang={format || "text"}
- code={result()}
+ lang={format() || "text"}
+ code={toolData()?.result}
/>
</div>
</Show>
@@ -1748,7 +1569,7 @@ export default function Share(props: {
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1763,23 +1584,6 @@ export default function Share(props: {
}
>
{(part) => {
- const metadata = createMemo(
- () =>
- msg.metadata?.tool[
- part().toolInvocation.toolCallId
- ],
- )
-
- const duration = createMemo(() =>
- DateTime.fromMillis(metadata()?.time.end || 0)
- .diff(
- DateTime.fromMillis(
- metadata()?.time.start || 0,
- ),
- )
- .toMillis(),
- )
-
return (
<div
id={anchor()}
@@ -1797,47 +1601,39 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-part-title data-size="md">
+ <div data-part-title>
{part().toolInvocation.toolName}
- </span>
+ </div>
<div data-part-tool-args>
<For
each={flattenToolArgs(
part().toolInvocation.args,
)}
>
- {([name, value]) => (
+ {(arg) => (
<>
<div></div>
- <div>{name}</div>
- <div>{value}</div>
+ <div>{arg[0]}</div>
+ <div>{arg[1]}</div>
</>
)}
</For>
</div>
<Switch>
- <Match
- when={
- part().toolInvocation.state ===
- "result" &&
- part().toolInvocation.result
- }
- >
+ <Match when={toolData()?.result}>
<div data-part-tool-result>
<ResultsButton
- results={results()}
+ results={showResults()}
onClick={() =>
- showResults((e) => !e)
+ setShowResults((e) => !e)
}
/>
- <Show when={results()}>
+ <Show when={showResults()}>
<TextPart
expand
data-size="sm"
data-color="dimmed"
- text={
- part().toolInvocation.result
- }
+ text={toolData()?.result}
/>
</Show>
</div>
@@ -1855,7 +1651,7 @@ export default function Share(props: {
</Match>
</Switch>
</div>
- <ToolFooter time={duration()} />
+ <ToolFooter time={toolData()?.duration || 0} />
</div>
</div>
)
@@ -1898,9 +1694,11 @@ export default function Share(props: {
</div>
<div data-section="content">
<div data-part-tool-body>
- <span data-element-label data-part-title>
- {part.type}
- </span>
+ <div data-part-title>
+ <span data-element-label>
+ {part.type}
+ </span>
+ </div>
<TextPart
text={JSON.stringify(part, null, 2)}
/>
diff --git a/packages/web/src/components/codeblock.module.css b/packages/web/src/components/codeblock.module.css
index 089490fb2..bdd5c40a8 100644
--- a/packages/web/src/components/codeblock.module.css
+++ b/packages/web/src/components/codeblock.module.css
@@ -1,7 +1,7 @@
.codeblock {
pre {
- --shiki-dark-bg: var(--sl-color-bg) !important;
- background-color: var(--sl-color-bg) !important;
+ --shiki-dark-bg: var(--sl-color-bg-surface) !important;
+ background-color: var(--sl-color-bg-surface) !important;
}
}
diff --git a/packages/web/src/components/markdownview.module.css b/packages/web/src/components/markdownview.module.css
index 887fec38d..b162ec43e 100644
--- a/packages/web/src/components/markdownview.module.css
+++ b/packages/web/src/components/markdownview.module.css
@@ -12,6 +12,10 @@
margin-bottom: 1rem;
}
+ strong {
+ font-weight: 600;
+ }
+
ol {
list-style-position: inside;
padding-left: 0.75rem;
diff --git a/packages/web/src/components/share.module.css b/packages/web/src/components/share.module.css
index 927d3be4b..4f2711120 100644
--- a/packages/web/src/components/share.module.css
+++ b/packages/web/src/components/share.module.css
@@ -5,6 +5,10 @@
gap: 2.5rem;
line-height: 1;
+ --sm-tool-width: 28rem;
+ --md-tool-width: 40rem;
+ --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");
}
@@ -37,7 +41,7 @@
[data-element-label] {
text-transform: uppercase;
- letter-spacing: 0.05em;
+ letter-spacing: -0.5px;
color: var(--sl-color-text-dimmed);
}
@@ -164,30 +168,6 @@
}
}
}
- [data-section="system-prompt"] {
- display: flex;
- gap: 0.3125rem;
-
- [data-section="icon"] {
- flex: 0 0 auto;
- color: var(--sl-color-text-dimmed);
- opacity: 0.85;
- svg {
- display: block;
- }
- }
-
- [data-section="content"] {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- }
-
- button {
- line-height: 1rem;
- font-size: 0.875rem;
- }
- }
}
.parts {
@@ -227,6 +207,7 @@
}
& > [data-section="content"] {
+ flex: 1 1 auto;
min-width: 0;
padding: 0 0 0.375rem;
display: flex;
@@ -236,21 +217,29 @@
[data-part-tool-body] {
display: flex;
flex-direction: column;
+ align-items: flex-start;
gap: 0.375rem;
}
- span[data-part-title] {
+ [data-part-title] {
line-height: 18px;
- font-size: 0.75rem;
+ font-size: 0.875rem;
+ color: var(--sl-color-text-secondary);
+ max-wdith: var(--sm-tool-width);
+
+ display: flex;
+ align-items: flex-start;
+ gap: 0.375rem;
+
+ span[data-element-label] {
+ color: var(--sl-color-text-secondary);
+ }
b {
+ color: var(--sl-color-text);
word-break: break-all;
font-weight: 500;
}
-
- &[data-size="md"] {
- font-size: 0.875rem;
- }
}
span[data-part-footer] {
@@ -267,7 +256,7 @@
display: inline-grid;
align-items: center;
grid-template-columns: max-content max-content minmax(0, 1fr);
- max-width: 100%;
+ max-width: var(--md-tool-width);
gap: 0.25rem 0.375rem;
& > div:nth-child(3n + 1) {
@@ -279,16 +268,14 @@
& > div:nth-child(3n + 2),
& > div:nth-child(3n + 3) {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
font-size: 0.75rem;
line-height: 1.5;
}
& > div:nth-child(3n + 3) {
padding-left: 0.125rem;
- color: var(--sl-color-text-dimmed);
+ word-break: break-word;
+ color: var(--sl-color-text-secondary);
}
}
@@ -302,6 +289,11 @@
font-size: 0.75rem;
}
}
+
+ [data-part-tool-edit] {
+ width: 100%;
+ max-width: var(--lg-tool-width);
+ }
}
}
@@ -325,16 +317,6 @@
& > [data-section="content"] > [data-part-tool-body] {
gap: 0.5rem;
}
- [data-part-title] {
- display: flex;
- align-items: flex-start;
- gap: 0.5rem;
-
- b {
- color: var(--sl-color-text);
- word-break: break-all;
- }
- }
}
[data-part-type="tool-grep"] {
@@ -342,16 +324,6 @@
> [data-section="content"] > [data-part-tool-body] {
gap: 0.5rem;
}
- [data-part-title] {
- display: flex;
- align-items: flex-start;
- gap: 0.5rem;
-
- b {
- color: var(--sl-color-text);
- word-break: break-all;
- }
- }
}
[data-part-type="tool-write"],
@@ -359,7 +331,9 @@
[data-part-type="tool-fetch"] {
[data-part-tool-result] {
[data-part-tool-code] {
+ 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);
@@ -372,8 +346,6 @@
}
}
}
- [data-part-type="tool-edit"] {
- }
}
.message-text {
@@ -384,6 +356,8 @@
flex-direction: column;
align-items: flex-start;
gap: 1rem;
+ align-self: flex-start;
+ max-width: var(--md-tool-width);
&[data-size="sm"] {
pre {
@@ -411,7 +385,7 @@
font-size: 0.75rem;
}
- &[data-highlight="true"] {
+ &[data-invert="true"] {
background-color: var(--sl-color-blue-high);
pre {
@@ -428,6 +402,10 @@
}
}
+ &[data-highlight="true"] {
+ background-color: var(--sl-color-blue-low);
+ }
+
&[data-expanded="true"] {
pre {
display: block;
@@ -450,6 +428,7 @@
gap: 0.5rem;
& > [data-section="body"] {
+ width: var(--sm-tool-width);
border: 1px solid var(--sl-color-divider);
border-radius: 0.25rem;
max-width: 100%;
@@ -460,7 +439,7 @@
width: 100%;
height: 1.625rem;
text-align: center;
- padding: 0 0.75rem 0 3.25rem;
+ padding: 0 3.25rem;
& > span {
max-width: min(100%, 140ch);
@@ -491,12 +470,10 @@
[data-section="content"] {
padding: 0.5rem calc(0.5rem + 3px);
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- gap: 1rem;
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;
@@ -533,6 +510,8 @@
flex-direction: column;
align-items: flex-start;
gap: 1rem;
+ align-self: flex-start;
+ max-width: var(--md-tool-width);
button {
flex: 0 0 auto;
@@ -540,6 +519,10 @@
font-size: 0.75rem;
}
+ &[data-highlight="true"] {
+ background-color: var(--sl-color-blue-low);
+ }
+
&[data-expanded="true"] {
[data-elment-markdown] {
display: block;
@@ -566,6 +549,7 @@
list-style-type: none;
padding: 0;
margin: 0;
+ width: var(--sm-tool-width);
border: 1px solid var(--sl-color-divider);
border-radius: 0.25rem;
@@ -577,6 +561,7 @@
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;
@@ -614,9 +599,9 @@
}
}
&[data-status="completed"] {
- color: var(--sl-color-text-dimmed);
+ color: var(--sl-color-text-secondary);
- & > span { border-color: var(--sl-color-hairline); }
+ & > span { border-color: var(--sl-color-green-low); }
& > span::before {
content: "";
position: absolute;
@@ -624,7 +609,7 @@
left: 2px;
width: calc(0.75rem - 2px - 4px);
height: calc(0.75rem - 2px - 4px);
- box-shadow: inset 1rem 1rem var(--sl-color-divider);
+ 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%);