mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 06:10:44 +01:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - New users meet Paperclip through the onboarding chat with their
first agent, and through task briefs that agents write
> - A browser walk of onboarding found internal handles and
control-plane jargon in those user-facing surfaces
> - A tool row showed `Read taskId: e3ecf9bc-…`, an agent-written brief
cited `task … (id 2b330993-…)`, and a Codex agent told a new user about
"receipts", "execution policy", and a "rejected policy patch"
> - Raw UUIDs and mechanism narration make the product look broken and
give the user nothing to act on
> - This pull request makes tool rows name a task by its identifier, and
tells agents to reference identifiers and to describe outcomes, not
mechanics
> - The benefit is that a new user sees "Read <task identifier>" and
plain progress updates
## Linked Issues or Issue Description
**What happened?**
In the task chat, a collapsed tool row rendered the raw call argument
`taskId: <uuid>`. An agent-written task description included a raw task
UUID next to the task identifier. A Codex chief of staff narrated
internal mechanics (receipts, execution policies, a rejected policy
patch) to a new user in chat.
**Expected behavior**
Tool rows name the task by its identifier. Agent-written text links
tasks and documents by identifier. Agents tell the user what happened,
what it means, and the next step.
**Steps to reproduce**
1. Run onboarding with the Codex API runner and accept a single-task
proposal.
2. Watch the handoff in the chief-of-staff chat.
3. See the task lookup tool row and the narration. With the Claude API
runner and the plan path, open a child task created from the plan and
read its description.
## What Changed
- `ui/src/components/task-chat/transcript-adapter.ts`: a tool row's
target never contains a UUID argument. A `taskId`/`issueId` UUID is kept
as `taskRefId` on the tool item.
- New `ui/src/components/task-chat/task-reference.ts`:
`useTaskReferenceLabel` reads the task from the shared issue detail
query cache and returns its identifier. It returns "task" until the task
resolves or when it cannot.
- `TaskChatRunnerActivityGroup` and `TaskChatToolCard` show that
identifier as the row subject. They load the task only for rows that
have a task reference.
- `skills/paperclip/SKILL.md` (Comment Style): two new required rules.
"No raw IDs in prose" and "Describe outcomes, not mechanics".
- First-task onboarding skill: a "Talk in outcomes" rule. Non-inline
handoff inputs use an identifier link, not a raw UUID.
- Chief-of-staff persona (`AGENTS.md` for the first agent): the same
plain-language rule, so it applies in every chat.
- `skills/paperclip-converting-plans-to-tasks/SKILL.md`: task
descriptions reference other tasks and documents by identifier link,
never by raw UUID.
- No change to `skills-releases/` snapshots. They are frozen releases.
## Verification
Red on master (fix files reverted, new tests kept):
```
ui tool-row-task-reference.test.tsx
FAIL ... > falls back to a plain word while the task cannot be resolved
AssertionError: expected 'ReadtaskId: e3ecf9bc-099a-4b10-8169-9…' not to contain 'e3ecf9bc-099a-4b10-8169-9ce490a91e3d'
Tests 3 failed (3)
server user-facing-wording-guidance.test.ts
Tests 6 failed (6)
```
Green with the fix:
```
ui tool-row-task-reference.test.tsx Tests 3 passed (3)
server user-facing-wording-guidance.test.ts Tests 6 passed (6)
```
Neighbouring suites:
- `cd ui && npx vitest run src/components/task-chat
src/components/TaskChatThread src/components/transcript`: 70 files, 1448
passed, 1 skipped.
- From the repo root: `onboarding-first-task-assets`,
`paperclip-skill-utils`, `onboarding-first-task-admission`,
`hiring-operational-examples`, `cli-invocation-safety`, and the new
guidance test all pass.
- `pnpm check:token-gates`: all gates clean. The change adds no styles.
- `ui` typecheck: no errors in the changed files.
- Not run locally: `agent-skills-routes.test.ts` hit a 30 s `beforeAll`
import timeout on a cold cache. CI covers it.
## Risks
- Low risk. A row that names a task loads that task once through the
shared issue detail cache. The thread usually has it cached already. A
failed load shows "task" and does not retry.
- Rows now show other non-UUID arguments after the identifier (for
example `<identifier> · status: done`). Rows without a task reference do
not change.
- The skill text changes the guidance that agents read. They do not
change permissions, tools, or API behavior.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, run in Claude Code
with tool use (shell, file edits, test execution).
## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.7 KiB
TypeScript
168 lines
6.7 KiB
TypeScript
import { useTaskChatExpansion } from "./expansion-state";
|
||
import { cn } from "@/lib/utils";
|
||
import {
|
||
Check,
|
||
ChevronRight,
|
||
Loader2,
|
||
ShieldCheck,
|
||
ShieldX,
|
||
X,
|
||
} from "lucide-react";
|
||
import type { TaskChatToolItem } from "./task-chat-model";
|
||
import { toolTaxonomy } from "./tool-taxonomy";
|
||
import { useTaskChatShimmer } from "./TaskChatShimmer";
|
||
import { TaskChatDiffCounts, TaskChatDiffView } from "./TaskChatDiffView";
|
||
import { useTaskReferenceLabel, withTaskReference } from "./task-reference";
|
||
|
||
const STATUS_ICON = {
|
||
pending: { Icon: Loader2, spin: false, tone: "text-muted-foreground" },
|
||
in_progress: {
|
||
Icon: Loader2,
|
||
spin: true,
|
||
tone: "text-(--status-agent-running)",
|
||
},
|
||
completed: { Icon: Check, spin: false, tone: "text-muted-foreground" },
|
||
failed: { Icon: X, spin: false, tone: "text-destructive" },
|
||
interrupted: { Icon: X, spin: false, tone: "text-muted-foreground" },
|
||
} as const;
|
||
|
||
function ToolTarget({ text }: { text: string }) {
|
||
return (
|
||
<span className="task-chat-collapsed-line-fade min-w-0 flex-1 font-mono text-(length:--text-micro) leading-4">
|
||
{text}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
/** Names the task a call is about by identifier; its UUID is never shown. */
|
||
function TaskReferenceToolTarget({ taskRefId, target }: { taskRefId: string; target?: string }) {
|
||
const taskLabel = useTaskReferenceLabel(taskRefId);
|
||
return <ToolTarget text={withTaskReference(target, taskLabel)!} />;
|
||
}
|
||
|
||
/**
|
||
* Tool invocation as a flat activity row (v7): [glyph] [name] [mono target] …
|
||
* [hover chevron] [status]. No card chrome — activity is metadata, not
|
||
* content. Clicking toggles a compact result/change summary (left-rail
|
||
* inset). An edit row carries its `+N −M` counts and opens to a compact,
|
||
* height-capped diff of that one edit.
|
||
*/
|
||
export function TaskChatToolCard({ item }: { item: TaskChatToolItem }) {
|
||
const { Icon, spin, tone } = STATUS_ICON[item.status];
|
||
const shimmer = useTaskChatShimmer(item.id);
|
||
const RowIcon = toolTaxonomy(item.rawName ?? item.name).icon;
|
||
const [showDetail, setShowDetail] = useTaskChatExpansion(item.id, false);
|
||
const expandable = Boolean(item.target || item.detail || item.diff);
|
||
|
||
return (
|
||
<div data-testid="task-chat-tool-card" className="flex min-w-0 max-w-full flex-col text-xs">
|
||
<button
|
||
type="button"
|
||
onClick={expandable ? () => setShowDetail((v) => !v) : undefined}
|
||
aria-expanded={expandable ? showDetail : undefined}
|
||
className={cn(
|
||
"group/tool -mx-1.5 flex min-h-6 w-full min-w-0 max-w-full items-center gap-2 overflow-hidden rounded-sm px-1.5 py-1 text-left leading-none text-muted-foreground",
|
||
expandable
|
||
? "cursor-pointer transition-colors hover:bg-muted/60 hover:text-foreground"
|
||
: "cursor-default",
|
||
)}
|
||
>
|
||
<span className="flex w-5 shrink-0 items-center justify-center">
|
||
<RowIcon
|
||
className="h-3.5 w-3.5 shrink-0"
|
||
aria-hidden
|
||
data-testid="task-chat-tool-icon"
|
||
/>
|
||
</span>
|
||
<span data-conversation-tool-running={item.status === "in_progress" && shimmer ? "true" : undefined} className="flex min-w-0 flex-1 items-baseline gap-2 overflow-hidden">
|
||
<span className="shrink-0 font-medium leading-4">{item.name}</span>
|
||
{item.taskRefId ? (
|
||
<TaskReferenceToolTarget taskRefId={item.taskRefId} target={item.target} />
|
||
) : item.target ? (
|
||
<ToolTarget text={item.target} />
|
||
) : null}
|
||
</span>
|
||
<span className="ml-auto flex shrink-0 items-center gap-2">
|
||
{item.diff && (item.diff.added > 0 || item.diff.removed > 0) ? (
|
||
<TaskChatDiffCounts
|
||
added={item.diff.added}
|
||
removed={item.diff.removed}
|
||
className="text-(length:--text-micro)"
|
||
/>
|
||
) : null}
|
||
{item.decision ? (
|
||
<span
|
||
className={cn(
|
||
"flex items-center gap-1 text-(length:--text-micro)",
|
||
item.decision === "allowed"
|
||
? "text-(--status-task-icon-done)"
|
||
: "text-destructive",
|
||
)}
|
||
>
|
||
{item.decision === "allowed" ? (
|
||
<ShieldCheck className="h-3 w-3" />
|
||
) : (
|
||
<ShieldX className="h-3 w-3" />
|
||
)}
|
||
{item.decision}
|
||
</span>
|
||
) : null}
|
||
{expandable ? (
|
||
<ChevronRight
|
||
className={cn(
|
||
"h-3 w-3 opacity-0 transition-[opacity,transform] group-hover/tool:opacity-80",
|
||
showDetail ? "rotate-90" : null,
|
||
)}
|
||
aria-hidden
|
||
/>
|
||
) : null}
|
||
<Icon
|
||
className={cn("h-3.5 w-3.5", tone, spin && "animate-spin")}
|
||
aria-hidden
|
||
/>
|
||
{item.status === "interrupted" ? (
|
||
<span className="text-(length:--text-micro) text-muted-foreground">
|
||
Interrupted
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
</button>
|
||
{item.target && showDetail && !item.diff?.lines?.length ? (
|
||
<div
|
||
className="task-chat-expanded-line-wrap ml-1.5 mt-0.5 min-w-0 max-w-full overflow-hidden border-l-2 border-border py-1 pl-2.5 font-mono text-(length:--text-micro) leading-relaxed text-muted-foreground"
|
||
data-testid="task-chat-tool-target-detail"
|
||
>
|
||
{item.target}
|
||
</div>
|
||
) : null}
|
||
{item.detail && showDetail ? (
|
||
<pre className="task-chat-expanded-line-wrap ml-1.5 mt-0.5 min-w-0 max-w-full overflow-hidden whitespace-pre-wrap border-l-2 border-border py-1 pl-2.5 font-mono text-(length:--text-micro) leading-relaxed text-muted-foreground">
|
||
{item.detail}
|
||
</pre>
|
||
) : null}
|
||
{item.diff && showDetail ? (
|
||
item.diff.lines && item.diff.lines.length > 0 ? (
|
||
<div className="ml-1.5 mt-1 min-w-0 max-w-full" data-testid="task-chat-tool-diff">
|
||
<TaskChatDiffView lines={item.diff.lines} truncated={item.diff.truncated} />
|
||
</div>
|
||
) : (
|
||
<div
|
||
className="ml-1.5 mt-0.5 border-l-2 border-border py-1 pl-2.5"
|
||
data-testid="task-chat-tool-change-summary"
|
||
>
|
||
<div className="flex min-w-0 items-center gap-2 text-(length:--text-micro) text-muted-foreground">
|
||
<span className="shrink-0">Changed</span>
|
||
{item.diff.path ? (
|
||
<span className="min-w-0 truncate font-mono text-foreground">
|
||
{item.diff.path}
|
||
</span>
|
||
) : null}
|
||
<TaskChatDiffCounts added={item.diff.added} removed={item.diff.removed} className="ml-auto" />
|
||
</div>
|
||
</div>
|
||
)
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|