mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 08:53:19 +01:00
feat(task-chat): show web searches, model switches, waits and verification checks as dedicated rows (#19262)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agents run through the native runner. The runner turns harness activity into typed canonical events such as `research.*`, `model.route.changed`, `wait.*` and the run's own verification checks. > - The task chat flattened these events into generic tool rows. A web search showed as "Ran Research eventType: research.started, summary: …". The run's verification checks were dropped completely. > - Live tests found a second break: the Rust runner core forwarded Codex `webSearch` items without their query, action or results. The canonical `research.*` events therefore had `query: null`. > - This pull request keeps the full payload for these families (as #19252 did for plans), adds small dedicated rows, and keeps the web-search fields through the durable runner. > - The benefit is that users see what the agent searched for, which model actually ran, how long a wait lasts, and whether each check passed, without opening the run log. ## Linked Issues or Issue Description Refs #19252 (same pattern for the plan card) **Subsystem affected** Task chat activity rows (UI), the native run-event transcript mapping, and the Rust runner core Codex normalizer. **Problem or motivation** Provider activity that the runner already maps to canonical events reaches the task chat as unreadable generic rows. Verification checks never appear. For Codex, the web-search query and action are lost before they reach the canonical event. **Proposed solution** Keep `research`, `model_identity`, `artifact`, `review` and `wait` events as `provider_activity` entries with their full payload. Project the runner's `verification` items as a new `verification` family. Render dedicated rows: a web search with its quoted query and source-domain chips, a model switch as `from → to · reason`, a wait with a live countdown, and green/red check lines. Keep bounded web-search and sleep fields in the Rust normalizer. **Alternatives considered** A separate "Checks" block beside the final answer. We kept the checks in the activity group, because the group summary already leads with "Check failed" when a check fails. ## What Changed - `ui/src/components/transcript/native-run-events.ts`: keep research, model, artifact, review and wait events as `provider_activity` with the full payload. Map `item.completed` items of kind `verification` to the new `verification` family. Context, hook, safety and delegation stay on the generic path, because other work items own them. - `packages/adapter-utils/src/types.ts`, `ui/src/components/task-chat/task-chat-model.ts`, `ui/src/lib/issue-chat-messages.ts`: add the `verification` provider-activity family. - `ui/src/components/task-chat/task-chat-activity-presentation.ts`: add labels: "Searched the web “query”", "Opened a web page host/path", "Switched models a → b · reason", "Waiting 30s" / "Waited 30s", and "Check passed" / "Check failed" / "Check not run". - `ui/src/components/task-chat/TaskChatRunnerActivityGroup.tsx` and `TaskChatProtocolActivityRow.tsx`: show up to three source-domain chips under a finished search, a live countdown for a running wait, and a check line with a tinted icon and its detail. - `ui/src/components/task-chat/completed-activity-summary.ts`: the group summary leads with "Check failed" when a check fails, and says "Switched models" for a reroute. - `ui/src/components/task-chat/transcript-adapter.ts`: verification detail keys, and a `startedAt` timestamp that stays across lifecycle merges (used by the countdown). - `packages/paperclip-runner/runner/crates/runner-core/src/provider_events.rs`: keep bounded `query`, `action` and http(s) `results` for Codex `webSearch` items, and `durationMs` for `sleep` items. Other item types do not change. - `ui/storybook/stories/tasks/task-chat-provider-activity.stories.tsx`: new story. Its fixtures are persisted run events that go through the production transcript path. ## Verification - `pnpm --filter @paperclipai/ui exec vitest run src/components/transcript src/components/task-chat src/components/TaskChatThread.test.tsx src/adapters/paperclip-runner src/lib`: 275 files and 3669 tests pass. - `cargo test -p paperclip-runner-core`: all tests pass, including the new `keeps_bounded_web_search_and_sleep_identity`. - UI typecheck is clean. `pnpm check:token-gates` is clean. `cargo fmt --check` is clean. - Storybook: open "Tasks and conversations / … / Task chat / Provider activity". It shows a settled run, a live search, a live wait countdown and a narrow layout. - Live test on a local worktree instance with a real Codex agent (gpt-5.6-sol, Codex app-server 0.160). The task asked for a web search, a parser with tests, a pause, and two checks. The task chat showed "Searched the web “ISO 8601 week date first week Thursday conversion calendar date”" and two green check rows ("node --test week-date.test.js · 8 tests passed; 0 failed.", "node --check week-date.js"). The group summary read "Ran checks". Before the runner fix, the same search had `query: null`. Not verified live: - The source-domain chips. Codex 0.160 sends `results: null`, because it keeps search results on the server. The chips appear only when a provider reports sources. Unit tests and the story cover them. - The wait countdown and the model-switch row. In the live run Codex paused with a shell `sleep`, not a native `sleep` item, and no reroute occurred. Unit tests and the story cover both. - Claude. Its WebSearch and WebFetch calls arrive as generic tool executions, because ACPX drops the tool metadata. They show as tool rows, as before. The ACPX `_meta` work is separate. ## Risks - Low risk. The change only affects how canonical events display. The generic path stays for the families that this PR does not change. - The Rust normalizer now persists the web-search query and the source URLs/titles in the run events. This data was already intended for the canonical `research.v1` event, and it is bounded (4,000 characters per text field, 64 results, http(s) URLs only). - A search that Codex reports with `action: other` and no query shows as "Searched the web" with no subject. > 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`) in Claude Code, with tool use (shell, file edit, browser automation) and extended reasoning. ## 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 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
053ae3920f
commit
b0fa2ef9bf
16 files changed
+909
-32
No files matched your search
@@ -10,6 +10,7 @@ import { TaskChatBubble } from "./TaskChatBubble";
|
||||
import { TaskChatExpansionState } from "./expansion-state";
|
||||
import type {
|
||||
TaskChatActivityPhaseItem,
|
||||
TaskChatProviderActivityItem,
|
||||
TaskChatToolItem,
|
||||
} from "./task-chat-model";
|
||||
|
||||
@@ -31,6 +32,27 @@ const tool = (
|
||||
detail: `output-${id}`,
|
||||
});
|
||||
|
||||
const providerItem = (
|
||||
id: string,
|
||||
family: TaskChatProviderActivityItem["family"],
|
||||
status: TaskChatProviderActivityItem["status"],
|
||||
details: TaskChatProviderActivityItem["details"],
|
||||
extra: Partial<TaskChatProviderActivityItem> = {},
|
||||
): TaskChatProviderActivityItem => ({
|
||||
id,
|
||||
kind: "protocol",
|
||||
surface: "provider_activity",
|
||||
family,
|
||||
eventType: `${family}.activity`,
|
||||
status,
|
||||
title: family,
|
||||
details,
|
||||
steps: [],
|
||||
links: [],
|
||||
children: [],
|
||||
...extra,
|
||||
});
|
||||
|
||||
describe("TaskChatRunnerActivityGroup", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
@@ -334,4 +356,61 @@ describe("TaskChatRunnerActivityGroup", () => {
|
||||
),
|
||||
).toBeNull();
|
||||
});
|
||||
it("shows research sources, check outcomes and a wait countdown in expanded history", () => {
|
||||
const search = providerItem("search", "research", "completed", [
|
||||
{ label: "Action", value: "search" },
|
||||
{ label: "Query", value: "temporal week dates" },
|
||||
], {
|
||||
links: [
|
||||
{ label: "MDN", href: "https://developer.mozilla.org/a" },
|
||||
{ label: "MDN again", href: "https://developer.mozilla.org/b" },
|
||||
{ label: "TC39", href: "https://tc39.es/c" },
|
||||
],
|
||||
});
|
||||
const passed = providerItem("check-1", "verification", "completed", [
|
||||
{ label: "Status", value: "passed" },
|
||||
{ label: "Detail", value: "6 tests passed" },
|
||||
], { summary: "node --test" });
|
||||
const failed = providerItem("check-2", "verification", "failed", [
|
||||
{ label: "Status", value: "failed" },
|
||||
], { summary: "pnpm typecheck" });
|
||||
const skipped = providerItem("check-3", "verification", "informational", [
|
||||
{ label: "Status", value: "not_run" },
|
||||
{ label: "Reason Code", value: "policy_restricted" },
|
||||
], { summary: "pnpm test:e2e" });
|
||||
const longFailure = "x".repeat(300);
|
||||
const failedLong = providerItem("check-4", "verification", "failed", [
|
||||
{ label: "Status", value: "failed" },
|
||||
{ label: "Detail", value: longFailure },
|
||||
], { summary: "pnpm typecheck" });
|
||||
const wait = providerItem("wait", "wait", "running", [
|
||||
{ label: "Planned Duration Ms", value: "30000" },
|
||||
], { startedAt: new Date(Date.now() - 12_000).toISOString() });
|
||||
render([search, passed, failed, skipped, failedLong, wait], "live", false);
|
||||
act(() => toggle().click());
|
||||
|
||||
const strip = container.querySelector('[data-testid="task-chat-research-sources"]');
|
||||
expect([...strip!.querySelectorAll("a")].map((link) => link.textContent)).toEqual(["developer.mozilla.org", "tc39.es"]);
|
||||
expect(strip?.textContent).toContain("+1 more");
|
||||
expect(container.textContent).toContain("Searched the web“temporal week dates”");
|
||||
|
||||
const passedRow = container.querySelector('[data-activity-row="check-1"]')!;
|
||||
expect(passedRow.textContent).toContain("Check passed");
|
||||
expect(passedRow.textContent).toContain("6 tests passed");
|
||||
expect(passedRow.querySelector("[data-activity-icon] svg")?.getAttribute("class")).toContain("--status-task-icon-done");
|
||||
const failedRow = container.querySelector('[data-activity-row="check-2"]')!;
|
||||
expect(failedRow.textContent).toContain("Check failed");
|
||||
expect(failedRow.querySelector("[data-activity-icon] svg")?.getAttribute("class")).toContain("text-destructive");
|
||||
expect(failedRow.closest("button")).toBeNull();
|
||||
expect(container.querySelector('[data-activity-row="check-3"]')?.textContent).toContain("Policy restricted");
|
||||
|
||||
const longRow = container.querySelector<HTMLButtonElement>('[data-activity-item-id="check-4"] button')!;
|
||||
act(() => longRow.click());
|
||||
const detail = container.querySelector('[data-activity-item-id="check-4"] [data-testid="task-chat-verification-details"]');
|
||||
expect(detail?.textContent).toContain("pnpm typecheck");
|
||||
expect(detail?.textContent).toContain(longFailure);
|
||||
|
||||
expect(container.querySelector('[data-activity-row="wait"]')?.textContent).toContain("Waiting 30s");
|
||||
expect(container.querySelector('[data-testid="task-chat-wait-countdown"]')?.textContent).toMatch(/^1[78]s left$/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user