Files
PaperClipAI/ui/src/components/task-chat/TaskChatProtocolActivityRow.test.tsx
T
DottaandPaperclip 5cb4f061dd fix(ui): simplify provider notices and hide completion calls (#13109)
## 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>
2026-09-09 15:06:20 -05:00

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();
});
});