summaryrefslogtreecommitdiffhomepage
path: root/packages/web
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-08-10 19:24:16 -0500
committeradamdotdevin <[email protected]>2025-08-10 19:25:03 -0500
commitb8d2aebf09313d451cec873b3d0807b818015b32 (patch)
tree05f4edc9e4b15b6754e555a7b4eb03b46c997a58 /packages/web
parent20e818ad05b385b51a29d265f0731155562e88c2 (diff)
downloadopencode-b8d2aebf09313d451cec873b3d0807b818015b32.tar.gz
opencode-b8d2aebf09313d451cec873b3d0807b818015b32.zip
feat: thinking blocks rendered in tui and share page
Diffstat (limited to 'packages/web')
-rw-r--r--packages/web/src/components/Share.tsx12
-rw-r--r--packages/web/src/components/icons/custom.tsx23
-rw-r--r--packages/web/src/components/share/part.module.css23
-rw-r--r--packages/web/src/components/share/part.tsx12
4 files changed, 60 insertions, 10 deletions
diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx
index 4a75f737a..47632492d 100644
--- a/packages/web/src/components/Share.tsx
+++ b/packages/web/src/components/Share.tsx
@@ -61,7 +61,7 @@ export default function Share(props: {
const [store, setStore] = createStore<{
info?: Session.Info
messages: Record<string, MessageWithParts>
- }>({ info: props.info, messages: mapValues(props.messages, (x: any) => "metadata" in x ? fromV1(x) : x) })
+ }>({ info: props.info, messages: mapValues(props.messages, (x: any) => ("metadata" in x ? fromV1(x) : x)) })
const messages = createMemo(() => Object.values(store.messages).toSorted((a, b) => a.id?.localeCompare(b.id)))
const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected", "Disconnected"])
createEffect(() => {
@@ -128,12 +128,10 @@ export default function Share(props: {
setStore("messages", messageID, reconcile(d.content))
}
if (type === "part") {
- setStore("messages", d.content.messageID, "parts", arr => {
+ setStore("messages", d.content.messageID, "parts", (arr) => {
const index = arr.findIndex((x) => x.id === d.content.id)
- if (index === -1)
- arr.push(d.content)
- if (index > -1)
- arr[index] = d.content
+ if (index === -1) arr.push(d.content)
+ if (index > -1) arr[index] = d.content
return [...arr]
})
}
@@ -350,7 +348,7 @@ export default function Share(props: {
if (x.type === "tool" && (x.state.status === "pending" || x.state.status === "running"))
return false
return true
- })
+ }),
)
return (
diff --git a/packages/web/src/components/icons/custom.tsx b/packages/web/src/components/icons/custom.tsx
index ba06ddfb3..8023032e5 100644
--- a/packages/web/src/components/icons/custom.tsx
+++ b/packages/web/src/components/icons/custom.tsx
@@ -54,7 +54,10 @@ export function IconOpencode(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
- <path fill="currentColor" d="M16.92 4.5c-1.851 0-3.298 1.394-4.608 3.165C10.512 5.373 9.007 4.5 7.206 4.5C3.534 4.5.72 9.28.72 14.338c0 3.165 1.531 5.162 4.096 5.162c1.846 0 3.174-.87 5.535-4.997c0 0 .984-1.737 1.66-2.934q.356.574.75 1.238l1.107 1.862c2.156 3.608 3.358 4.831 5.534 4.831c2.5 0 3.89-2.024 3.89-5.255c0-5.297-2.877-9.745-6.372-9.745m-8.37 8.886c-1.913 3-2.575 3.673-3.64 3.673c-1.097 0-1.749-.963-1.749-2.68c0-3.672 1.831-7.427 4.014-7.427c1.182 0 2.17.682 3.683 2.848c-1.437 2.204-2.307 3.586-2.307 3.586m7.224-.377L14.45 10.8a45 45 0 0 0-1.032-1.608c1.193-1.841 2.176-2.759 3.347-2.759c2.43 0 4.375 3.58 4.375 7.976c0 1.676-.549 2.649-1.686 2.649c-1.09 0-1.61-.72-3.68-4.05" />
+ <path
+ fill="currentColor"
+ d="M16.92 4.5c-1.851 0-3.298 1.394-4.608 3.165C10.512 5.373 9.007 4.5 7.206 4.5C3.534 4.5.72 9.28.72 14.338c0 3.165 1.531 5.162 4.096 5.162c1.846 0 3.174-.87 5.535-4.997c0 0 .984-1.737 1.66-2.934q.356.574.75 1.238l1.107 1.862c2.156 3.608 3.358 4.831 5.534 4.831c2.5 0 3.89-2.024 3.89-5.255c0-5.297-2.877-9.745-6.372-9.745m-8.37 8.886c-1.913 3-2.575 3.673-3.64 3.673c-1.097 0-1.749-.963-1.749-2.68c0-3.672 1.831-7.427 4.014-7.427c1.182 0 2.17.682 3.683 2.848c-1.437 2.204-2.307 3.586-2.307 3.586m7.224-.377L14.45 10.8a45 45 0 0 0-1.032-1.608c1.193-1.841 2.176-2.759 3.347-2.759c2.43 0 4.375 3.58 4.375 7.976c0 1.676-.549 2.649-1.686 2.649c-1.09 0-1.61-.72-3.68-4.05"
+ />
</svg>
)
}
@@ -63,6 +66,22 @@ export function IconMeta(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
export function IconRobot(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
- <path fill="currentColor" d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2M6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1zm-4 3H0v6h2zm20 0h2v6h-2zM9 14.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6 0a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3" /></svg>
+ <path
+ fill="currentColor"
+ d="M13.5 2c0 .444-.193.843-.5 1.118V5h5a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h5V3.118A1.5 1.5 0 1 1 13.5 2M6 7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1zm-4 3H0v6h2zm20 0h2v6h-2zM9 14.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m6 0a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"
+ />
+ </svg>
+ )
+}
+
+// https://icones.js.org/collection/ri?s=brain&icon=ri:brain-2-line
+export function IconBrain(props: JSX.SvgSVGAttributes<SVGSVGElement>) {
+ return (
+ <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+ <path
+ fill="currentColor"
+ d="M7 6q.001.357.115.67a1 1 0 0 1-1 1.333L6 8a2 2 0 0 0-1.491 3.333a1 1 0 0 1 0 1.334a2 2 0 0 0 .864 3.233a1 1 0 0 1 .67 1.135a2.5 2.5 0 1 0 4.932.824q.009-.063.025-.123V6a2 2 0 1 0-4 0m6 11.736q.016.06.025.122a2.5 2.5 0 1 0 4.932-.823a1 1 0 0 1 .67-1.135a2 2 0 0 0 .864-3.233a1 1 0 0 1 0-1.334a2 2 0 0 0-1.607-3.33a1 1 0 0 1-.999-1.333q.113-.313.115-.67a2 2 0 1 0-4 0zM9 2a4 4 0 0 1 3 1.354a4 4 0 0 1 6.998 2.771A4.002 4.002 0 0 1 21.465 12A3.997 3.997 0 0 1 20 17.465v.035a4.5 4.5 0 0 1-8 2.828A4.5 4.5 0 0 1 4 17.5v-.035A3.997 3.997 0 0 1 2.535 12a4.002 4.002 0 0 1 2.467-5.874L5 6a4 4 0 0 1 4-4"
+ />
+ </svg>
)
}
diff --git a/packages/web/src/components/share/part.module.css b/packages/web/src/components/share/part.module.css
index ffae0c3b7..3dd321425 100644
--- a/packages/web/src/components/share/part.module.css
+++ b/packages/web/src/components/share/part.module.css
@@ -128,6 +128,29 @@
max-width: var(--md-tool-width);
}
+ [data-component="assistant-reasoning"] {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ flex-grow: 1;
+ max-width: var(--md-tool-width);
+
+ & > [data-component="assistant-reasoning-markdown"] {
+ align-self: flex-start;
+ font-size: 0.875rem;
+ border: 1px solid var(--sl-color-blue-high);
+ padding: 0.5rem calc(0.5rem + 3px);
+ border-radius: 0.25rem;
+ position: relative;
+
+ [data-component="copy-button"] {
+ top: 0.5rem;
+ right: calc(0.5rem - 1px);
+ }
+ }
+ }
+
[data-component="assistant-text"] {
min-width: 0;
display: flex;
diff --git a/packages/web/src/components/share/part.tsx b/packages/web/src/components/share/part.tsx
index 772a80dc6..30f927fc4 100644
--- a/packages/web/src/components/share/part.tsx
+++ b/packages/web/src/components/share/part.tsx
@@ -19,7 +19,7 @@ import {
IconMagnifyingGlass,
IconDocumentMagnifyingGlass,
} from "../icons"
-import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic } from "../icons/custom"
+import { IconMeta, IconRobot, IconOpenAI, IconGemini, IconAnthropic, IconBrain } from "../icons/custom"
import { ContentCode } from "./content-code"
import { ContentDiff } from "./content-diff"
import { ContentText } from "./content-text"
@@ -83,6 +83,9 @@ export function Part(props: PartProps) {
>
{(model) => <ProviderIcon model={model()} size={18} />}
</Match>
+ <Match when={props.part.type === "reasoning" && props.message.role === "assistant"}>
+ <IconBrain width={18} height={18} />
+ </Match>
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
<IconQueueList width={18} height={18} />
</Match>
@@ -157,6 +160,13 @@ export function Part(props: PartProps) {
)}
</div>
)}
+ {props.message.role === "assistant" && props.part.type === "reasoning" && (
+ <div data-component="assistant-reasoning">
+ <div data-component="assistant-reasoning-markdown">
+ <ContentMarkdown expand={props.last} text={props.part.text || "Thinking..."} />
+ </div>
+ </div>
+ )}
{props.message.role === "user" && props.part.type === "file" && (
<div data-component="attachment">
<div data-slot="copy">Attachment</div>