Browse Source

feat(desktop): better task tool rendering

Adam 2 months ago
parent
commit
986d12fd20

+ 38 - 0
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;

+ 126 - 8
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>
     )
   },

+ 3 - 3
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;
   }
 }