Files
PaperClipAI/ui/src/components/task-chat/task-chat-fixtures.ts
T
scotttongandClaude Fable 5 a7e689b3c3 feat(ui): rename "Agent mode" to "Auto mode" and show full work-mode labels (#11866)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The task composer and the New Task dialog show a work-mode chip (how
the agent will run a task)
> - The default mode was labeled "Agent mode", and the New Task dialog
chip abbreviated every mode to one word ("Auto", "Plan", "Ask")
> - "Agent mode" is confusing because every mode runs an agent, and the
abbreviated chip hid what the label means
> - This pull request renames the mode to "Auto mode" and makes every
mode chip show the full label
> - The benefit is a clearer, consistent mode name in every place the
user selects a work mode

## Linked Issues or Issue Description

No public GitHub issue exists for this change. Description follows the
enhancement template:

**What existing behavior does this improve?**

The work-mode selector chips in the task composer and in the New Task
dialog.

**Subsystem affected**

UI (`ui/src/lib/work-mode-meta.ts`,
`ui/src/components/NewIssueDialog.tsx`).

**Current behavior**

The default work mode is labeled "Agent mode". The New Task dialog chip
shows a shortened label ("Auto", "Plan", "Ask") from a separate
`shortLabel` field.

**Proposed behavior**

The default work mode is labeled "Auto mode". Every chip shows the full
label ("Auto mode", "Plan mode", "Ask mode"). The `shortLabel` field is
removed so no surface can fall back to the short form.

**Reason and benefit**

"Agent mode" does not describe the behavior — all modes use an agent.
"Auto mode" states what the mode does. One label field keeps every
surface consistent.

**Breaking changes**

None. This is a display-string change only. No API, storage, or mode-key
changes.

## What Changed

- Renamed the `standard` work-mode label from "Agent mode" to "Auto
mode" in `ui/src/lib/work-mode-meta.ts`, the single source for all mode
chips.
- Changed the New Task dialog mode chip to render the full `label`
instead of `shortLabel`.
- Deleted the `shortLabel` field from `WorkModeMeta` so nothing can
silently regress to the short form.
- Updated unit tests and fixtures to pin the full labels.

## Verification

- Run `pnpm --filter @paperclipai/ui test --
src/lib/work-mode-meta.test.ts src/components/NewIssueDialog.test.tsx
src/components/IssueChatThread.test.tsx
src/components/task-chat/TaskChatComposer.test.tsx`. All tests pass. The
tests assert the labels are exactly "Auto mode", "Plan mode", and "Ask
mode".
- Manual: start the dev server, open the board, press `c` to open the
New Task dialog, and press Cmd+Period to cycle modes. The chip reads
"Auto mode", "Plan mode", then "Ask mode". The composer chip on an open
task shows the same labels.

## Risks

- Low risk. Display strings only. The chip is a few pixels wider in the
New Task dialog; no layout overflow was observed in any of the three
modes.

## Model Used

- Claude Fable 5 (Anthropic, model ID `claude-fable-5`), extended
thinking with tool use, run inside a Claude Code agent session.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 10:28:55 -07:00

