mirror of
https://github.com/sst/opencode.git
synced 2025-08-31 02:07:24 +00:00
styling tool calls
This commit is contained in:
parent
d398001f96
commit
7a29af4e30
2 changed files with 165 additions and 25 deletions
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue