summaryrefslogtreecommitdiffhomepage
path: root/app/packages/web/src/components
diff options
context:
space:
mode:
authorJay V <[email protected]>2025-05-29 14:24:21 -0400
committerJay V <[email protected]>2025-05-29 14:24:25 -0400
commit7a29af4e30c1a0edbe6f4122bc512974720ff0c8 (patch)
tree88f1a47b710490df5540676f3a8b4168a0d32008 /app/packages/web/src/components
parentd398001f96fd1a7438ac2ef07b4b87bb13766b27 (diff)
downloadopencode-7a29af4e30c1a0edbe6f4122bc512974720ff0c8.tar.gz
opencode-7a29af4e30c1a0edbe6f4122bc512974720ff0c8.zip
styling tool calls
Diffstat (limited to 'app/packages/web/src/components')
-rw-r--r--app/packages/web/src/components/Share.tsx130
-rw-r--r--app/packages/web/src/components/share.module.css60
2 files changed, 165 insertions, 25 deletions
diff --git a/app/packages/web/src/components/Share.tsx b/app/packages/web/src/components/Share.tsx
index aba324d2f..12619b600 100644
--- a/app/packages/web/src/components/Share.tsx
+++ b/app/packages/web/src/components/Share.tsx
@@ -1,3 +1,4 @@
+import { type JSX } from "solid-js"
import {
For,
Show,
@@ -58,6 +59,39 @@ type SessionInfo = {
cost?: number
}
+// Converts `{a:{b:{c:1}}` to `[['a.b.c', 1]]`
+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" &&
+ !Array.isArray(value)
+ ) {
+ entries.push(...flattenToolArgs(value, path));
+ }
+ else {
+ entries.push([path, value]);
+ }
+ }
+
+ return entries;
+}
+
+function getStatusText(status: [Status, string?]): string {
+ switch (status[0]) {
+ case "connected": return "Connected"
+ case "connecting": return "Connecting..."
+ case "disconnected": return "Disconnected"
+ case "reconnecting": return "Reconnecting..."
+ case "error": return status[1] || "Error"
+ default: return "Unknown"
+ }
+}
+
function ProviderIcon(props: { provider: string, size?: number }) {
const size = props.size || 16
return (
@@ -77,26 +111,18 @@ function ProviderIcon(props: { provider: string, size?: number }) {
)
}
-function getStatusText(status: [Status, string?]): string {
- switch (status[0]) {
- case "connected": return "Connected"
- case "connecting": return "Connecting..."
- case "disconnected": return "Disconnected"
- case "reconnecting": return "Reconnecting..."
- case "error": return status[1] || "Error"
- default: return "Unknown"
- }
+interface TextPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
+ text: string
+ expand?: boolean
+ highlight?: boolean
}
-
-function TextPart(
- props: { text: string, expand?: boolean, highlight?: boolean }
-) {
+function TextPart({ text, expand, highlight, ...props }: TextPartProps) {
const [expanded, setExpanded] = createSignal(false)
const [overflowed, setOverflowed] = createSignal(false)
let preEl: HTMLPreElement | undefined
function checkOverflow() {
- if (preEl && !props.expand) {
+ if (preEl && !expand) {
setOverflowed(preEl.scrollHeight > preEl.clientHeight + 1)
}
}
@@ -107,7 +133,7 @@ function TextPart(
})
createEffect(() => {
- props.text
+ text
setTimeout(checkOverflow, 0)
})
@@ -118,10 +144,11 @@ function TextPart(
return (
<div
data-element-message-text
- data-highlight={props.highlight}
- data-expanded={expanded() || props.expand === true}
+ data-highlight={highlight}
+ data-expanded={expanded() || expand === true}
+ {...props}
>
- <pre ref={el => (preEl = el)}>{props.text}</pre>
+ <pre ref={el => (preEl = el)}>{text}</pre>
{overflowed() &&
<button
type="button"
@@ -461,7 +488,11 @@ export default function Share(props: { api: string }) {
<div></div>
</div>
<div data-section="content">
- <span data-element-label data-part-title>
+ <span
+ data-size="md"
+ data-part-title
+ data-element-label
+ >
{assistant().providerID}
</span>
<span data-part-model>
@@ -490,14 +521,73 @@ export default function Share(props: { api: string }) {
System
</span>
<TextPart
+ data-size="sm"
text={part().text}
- expand={isLastPart()}
+ data-color="dimmed"
/>
<PartFooter time={time} />
</div>
</>
}
</Match>
+ { /* Tool call */}
+ <Match when={
+ msg.role === "assistant"
+ && part.type === "tool-invocation"
+ && part
+ }>
+ {part =>
+ <>
+ <div data-section="decoration">
+ <div>
+ <IconWrenchScrewdriver width={18} height={18} />
+ </div>
+ <div></div>
+ </div>
+ <div data-section="content">
+ <span data-part-title data-size="md">
+ {part().toolInvocation.toolName}
+ </span>
+ <div data-part-tool-args>
+ <For each={
+ flattenToolArgs(part().toolInvocation.args)
+ }>
+ {([name, value]) =>
+ <>
+ <div></div>
+ <div>{name}</div>
+ <div>{value}</div>
+ </>
+ }
+ </For>
+ </div>
+ <Switch>
+ <Match when={
+ part().toolInvocation.state === "result"
+ && part().toolInvocation.result
+ }>
+ <TextPart
+ data-size="sm"
+ data-color="dimmed"
+ text={part().toolInvocation.result}
+ expand={isLastPart()}
+ />
+ </Match>
+ <Match when={
+ part().toolInvocation.state === "call"
+ }>
+ <TextPart
+ data-size="sm"
+ data-color="dimmed"
+ text="Calling..."
+ />
+ </Match>
+ </Switch>
+ <PartFooter time={time} />
+ </div>
+ </>
+ }
+ </Match>
{ /* Fallback */}
<Match when={true}>
<div data-section="decoration">
diff --git a/app/packages/web/src/components/share.module.css b/app/packages/web/src/components/share.module.css
index a6964dd51..1c0681623 100644
--- a/app/packages/web/src/components/share.module.css
+++ b/app/packages/web/src/components/share.module.css
@@ -122,7 +122,7 @@
[data-section="part"] {
display: flex;
- gap: 0.5rem;
+ gap: 0.625rem;
}
[data-section="decoration"] {
@@ -151,14 +151,18 @@
}
[data-section="content"] {
- padding: 1px 0 0.375rem;
+ padding: 0 0 0.375rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
span[data-part-title] {
- padding-top: 2px;
+ line-height: 18px;
font-size: 0.75rem;
+
+ &[data-size="md"] {
+ font-size: 0.875rem;
+ }
}
span[data-part-footer] {
@@ -170,6 +174,37 @@
span[data-part-model] {
line-height: 1.5;
}
+
+ [data-part-tool-args] {
+ display: inline-grid;
+ align-items: center;
+ grid-template-columns: max-content max-content minmax(0, 1fr);
+ max-width: 100%;
+ 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) {
+ 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);
+ }
+
+ }
}
}
@@ -180,7 +215,6 @@
display: flex;
flex-direction: column;
align-items: flex-start;
- color: var(--sl-color-text);
gap: 1rem;
pre {
@@ -188,6 +222,19 @@
font-size: 0.875rem;
white-space: pre-wrap;
overflow-wrap: anywhere;
+ color: var(--sl-color-text);
+ }
+
+ &[data-size="sm"] {
+ pre {
+ font-size: 0.75rem;
+ }
+ }
+
+ &[data-color="dimmed"] {
+ pre {
+ color: var(--sl-color-text-dimmed);
+ }
}
button {
@@ -198,7 +245,10 @@
&[data-highlight="true"] {
background-color: var(--sl-color-blue-high);
- color: var(--sl-color-text-invert);
+
+ pre {
+ color: var(--sl-color-text-invert);
+ }
button {
opacity: 0.85;