319 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Synthetic fixtures for the Task Chat Redesign dev harness. No live agent is
* required: every state in the inventory maps to a deterministic scenario the
* harness renders and the finish-line test iterates. Tier-B states are driven
* entirely from here (live protocol wiring is a flagged dependency).
*/
import type { TaskChatItem, TaskChatPlan } from "./task-chat-model";
import type { TaskChatStateId } from "./task-chat-states";
export interface TaskChatScenario {
surface: "thread" | "plan";
items: TaskChatItem[];
plan?: TaskChatPlan;
}
const AGENT = "Atlas";
/** A short human→agent exchange used as context in several scenarios. */
function exchangePrefix(): TaskChatItem[] {
return [
{ id: "m-user-1", kind: "message", author: "human", text: "Add a rate limiter to the login route.", timestamp: "2:31 PM" },
];
}
const SAMPLE_PLAN: TaskChatPlan = {
revision: 2,
updatedAt: "2:33 PM",
entries: [
{ id: "p1", content: "Read the login route and existing middleware", status: "completed", priority: "medium" },
{ id: "p2", content: "Add a token-bucket rate limiter util", status: "in_progress", priority: "high" },
{ id: "p3", content: "Wire the limiter into POST /login", status: "pending", priority: "high" },
{ id: "p4", content: "Add tests for the limit + reset window", status: "pending", priority: "low" },
],
};
export function buildScenario(id: TaskChatStateId): TaskChatScenario {
switch (id) {
case "session-start":
return {
surface: "thread",
items: [
{ id: "mk-start", kind: "marker", variant: "session_start", label: "Session started", detail: "claude · Auto mode" },
...exchangePrefix(),
],
};
case "human-message":
return { surface: "thread", items: exchangePrefix() };
case "agent-message":
return {
surface: "thread",
items: [
...exchangePrefix(),
{ id: "m-agent-1", kind: "message", author: "agent", authorName: AGENT, agentIcon: "bot", text: "On it — I'll add a token-bucket limiter and wire it into the login route.", timestamp: "2:31 PM" },
],
};
case "thinking":
// The surviving thinking signal (PAP-361): the live line's "Thinking…"
// state — Brain icon + shimmer on the pill. Thinking rows no longer
// render in the thread or nest under turns.
return {
surface: "thread",
items: [
...exchangePrefix(),
{
id: "turn-thinking",
kind: "turn",
settled: false,
summary: { toolCount: 1, added: 0, removed: 0 },
liveStatus: { id: "st-thinking", kind: "status", status: "running", label: "Thinking", startedAtMs: Date.now() - 6100, tokens: { used: 18240, size: 200000 } },
items: [
{ id: "th-grep", kind: "tool", name: "Grep", target: "rateLimit", toolKind: "search", status: "completed" },
],
},
],
};
case "responding":
// A streaming interstitial update gets its own row directly above the
// status line (PAP-361, amended): it wraps into the 1lh viewport
// line-scroll while the gerund rotation below runs uninterrupted.
// Ephemeral — when it finishes, the row slides out and the text renders
// nowhere.
return {
surface: "thread",
items: [
...exchangePrefix(),
{
id: "turn-responding",
kind: "turn",
settled: false,
summary: { toolCount: 1, added: 0, removed: 0 },
liveStatus: {
id: "st-responding", kind: "status", status: "running", label: "Responding", startedAtMs: Date.now() - 9300, tokens: { used: 18240, size: 200000 },
selfTalk:
"I found an existing ipRateLimit helper, so I'll extend it with a per-account token bucket keyed on the email address instead of adding a second limiter. The bucket refills at six requests a minute, matching the lockout policy the auth spec documents, and failed attempts drain it twice as fast so brute-force runs hit the ceiling quickly while a fat-fingered password barely registers.",
},
items: [
{ id: "resp-read", kind: "tool", name: "Read", target: "server/src/routes/auth.ts", toolKind: "read", status: "completed" },
],
},
],
};
case "responding-burst":
// A run emitting several interstitial updates in quick succession
// (PAP-368): the lab replay streams each blank-line-separated segment as
// its own update with a short gap between. The pill HOLDS each finished
// update until the next swaps in, paced by --motion-interstitial-dwell
// (latest-wins when updates outpace the dwell).
return {
surface: "thread",
items: [
...exchangePrefix(),
{
id: "turn-responding-burst",
kind: "turn",
settled: false,
summary: { toolCount: 2, added: 0, removed: 0 },
liveStatus: {
id: "st-responding-burst", kind: "status", status: "running", label: "Responding", startedAtMs: Date.now() - 21400, tokens: { used: 18240, size: 200000 },
selfTalk:
"Found the existing ipRateLimit helper — extending it beats adding a second limiter.\n\n" +
"Wiring a per-account token bucket keyed on the email address, refilling at six requests a minute per the auth spec.\n\n" +
"Failed attempts drain the bucket twice as fast, so brute-force runs hit the ceiling while a fat-fingered password barely registers.\n\n" +
"Now updating the login route to consume from the bucket before the password check and adding tests for the lockout path.",
},
items: [
{ id: "burst-read", kind: "tool", name: "Read", target: "server/src/routes/auth.ts", toolKind: "read", status: "completed" },
{ id: "burst-grep", kind: "tool", name: "Grep", target: "ipRateLimit", toolKind: "search", status: "completed" },
],
},
],
};
case "tool-call":
return {
surface: "thread",
items: [
{ id: "tool-1", kind: "tool", name: "Read", target: "server/src/routes/auth.ts", toolKind: "read", status: "in_progress" },
],
};
case "diff":
return {
surface: "thread",
items: [
{
id: "tool-diff", kind: "tool", name: "Edit", target: "server/src/routes/auth.ts", toolKind: "edit", status: "completed", decision: "allowed",
diff: {
path: "server/src/routes/auth.ts", added: 3, removed: 1,
lines: [
{ kind: "context", text: "router.post('/login', async (req, res) => {" },
{ kind: "remove", text: " const ok = await checkPassword(req.body);" },
{ kind: "add", text: " await rateLimiter.consume(req.body.email);" },
{ kind: "add", text: " const ok = await checkPassword(req.body);" },
{ kind: "add", text: " if (!ok) return res.status(401).end();" },
],
},
},
],
};
case "working":
// Parent-row live turn (PAP-354): the tool-state line owns the activity;
// expanding nests the chronological history underneath.
return {
surface: "thread",
items: [
...exchangePrefix(),
{
id: "turn-working",
kind: "turn",
settled: false,
summary: { toolCount: 2, added: 0, removed: 0 },
liveStatus: { id: "st-working", kind: "status", status: "working", label: "Editing files", detail: "Edit · server/src/routes/auth.ts", toolName: "Edit", startedAtMs: Date.now() - 4200 },
items: [
{ id: "w-read", kind: "tool", name: "Read", target: "server/src/routes/auth.ts", toolKind: "read", status: "completed" },
{ id: "w-edit", kind: "tool", name: "Edit", target: "server/src/routes/auth.ts", toolKind: "edit", status: "in_progress" },
],
},
],
};
case "running":
// Generic label → the parent row header rotates whimsical gerunds.
return {
surface: "thread",
items: [
...exchangePrefix(),
{
id: "turn-running",
kind: "turn",
settled: false,
summary: { toolCount: 1, added: 0, removed: 0 },
liveStatus: { id: "st-running", kind: "status", status: "running", label: "Running", detail: "no output for 3s — still running", startedAtMs: Date.now() - 12000, tokens: { used: 18240, size: 200000 } },
items: [
{ id: "r-grep", kind: "tool", name: "Grep", target: "rateLimit", toolKind: "search", status: "completed" },
],
},
],
};
case "completed":
return {
surface: "thread",
items: [
...exchangePrefix(),
// Round 9: the settled turn attaches to the final reply bubble — the
// "✓ Worked · …" summary renders on the bubble's always-visible
// timestamp line ("2:34 PM · ✓ Worked · 38s · 2 tools").
{
id: "m-done",
kind: "message",
author: "agent",
authorName: AGENT,
agentIcon: "bot",
text: "Done — added a per-account token-bucket limiter and wired it into the login route. Tests pass.",
timestamp: "2:34 PM",
attachedTurn: {
id: "turn-done",
kind: "turn",
settled: true,
// "Worked · N tools" expands to exactly the tool rows (PAP-361):
// toolCount matches the nested rows, no thinking row.
summary: { durationLabel: "38s", toolCount: 2, added: 34, removed: 3, tokensLabel: "12.3k tokens" },
items: [
{ id: "tool-done-1", kind: "tool", name: "Read", target: "server/src/routes/auth.ts", toolKind: "read", status: "completed" },
{ id: "tool-done-2", kind: "tool", name: "Edit", target: "server/src/routes/auth.ts", toolKind: "edit", status: "completed", diff: { path: "server/src/routes/auth.ts", added: 34, removed: 3 } },
],
},
},
],
};
case "awaiting-approval":
return {
surface: "thread",
items: [
{
id: "st-approval", kind: "status", status: "awaiting_approval", label: "Approve running a command?",
detail: "npm run migrate — modifies the database",
approval: {
toolName: "execute",
options: [
{ id: "reject", label: "Deny", kind: "reject_once" },
{ id: "allow-always", label: "Always allow", kind: "allow_always" },
{ id: "allow", label: "Allow once", kind: "allow_once" },
],
},
},
],
};
case "activity-phases": {
const phase = (id: string, text: string | undefined, active: boolean, tools: TaskChatItem[]) => ({
id,
kind: "activity_phase" as const,
active,
interstitial: text ? { id: `${id}:message`, kind: "message" as const, author: "agent" as const, authorName: AGENT, text, interstitial: true } : undefined,
items: tools.filter((item): item is Extract<TaskChatItem, { kind: "tool" | "usage" }> => item.kind === "tool" || item.kind === "usage"),
summary: active ? "Ran 1 command, called 1 tool" : id.endsWith("opening") ? "Called 2 tools" : "Read 3 files, edited 1 file",
});
return {
surface: "thread",
items: [
...exchangePrefix(),
{
id: "turn-long-run", kind: "turn", settled: false,
summary: { toolCount: 8, added: 4, removed: 1 },
liveStatus: { id: "long-status", kind: "status", status: "working", label: "Running tests", detail: "Bash · vitest", toolName: "Bash", startedAtMs: Date.now() - 48_000 },
items: [
phase("phase-opening", undefined, false, [
{ id: "generic-1", kind: "tool", name: "Tool", rawName: "tool call", status: "completed" },
{ id: "generic-2", kind: "tool", name: "Tool", rawName: "acp_tool", status: "failed", detail: "Adapter interrupted" },
]),
phase("phase-read", "I found the relevant adapter and am tracing its render boundary.", false, [
{ id: "read-1", kind: "tool", name: "Read", status: "completed", target: "ui/src/components/task-chat/transcript-adapter.ts" },
{ id: "read-2", kind: "tool", name: "Read", status: "completed", target: "ui/src/components/task-chat/TaskChatTurn.tsx" },
{ id: "read-3", kind: "tool", name: "Read", status: "completed", target: "ui/src/components/task-chat/TaskChatThreadView.tsx" },
{ id: "edit-1", kind: "tool", name: "Edit", status: "completed", target: "ui/src/components/task-chat/task-chat-model.ts" },
]),
phase("phase-active", "The grouping is wired; I’m running focused checks now.", true, [
{ id: "bash-1", kind: "tool", name: "Bash", status: "in_progress", target: "vitest task-chat" },
{ id: "mcp-1", kind: "tool", name: "Search", rawName: "mcp__docs__search", status: "completed" },
]),
],
},
],
};
}
case "plan-todo":
return { surface: "plan", items: [], plan: SAMPLE_PLAN };
case "interrupted":
return {
surface: "thread",
items: [
{ id: "m-int", kind: "message", author: "agent", authorName: AGENT, text: "Starting the migration now…" },
{ id: "mk-int", kind: "marker", variant: "interrupted", label: "Interrupted", detail: "stopped by you at 2:35 PM" },
],
};
case "refused":
return {
surface: "thread",
items: [
{ id: "st-refused", kind: "status", status: "refused", label: "Turn ended: refusal", detail: "The agent declined to complete this request." },
],
};
case "truncated":
return {
surface: "thread",
items: [
{ id: "st-trunc", kind: "status", status: "truncated", label: "Turn ended: max tokens", detail: "Output was cut off — continue to resume.", tokens: { used: 199120, size: 200000 } },
],
};
case "live-token-cost":
return {
surface: "thread",
items: [
{ id: "usage-1", kind: "usage", usage: { used: 42800, size: 200000, inputTokens: 38200, outputTokens: 4600, costUsd: 0.1284 } },
],
};
default: {
const _never: never = id;
return _never;
}
}
}