mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-12 08:19:54 +02:00
## Thinking Path
> - Paperclip shows agent work in task feeds.
> - Native tools and provider notices appear in that feed.
> - A routine completion call adds no useful action for the user.
> - A provider notice needs readable text to explain its warning.
> - This change hides the completion call and displays the notice
summary.
> - Saved run events remain available for inspection.
## Linked Issues or Issue Description
**What happened?**
The feed showed paperclip_finish as a normal tool call. Provider notices
showed
a generic name or a large key-value table instead of a clear warning
message.
**Expected behavior**
Hide the routine completion call from the task feed. Show a warning
icon,
a short severity heading, and the full notice summary across the row.
**Steps to reproduce**
1. Open a native Codex task that calls paperclip_finish.
2. Inspect its tool activity in the task feed.
3. Inspect a run with a repository-trust provider notice.
**Paperclip version or commit**
Reproduced on the implementation checkout. Replayed onto master at
6abeb6733.
The search found no duplicate PR for this display change.
**Deployment mode**
Local source checkout with native task feeds.
## What Changed
- Hide paperclip_finish calls and results in task-feed adapters.
- Preserve the raw events for run-log inspection.
- Carry provider-notice text into the transcript.
- Render notices with an icon, severity heading, and full-width summary.
- Cover the display and transcript mapping with regression tests.
## Verification
- The affected UI tests and the token gates passed on the implementation
checkout.
- Browser inspection confirmed the warning text is readable and
completion calls are hidden.
- Repository typecheck and build passed. Repository test groups passed
after resource retests.
- On this PR branch, 123 focused UI tests and the token gates passed.
All GitHub checks passed; Greptile is 5/5 with no unresolved threads.
## Risks
- The task feed hides one known internal tool. Raw run events remain
unchanged.
- Long warning text must wrap within the available width.
- No provider warning classification, accounting, or recovery behavior
changes here.
## Model Used
OpenAI Codex, GPT-6 (`gpt-6-astra`). Used for reasoning, code edits,
tool use,
and test execution. The exact context-window limit is not exposed in
this
session. Real-provider acceptance used Codex CLI 0.153.4 with
`gpt-5.6-sol`.
## 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
#` 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
Co-authored-by: Paperclip <noreply@paperclip.ing>
206 lines
8.1 KiB
TypeScript
206 lines
8.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { act } from "react";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { ThemeProvider } from "@/context/ThemeContext";
|
|
import { MemoryRouter } from "@/lib/router";
|
|
import type { TaskChatProtocolItem, TaskChatProviderActivityItem } from "./task-chat-model";
|
|
import { TaskChatProtocolActivityRow } from "./TaskChatProtocolActivityRow";
|
|
|
|
describe("TaskChatProtocolActivityRow", () => {
|
|
let container: HTMLDivElement;
|
|
let root: Root;
|
|
|
|
beforeEach(() => {
|
|
container = document.createElement("div");
|
|
document.body.appendChild(container);
|
|
root = createRoot(container);
|
|
});
|
|
|
|
afterEach(() => {
|
|
act(() => root.unmount());
|
|
container.remove();
|
|
});
|
|
|
|
function render(item: TaskChatProtocolItem) {
|
|
act(() => root.render(
|
|
<MemoryRouter>
|
|
<ThemeProvider>
|
|
<TaskChatProtocolActivityRow item={item} />
|
|
</ThemeProvider>
|
|
</MemoryRouter>,
|
|
));
|
|
}
|
|
|
|
it("renders long research results as a bounded readable list", () => {
|
|
const item: TaskChatProviderActivityItem = {
|
|
id: "research",
|
|
kind: "protocol",
|
|
surface: "provider_activity",
|
|
family: "research",
|
|
eventType: "research.completed",
|
|
status: "completed",
|
|
title: "Research",
|
|
summary: "smoked pork shoulder",
|
|
details: [
|
|
{ label: "Action", value: "search" },
|
|
{ label: "Status", value: "completed" },
|
|
{ label: "Query", value: "site:nextjs.org/docs/app/api-reference/config/eslint Next.js 16 eslint.config.mjs core-web-vitals typescript", mono: true },
|
|
],
|
|
steps: [],
|
|
links: Array.from({ length: 6 }, (_, index) => ({
|
|
label: `Next.js configuration result ${index + 1}`,
|
|
href: `https://nextjs.org/docs/result-${index + 1}`,
|
|
description: "A long provider-authored snippet that stays on one compact line in the activity feed.",
|
|
})),
|
|
children: [],
|
|
};
|
|
render(item);
|
|
|
|
const row = container.querySelector<HTMLButtonElement>('[data-testid="task-chat-protocol-activity-row"] button');
|
|
expect(container.querySelector("article")).toBeNull();
|
|
expect(row?.textContent).toContain("Searched the web");
|
|
expect(row?.textContent).toContain("smoked pork shoulder");
|
|
expect(
|
|
row
|
|
?.querySelector('[data-testid="task-chat-protocol-activity-icon"]')
|
|
?.parentElement?.classList.contains("justify-center"),
|
|
).toBe(true);
|
|
expect(
|
|
row?.querySelector(".task-chat-collapsed-line-fade")?.textContent,
|
|
).toContain("smoked pork shoulder");
|
|
expect(row?.getAttribute("aria-expanded")).toBe("false");
|
|
expect(row?.getAttribute("aria-controls")).toMatch(/^task-chat-protocol-activity-/);
|
|
|
|
act(() => row?.click());
|
|
|
|
expect(row?.getAttribute("aria-expanded")).toBe("true");
|
|
const detail = container.querySelector('[data-testid="task-chat-protocol-activity-detail"]');
|
|
expect(detail?.id).toBe(row?.getAttribute("aria-controls"));
|
|
expect(detail?.querySelector('[data-testid="task-chat-research-query"]')?.textContent).toContain("eslint.config.mjs");
|
|
expect(detail?.querySelectorAll('ol[aria-label="Research sources"] li')).toHaveLength(5);
|
|
expect(detail?.textContent).toContain("Show 1 more result");
|
|
expect(detail?.textContent).not.toContain("Actionsearch");
|
|
|
|
const showMore = Array.from(detail?.querySelectorAll("button") ?? []).find((button) => button.textContent?.includes("Show 1 more result"));
|
|
act(() => showMore?.click());
|
|
expect(detail?.querySelectorAll('ol[aria-label="Research sources"] li')).toHaveLength(6);
|
|
});
|
|
|
|
it("summarizes workspace files without rendering inline diff bodies", () => {
|
|
render({
|
|
id: "workspace",
|
|
kind: "protocol",
|
|
surface: "workspace_change",
|
|
changeSetId: "change-1",
|
|
revision: 1,
|
|
source: "runner_verified",
|
|
complete: true,
|
|
files: Array.from({ length: 10 }, (_, index) => ({
|
|
path: `ui/src/file-${index + 1}.tsx`,
|
|
operation: "modify" as const,
|
|
previousPath: null,
|
|
additions: 1,
|
|
deletions: 1,
|
|
binary: false,
|
|
diff: `-old-${index + 1}\n+new-${index + 1}`,
|
|
})),
|
|
totals: { files: 10, additions: 10, deletions: 10 },
|
|
patchArtifactRef: null,
|
|
});
|
|
|
|
const row = container.querySelector<HTMLButtonElement>('[data-testid="task-chat-protocol-activity-row"] button');
|
|
expect(row?.textContent).toContain("Edited files");
|
|
act(() => row?.click());
|
|
expect(container.textContent).toContain("ui/src/file-1.tsx");
|
|
expect(container.textContent).toContain("2 more files not shown");
|
|
expect(container.textContent).not.toContain("new-1");
|
|
expect(container.querySelector('[data-testid="task-chat-workspace-change-details"] pre')).toBeNull();
|
|
});
|
|
|
|
it("renders durable resources as direct compact links", () => {
|
|
render({
|
|
id: "resource",
|
|
kind: "protocol",
|
|
surface: "resource",
|
|
resourceKind: "document",
|
|
title: "Implementation notes",
|
|
subtitle: "markdown · ready",
|
|
href: "/documents/notes",
|
|
});
|
|
|
|
const link = container.querySelector<HTMLAnchorElement>('[data-testid="task-chat-protocol-activity-row"]');
|
|
expect(link?.getAttribute("href")).toBe("/documents/notes");
|
|
expect(link?.textContent).toContain("Added a document");
|
|
expect(link?.textContent).toContain("Implementation notes");
|
|
});
|
|
|
|
it("renders MCP executions with semantic copy, source metadata, and the MCP icon", () => {
|
|
render({
|
|
id: "tool",
|
|
kind: "protocol",
|
|
surface: "provider_activity",
|
|
family: "tool_execution",
|
|
eventType: "tool.execution.completed",
|
|
status: "completed",
|
|
title: "Tool execution",
|
|
summary: "Searching the task index",
|
|
details: [
|
|
{ label: "Transport", value: "mcp" },
|
|
{ label: "Namespace", value: "paperclip", mono: true },
|
|
{ label: "Operation", value: "search" },
|
|
{ label: "Name", value: "search_tasks", mono: true },
|
|
],
|
|
steps: [],
|
|
links: [],
|
|
children: [],
|
|
});
|
|
|
|
const row = container.querySelector('[data-testid="task-chat-protocol-activity-row"]');
|
|
expect(row?.textContent).toContain("Searched tasks");
|
|
expect(row?.textContent).toContain("Searching the task index · Paperclip · search_tasks");
|
|
expect(row?.textContent).not.toMatch(/Ran a tool|Tool execution|tool call/i);
|
|
expect(row?.querySelector('[data-testid="task-chat-protocol-activity-icon"]')?.querySelectorAll("path")).toHaveLength(3);
|
|
});
|
|
|
|
it("shows a notice as a warning and full-width message without metadata or a disclosure", () => {
|
|
const summary = "Project-local configuration is disabled.\nTrust the repository to load its hooks.";
|
|
render({
|
|
id: "notice", kind: "protocol", surface: "provider_activity", family: "provider_notice",
|
|
eventType: "provider.notice.recorded", status: "informational", title: "Provider notice", summary,
|
|
details: [
|
|
{ label: "Category", value: "configWarning" },
|
|
{ label: "Recoverable", value: "Yes" },
|
|
{ label: "Summary", value: summary },
|
|
], steps: [], links: [], children: [],
|
|
});
|
|
expect(container.textContent).toBe(`Warning${summary}`);
|
|
expect(container.querySelector("p")?.textContent).toBe(summary);
|
|
expect(container.querySelector("dl")).toBeNull();
|
|
expect(container.querySelector("button")).toBeNull();
|
|
expect(container.querySelector('[data-testid="task-chat-protocol-activity-icon"]')).not.toBeNull();
|
|
});
|
|
|
|
it("does not make an informational row focusable when it has no details", () => {
|
|
render({
|
|
id: "notice",
|
|
kind: "protocol",
|
|
surface: "provider_activity",
|
|
family: "provider_notice",
|
|
eventType: "provider.notice.recorded",
|
|
status: "informational",
|
|
title: "Provider notice",
|
|
summary: "Retrying automatically",
|
|
details: [],
|
|
steps: [],
|
|
links: [],
|
|
children: [],
|
|
});
|
|
|
|
const row = container.querySelector('[data-testid="task-chat-protocol-activity-row"]');
|
|
expect(row?.querySelector("button")).toBeNull();
|
|
expect(row?.querySelector('[aria-expanded]')).toBeNull();
|
|
});
|
|
});
|