summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-21 05:50:26 -0600
committerAdam <[email protected]>2025-12-22 05:46:06 -0600
commit986d12fd204c0d56c225090090eb2d7c05f0b55c (patch)
tree7d7baebb957704073626f5974f2dda48e5e3d561
parentd04a72a4ad6af0bfe75bbff36004d28adca179b2 (diff)
downloadopencode-986d12fd204c0d56c225090090eb2d7c05f0b55c.tar.gz
opencode-986d12fd204c0d56c225090090eb2d7c05f0b55c.zip
feat(desktop): better task tool rendering
-rw-r--r--packages/ui/src/components/message-part.css38
-rw-r--r--packages/ui/src/components/message-part.tsx134
-rw-r--r--packages/ui/src/styles/utilities.css6
3 files changed, 167 insertions, 11 deletions
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css
index 5d2703960..ffeb4cb28 100644
--- a/packages/ui/src/components/message-part.css
+++ b/packages/ui/src/components/message-part.css
@@ -287,6 +287,44 @@
}
}
+[data-component="task-tools"] {
+ padding: 8px 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+
+ [data-slot="task-tool-item"] {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text-weak);
+
+ [data-slot="icon-svg"] {
+ flex-shrink: 0;
+ color: var(--icon-weak);
+ }
+ }
+
+ [data-slot="task-tool-title"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-medium);
+ line-height: var(--line-height-large);
+ color: var(--text-weak);
+ }
+
+ [data-slot="task-tool-subtitle"] {
+ font-family: var(--font-family-sans);
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-regular);
+ line-height: var(--line-height-large);
+ color: var(--text-weaker);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+}
+
[data-component="diagnostics"] {
display: flex;
flex-direction: column;
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 01a6fb5f5..f2fa0f320 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -87,6 +87,110 @@ function getDirectory(path: string | undefined) {
return relativizeProjectPaths(_getDirectory(path), data.directory)
}
+export function getSessionToolParts(store: ReturnType<typeof useData>["store"], sessionId: string): ToolPart[] {
+ const messages = store.message[sessionId]?.filter((m) => m.role === "assistant")
+ if (!messages) return []
+
+ const parts: ToolPart[] = []
+ for (const m of messages) {
+ const msgParts = store.part[m.id]
+ if (msgParts) {
+ for (const p of msgParts) {
+ if (p && p.type === "tool") parts.push(p as ToolPart)
+ }
+ }
+ }
+ return parts
+}
+
+import type { IconProps } from "./icon"
+
+export type ToolInfo = {
+ icon: IconProps["name"]
+ title: string
+ subtitle?: string
+}
+
+export function getToolInfo(tool: string, input: Record<string, any> = {}): ToolInfo {
+ switch (tool) {
+ case "read":
+ return {
+ icon: "glasses",
+ title: "Read",
+ subtitle: input.filePath ? getFilename(input.filePath) : undefined,
+ }
+ case "list":
+ return {
+ icon: "bullet-list",
+ title: "List",
+ subtitle: input.path ? getFilename(input.path) : undefined,
+ }
+ case "glob":
+ return {
+ icon: "magnifying-glass-menu",
+ title: "Glob",
+ subtitle: input.pattern,
+ }
+ case "grep":
+ return {
+ icon: "magnifying-glass-menu",
+ title: "Grep",
+ subtitle: input.pattern,
+ }
+ case "webfetch":
+ return {
+ icon: "window-cursor",
+ title: "Webfetch",
+ subtitle: input.url,
+ }
+ case "task":
+ return {
+ icon: "task",
+ title: `${input.subagent_type || "task"} Agent`,
+ subtitle: input.description,
+ }
+ case "bash":
+ return {
+ icon: "console",
+ title: "Shell",
+ subtitle: input.description,
+ }
+ case "edit":
+ return {
+ icon: "code-lines",
+ title: "Edit",
+ subtitle: input.filePath ? getFilename(input.filePath) : undefined,
+ }
+ case "write":
+ return {
+ icon: "code-lines",
+ title: "Write",
+ subtitle: input.filePath ? getFilename(input.filePath) : undefined,
+ }
+ case "todowrite":
+ return {
+ icon: "checklist",
+ title: "To-dos",
+ }
+ case "todoread":
+ return {
+ icon: "checklist",
+ title: "Read to-dos",
+ }
+ default:
+ return {
+ icon: "mcp",
+ title: tool,
+ }
+ }
+}
+
+function getToolPartInfo(part: ToolPart): ToolInfo {
+ const state = part.state as any
+ const input = state.input || {}
+ return getToolInfo(part.tool, input)
+}
+
export function registerPartComponent(type: string, component: PartComponent) {
PART_MAPPING[type] = component
}
@@ -453,23 +557,37 @@ ToolRegistry.register({
ToolRegistry.register({
name: "task",
render(props) {
+ const summary = () =>
+ (props.metadata.summary ?? []) as { id: string; tool: string; state: { status: string; title?: string } }[]
+
return (
<BasicTool
- {...props}
icon="task"
+ defaultOpen={true}
trigger={{
title: `${props.input.subagent_type || props.tool} Agent`,
titleClass: "capitalize",
subtitle: props.input.description,
}}
>
- {/* <Show when={false && props.output}> */}
- {/* {(output) => ( */}
- {/* <div data-component="tool-output" data-scrollable> */}
- {/* <Markdown text={output()} /> */}
- {/* </div> */}
- {/* )} */}
- {/* </Show> */}
+ <div data-component="tool-output" data-scrollable>
+ <div data-component="task-tools">
+ <For each={summary()}>
+ {(item) => {
+ const info = getToolInfo(item.tool)
+ return (
+ <div data-slot="task-tool-item">
+ <Icon name={info.icon} size="small" />
+ <span data-slot="task-tool-title">{info.title}</span>
+ <Show when={item.state.title}>
+ <span data-slot="task-tool-subtitle">{item.state.title}</span>
+ </Show>
+ </div>
+ )
+ }}
+ </For>
+ </div>
+ </div>
</BasicTool>
)
},
diff --git a/packages/ui/src/styles/utilities.css b/packages/ui/src/styles/utilities.css
index 66136d724..8c954f1fe 100644
--- a/packages/ui/src/styles/utilities.css
+++ b/packages/ui/src/styles/utilities.css
@@ -12,16 +12,16 @@
/* } */
::-webkit-scrollbar-track {
- background: var(--theme-background-panel);
+ background: transparent;
}
::-webkit-scrollbar-thumb {
- background-color: var(--theme-border-subtle);
+ background-color: var(--surface-float-base);
border-radius: var(--radius-md);
}
* {
- scrollbar-color: var(--theme-border-subtle) var(--theme-background-panel);
+ scrollbar-color: var(--surface-float-base) transparent;
}
}