mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 21:49:18 +01:00
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:
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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user