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 <noreply@anthropic.com>
This commit is contained in:
scotttongandClaude Fable 5 committed 2026-08-07 16:56:37 -07:00
1 parent 58a5e1cccb
commit 61387b20e6
6 files changed
+121 -60

No files matched your search

@@ -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).
@@ -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", () => {
+12 -19
View File
@@ -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",
];
@@ -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);
+38 -15
View File
@@ -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<SVGProps<SVGSVGElement>>;
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<Exclude<ToolFamily, "mcp">, Omit<ToolTaxonomyEntry, "family">> = {
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<ToolFamily, "mcp"> {
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;
}
@@ -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");
});