From 61387b20e640118de08b675d10e62f35fd940eb8 Mon Sep 17 00:00:00 2001 From: scotttong Date: Sat, 1 Aug 2026 23:15:31 -0700 Subject: [PATCH] feat(ui): round-2 whimsy curation, informative-status icon pass, composer image chip X - Claude-Code whimsy pool trimmed to Brewing/Tinkering/Distilling/Deliberating - analog-office pool drops Cataloguing/Carbon-copying/Archiving, adds Trimming, Aligning, Combining, Whiteboarding, Diagramming, Sketching, Labeling, Sticky-noting - Grep becomes its own informative family: Grepping + search-code icon (never whimsified) - taxonomy icons: Search, Book, SquarePen, ChevronsLeftRightEllipsis, Network; MCP tools now use the official MCP logo (McpIcon) instead of Plug - Thinking/Responding statuses get Brain / MessageSquareReply glyphs in the live pill - MDXEditor image chip: settings button removed, delete uses lucide X Co-Authored-By: Claude Fable 5 --- .../task-chat/TaskChatStatusPill.tsx | 6 +- .../task-chat/status-whimsy.test.ts | 31 ++++++++-- ui/src/components/task-chat/status-whimsy.ts | 31 ++++------ .../task-chat/tool-taxonomy.test.ts | 58 +++++++++++++------ ui/src/components/task-chat/tool-taxonomy.ts | 53 ++++++++++++----- .../task-chat/transcript-adapter.test.ts | 2 +- 6 files changed, 121 insertions(+), 60 deletions(-) diff --git a/ui/src/components/task-chat/TaskChatStatusPill.tsx b/ui/src/components/task-chat/TaskChatStatusPill.tsx index 5433ac54007..eecadaf1b96 100644 --- a/ui/src/components/task-chat/TaskChatStatusPill.tsx +++ b/ui/src/components/task-chat/TaskChatStatusPill.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; import { Loader2, ShieldQuestion, OctagonX, Ban, Scissors } from "lucide-react"; import type { TaskChatStatusItem } from "./task-chat-model"; -import { toolTaxonomy } from "./tool-taxonomy"; +import { statusLabelIcon, toolTaxonomy } from "./tool-taxonomy"; import { isGenericStatusLabel, whimsyWord } from "./status-whimsy"; function elapsedLabel(ms?: number): string | null { @@ -61,7 +61,9 @@ export function TaskChatStatusPill({ item, onApprovalDecision }: TaskChatStatusP if (live) { const liveElapsed = liveElapsedLabel(liveElapsedMs ?? item.elapsedMs); - const ToolIcon = item.toolName ? toolTaxonomy(item.toolName).icon : null; + const ToolIcon = item.toolName + ? toolTaxonomy(item.toolName).icon + : statusLabelIcon(item.label); // Whimsy fills gaps, never replaces signal: only the generic // "Running"/"Working" labels swap for a deterministic whimsical gerund // (seeded by the run-scoped item id, rotating ~10s via the live tick). diff --git a/ui/src/components/task-chat/status-whimsy.test.ts b/ui/src/components/task-chat/status-whimsy.test.ts index 42c8e74c96c..988989f55ee 100644 --- a/ui/src/components/task-chat/status-whimsy.test.ts +++ b/ui/src/components/task-chat/status-whimsy.test.ts @@ -14,13 +14,34 @@ describe("whimsy word pools", () => { expect(WHIMSY_WORDS).toContain("Clipping"); }); - it("curates ~40 unique words, analog-office making up about half", () => { - expect(WHIMSY_WORDS.length).toBeGreaterThanOrEqual(38); + it("keeps the pools unique and analog-office dominant (round-2 curation)", () => { expect(new Set(WHIMSY_WORDS).size).toBe(WHIMSY_WORDS.length); expect(WHIMSY_WORDS.length).toBe(ANALOG_OFFICE_WORDS.length + CLAUDE_CODE_WORDS.length); - const officeShare = ANALOG_OFFICE_WORDS.length / WHIMSY_WORDS.length; - expect(officeShare).toBeGreaterThanOrEqual(0.45); - expect(officeShare).toBeLessThanOrEqual(0.65); + expect(ANALOG_OFFICE_WORDS.length).toBeGreaterThan(CLAUDE_CODE_WORDS.length); + }); + + it("holds only the four board-kept Claude-Code words", () => { + expect([...CLAUDE_CODE_WORDS]).toEqual(["Brewing", "Tinkering", "Distilling", "Deliberating"]); + }); + + it("applies the round-2 analog-office edits", () => { + for (const removed of ["Cataloguing", "Carbon-copying", "Archiving"]) { + expect(ANALOG_OFFICE_WORDS).not.toContain(removed); + } + for (const added of [ + "Trimming", + "Aligning", + "Combining", + "Whiteboarding", + "Diagramming", + "Sketching", + "Labeling", + "Sticky-noting", + ]) { + expect(ANALOG_OFFICE_WORDS).toContain(added); + } + // "Grepping" is an informative taxonomy verb, never random whimsy. + expect(WHIMSY_WORDS).not.toContain("Grepping"); }); it("keeps every word a gerund so it reads sensibly after-the-fact", () => { diff --git a/ui/src/components/task-chat/status-whimsy.ts b/ui/src/components/task-chat/status-whimsy.ts index aabc9f3e1c5..b9bc3d23f09 100644 --- a/ui/src/components/task-chat/status-whimsy.ts +++ b/ui/src/components/task-chat/status-whimsy.ts @@ -10,7 +10,9 @@ * ~10s of elapsed run time. */ -/** Paperclip / analog-office pool — "Clipping" is the guaranteed brand nod. */ +/** Paperclip / analog-office pool — "Clipping" is the guaranteed brand nod. + * Board-curated round 2 (PAP-349): dropped Cataloguing / Carbon-copying / + * Archiving, added the workshop-and-whiteboard verbs. */ export const ANALOG_OFFICE_WORDS: readonly string[] = [ "Clipping", "Organizing", @@ -21,40 +23,31 @@ export const ANALOG_OFFICE_WORDS: readonly string[] = [ "Collating", "Stapling", "Indexing", - "Archiving", "Annotating", "Drafting", "Proofreading", - "Cataloguing", "Alphabetizing", "Photocopying", - "Carbon-copying", "Laminating", "Hole-punching", "Bookmarking", "Highlighting", "Typing", + "Trimming", + "Aligning", + "Combining", + "Whiteboarding", + "Diagramming", + "Sketching", + "Labeling", + "Sticky-noting", ]; -/** Claude-Code-style whimsy pool. */ +/** Claude-Code-style whimsy pool — board-curated round 2 kept only these four. */ export const CLAUDE_CODE_WORDS: readonly string[] = [ - "Pondering", - "Percolating", - "Reticulating", - "Conjuring", - "Marinating", - "Ruminating", - "Noodling", "Brewing", - "Mulling", - "Simmering", "Tinkering", - "Cogitating", - "Musing", - "Incubating", "Distilling", - "Untangling", - "Puzzling", "Deliberating", ]; diff --git a/ui/src/components/task-chat/tool-taxonomy.test.ts b/ui/src/components/task-chat/tool-taxonomy.test.ts index 52f9fb09c0b..32dd0141653 100644 --- a/ui/src/components/task-chat/tool-taxonomy.test.ts +++ b/ui/src/components/task-chat/tool-taxonomy.test.ts @@ -1,15 +1,18 @@ import { describe, expect, it } from "vitest"; import { - BookOpen, - Bot, - FileSearch, - Globe, - Pencil, - Plug, + Book, + Brain, + ChevronsLeftRightEllipsis, + MessageSquareReply, + Network, + Search, + SearchCode, + SquarePen, SquareTerminal, Wrench, } from "lucide-react"; -import { isGenericToolName, mcpToolSegment, toolTaxonomy } from "./tool-taxonomy"; +import { McpIcon } from "./McpIcon"; +import { isGenericToolName, mcpToolSegment, statusLabelIcon, toolTaxonomy } from "./tool-taxonomy"; describe("toolTaxonomy", () => { it("maps each family to its icon and verb", () => { @@ -21,23 +24,28 @@ describe("toolTaxonomy", () => { expect(toolTaxonomy("Shell").family).toBe("terminal"); expect(toolTaxonomy("Grep")).toEqual({ + family: "grep", + icon: SearchCode, + verbLabel: "Grepping", + }); + + expect(toolTaxonomy("Glob")).toEqual({ family: "search", - icon: FileSearch, + icon: Search, verbLabel: "Searching", }); - expect(toolTaxonomy("Glob").family).toBe("search"); expect(toolTaxonomy("WebSearch").family).toBe("search"); expect(toolTaxonomy("Read")).toEqual({ family: "read", - icon: BookOpen, + icon: Book, verbLabel: "Reading files", }); expect(toolTaxonomy("NotebookRead").family).toBe("read"); expect(toolTaxonomy("Edit")).toEqual({ family: "edit", - icon: Pencil, + icon: SquarePen, verbLabel: "Editing files", }); expect(toolTaxonomy("Write").family).toBe("edit"); @@ -46,22 +54,22 @@ describe("toolTaxonomy", () => { expect(toolTaxonomy("WebFetch")).toEqual({ family: "web", - icon: Globe, + icon: ChevronsLeftRightEllipsis, verbLabel: "Fetching the web", }); expect(toolTaxonomy("Task")).toEqual({ family: "agent", - icon: Bot, + icon: Network, verbLabel: "Delegating", }); expect(toolTaxonomy("Agent").family).toBe("agent"); }); - it("collapses mcp__ names to a Plug entry with the tool segment verb", () => { + it("collapses mcp__ names to the MCP logo with the tool segment verb", () => { const entry = toolTaxonomy("mcp__linear-server__search_issues"); expect(entry.family).toBe("mcp"); - expect(entry.icon).toBe(Plug); + expect(entry.icon).toBe(McpIcon); expect(entry.verbLabel).toBe("Using Search_issues"); }); @@ -79,13 +87,27 @@ describe("toolTaxonomy", () => { describe("toolTaxonomy multi-word ACP titles", () => { it("classifies by the first word", () => { - expect(toolTaxonomy("Read File").icon).toBe(BookOpen); - expect(toolTaxonomy("Edit File").icon).toBe(Pencil); - expect(toolTaxonomy("Write File").icon).toBe(Pencil); + expect(toolTaxonomy("Read File").icon).toBe(Book); + expect(toolTaxonomy("Edit File").icon).toBe(SquarePen); + expect(toolTaxonomy("Write File").icon).toBe(SquarePen); expect(toolTaxonomy("Terminal").icon).toBe(SquareTerminal); }); }); +describe("statusLabelIcon", () => { + it("gives the tool-free informative statuses their glyphs", () => { + expect(statusLabelIcon("Thinking")).toBe(Brain); + expect(statusLabelIcon("Responding")).toBe(MessageSquareReply); + expect(statusLabelIcon("Responding (streaming)")).toBe(MessageSquareReply); + }); + + it("leaves whimsified and generic labels glyph-free", () => { + for (const label of ["Running", "Working", "Clipping", "Brewing", "Queued", "", undefined, null]) { + expect(statusLabelIcon(label)).toBeNull(); + } + }); +}); + describe("isGenericToolName", () => { it("flags acpx placeholder names, including status-suffixed variants", () => { expect(isGenericToolName("tool call")).toBe(true); diff --git a/ui/src/components/task-chat/tool-taxonomy.ts b/ui/src/components/task-chat/tool-taxonomy.ts index 6ea54e8173e..9364519f844 100644 --- a/ui/src/components/task-chat/tool-taxonomy.ts +++ b/ui/src/components/task-chat/tool-taxonomy.ts @@ -6,19 +6,26 @@ * tools. */ import { - BookOpen, - Bot, - FileSearch, - Globe, - Pencil, - Plug, + Book, + Brain, + ChevronsLeftRightEllipsis, + MessageSquareReply, + Network, + Search, + SearchCode, + SquarePen, SquareTerminal, Wrench, } from "lucide-react"; -import type { LucideIcon } from "lucide-react"; +import type { ComponentType, SVGProps } from "react"; +import { McpIcon } from "./McpIcon"; + +/** Lucide icons and hand-rolled SVGs (the MCP logo) share this shape. */ +export type ToolIcon = ComponentType>; export type ToolFamily = | "terminal" + | "grep" | "search" | "read" | "edit" @@ -29,7 +36,7 @@ export type ToolFamily = export interface ToolTaxonomyEntry { family: ToolFamily; - icon: LucideIcon; + icon: ToolIcon; /** Progressive verb for the status pill, without the trailing ellipsis. */ verbLabel: string; } @@ -64,11 +71,12 @@ export function mcpToolSegment(name: string): string | null { const FAMILY_META: Record, Omit> = { terminal: { icon: SquareTerminal, verbLabel: "Running a command" }, - search: { icon: FileSearch, verbLabel: "Searching" }, - read: { icon: BookOpen, verbLabel: "Reading files" }, - edit: { icon: Pencil, verbLabel: "Editing files" }, - web: { icon: Globe, verbLabel: "Fetching the web" }, - agent: { icon: Bot, verbLabel: "Delegating" }, + grep: { icon: SearchCode, verbLabel: "Grepping" }, + search: { icon: Search, verbLabel: "Searching" }, + read: { icon: Book, verbLabel: "Reading files" }, + edit: { icon: SquarePen, verbLabel: "Editing files" }, + web: { icon: ChevronsLeftRightEllipsis, verbLabel: "Fetching the web" }, + agent: { icon: Network, verbLabel: "Delegating" }, other: { icon: Wrench, verbLabel: "Working" }, }; @@ -83,7 +91,10 @@ function classify(n: string): Exclude { if (first === "bash" || first === "shell" || first === "run" || n.includes("terminal")) { return "terminal"; } - if (first === "grep" || first === "glob" || n.includes("search")) return "search"; + // Content grep is its own action (search-code glyph); file-name globbing and + // generic searches stay under the plain Search family. + if (first === "grep") return "grep"; + if (first === "glob" || n.includes("search")) return "search"; if (n.includes("fetch") || n.includes("web") || n.includes("http")) return "web"; if (first === "task" || first === "agent") return "agent"; return "other"; @@ -94,7 +105,19 @@ export function toolTaxonomy(name: string | undefined | null): ToolTaxonomyEntry const raw = (name ?? "").trim(); if (!raw) return { family: "other", ...FAMILY_META.other }; const mcpTool = mcpToolSegment(raw); - if (mcpTool) return { family: "mcp", icon: Plug, verbLabel: `Using ${mcpTool}` }; + if (mcpTool) return { family: "mcp", icon: McpIcon, verbLabel: `Using ${mcpTool}` }; const family = classify(raw.toLowerCase()); return { family, ...FAMILY_META[family] }; } + +/** + * Icons for the tool-free informative statuses ("Thinking", "Responding", + * "Responding (streaming)"). Whimsified and generic labels get no glyph — the + * pulse dot alone carries those. + */ +export function statusLabelIcon(label: string | undefined | null): ToolIcon | null { + const raw = (label ?? "").trim().toLowerCase(); + if (raw === "thinking") return Brain; + if (raw === "responding" || raw.startsWith("responding (")) return MessageSquareReply; + return null; +} diff --git a/ui/src/components/task-chat/transcript-adapter.test.ts b/ui/src/components/task-chat/transcript-adapter.test.ts index 8a64d06d52b..a582b066de6 100644 --- a/ui/src/components/task-chat/transcript-adapter.test.ts +++ b/ui/src/components/task-chat/transcript-adapter.test.ts @@ -16,7 +16,7 @@ function toolCall(name: string, input?: unknown): TranscriptEntry { describe("deriveRunStatusLabel", () => { it("labels a tail tool_call with the taxonomy verb and tool · target detail", () => { const status = deriveRunStatusLabel([toolCall("Grep", { pattern: "ui/src/components" })]); - expect(status.label).toBe("Searching"); + expect(status.label).toBe("Grepping"); expect(status.detail).toBe("Grep · ui/src/components"); expect(status.toolName).toBe("Grep"); });