mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:03:27 +01:00
refactor(task-chat): compact search and file activity
This commit is contained in:
1 parent
0f2b643c78
commit
24f5d5c603
7 files changed
+217
-58
No files matched your search
@@ -59,7 +59,7 @@ describe("TaskChatLiveTail", () => {
|
||||
expect(container.textContent).toContain("src/app.ts");
|
||||
});
|
||||
|
||||
it("renders a tool's diff inset", () => {
|
||||
it("keeps tool diff bodies out of the activity feed", () => {
|
||||
const items = parse([
|
||||
{ kind: "tool_call", ts: TS, name: "Edit", toolUseId: "t1", input: { file_path: "a.ts" } },
|
||||
{ kind: "diff", ts: TS, changeType: "add", text: "const x = 1;" },
|
||||
@@ -69,8 +69,16 @@ describe("TaskChatLiveTail", () => {
|
||||
|
||||
const phaseSummary = container.querySelector<HTMLButtonElement>('[data-testid="task-chat-phase-summary"]');
|
||||
expect(phaseSummary?.getAttribute("aria-expanded")).toBe("true");
|
||||
expect(container.textContent).toContain("const x = 1;");
|
||||
expect(container.textContent).not.toContain("const x = 1;");
|
||||
expect(container.textContent).not.toContain("+1 −1");
|
||||
|
||||
const tool = container.querySelector<HTMLButtonElement>(".tc-enter-tool button");
|
||||
expect(tool).not.toBeNull();
|
||||
flushSync(() => tool?.click());
|
||||
|
||||
expect(container.textContent).toContain("+1 −1");
|
||||
expect(container.textContent).not.toContain("const x = 1;");
|
||||
expect(container.querySelector('[data-testid="task-chat-tool-change-summary"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("drops the debug plumbing kinds RunTranscriptView surfaced", () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("TaskChatProtocolActivityRow", () => {
|
||||
));
|
||||
}
|
||||
|
||||
it("renders research as one compact line with expandable sources", () => {
|
||||
it("renders long research results as a bounded readable list", () => {
|
||||
const item: TaskChatProviderActivityItem = {
|
||||
id: "research",
|
||||
kind: "protocol",
|
||||
@@ -43,9 +43,17 @@ describe("TaskChatProtocolActivityRow", () => {
|
||||
status: "completed",
|
||||
title: "Research",
|
||||
summary: "smoked pork shoulder",
|
||||
details: [{ label: "Query", value: "smoked pork shoulder", mono: true }],
|
||||
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: [{ label: "Smoking guide", href: "https://example.com/smoking", description: "Temperature targets" }],
|
||||
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);
|
||||
@@ -62,10 +70,17 @@ describe("TaskChatProtocolActivityRow", () => {
|
||||
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?.textContent).toContain("Smoking guide");
|
||||
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("keeps workspace files and diffs inside the row detail", () => {
|
||||
it("summarizes workspace files without rendering inline diff bodies", () => {
|
||||
render({
|
||||
id: "workspace",
|
||||
kind: "protocol",
|
||||
@@ -74,16 +89,26 @@ describe("TaskChatProtocolActivityRow", () => {
|
||||
revision: 1,
|
||||
source: "runner_verified",
|
||||
complete: true,
|
||||
files: [{ path: "ui/src/App.tsx", operation: "modify", previousPath: null, additions: 1, deletions: 1, binary: false, diff: "-old\n+new" }],
|
||||
totals: { files: 1, additions: 1, deletions: 1 },
|
||||
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/App.tsx");
|
||||
expect(container.textContent).toContain("+new");
|
||||
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", () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MarkdownBody } from "@/components/MarkdownBody";
|
||||
import { WorkspaceFileLink } from "@/components/WorkspaceFileLink";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
TaskChatProtocolDetail,
|
||||
TaskChatProtocolItem,
|
||||
TaskChatProtocolStep,
|
||||
TaskChatProviderActivityItem,
|
||||
@@ -23,6 +24,9 @@ import {
|
||||
protocolActivityPresentation,
|
||||
} from "./task-chat-activity-presentation";
|
||||
|
||||
const COMPACT_RESEARCH_RESULT_LIMIT = 5;
|
||||
const COMPACT_WORKSPACE_FILE_LIMIT = 8;
|
||||
|
||||
function StatusIcon({ status }: { status: "running" | "completed" | "failed" | "interrupted" | "informational" }) {
|
||||
if (status === "running") return <Loader2 className="h-3.5 w-3.5 animate-spin text-(--status-agent-running)" aria-hidden />;
|
||||
if (status === "failed" || status === "interrupted") return <X className="h-3.5 w-3.5 text-destructive" aria-hidden />;
|
||||
@@ -37,7 +41,91 @@ function stepStatusIcon(status: TaskChatProtocolStep["status"]) {
|
||||
return <Circle className="h-3 w-3 text-muted-foreground" aria-hidden />;
|
||||
}
|
||||
|
||||
function DetailList({ details }: { details: readonly TaskChatProtocolDetail[] }) {
|
||||
if (details.length === 0) return null;
|
||||
return (
|
||||
<dl className="flex min-w-0 flex-col gap-1.5" data-testid="task-chat-provider-detail-list">
|
||||
{details.map((detail) => (
|
||||
<div className="grid min-w-0 grid-cols-1 gap-0.5 sm:grid-cols-(--gtc-task-chat-details) sm:gap-3" key={`${detail.label}:${detail.value}`}>
|
||||
<dt className="text-muted-foreground">{detail.label}</dt>
|
||||
<dd className={cn("min-w-0 break-words text-foreground", detail.mono && "font-mono")}>{detail.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function sourceHostname(href: string): string {
|
||||
try {
|
||||
return new URL(href).hostname.replace(/^www\./, "");
|
||||
} catch {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
function ResearchDetails({ item }: { item: TaskChatProviderActivityItem }) {
|
||||
const [showAllResults, setShowAllResults] = useState(false);
|
||||
const query = item.details.find((detail) => detail.label.toLowerCase() === "query");
|
||||
const additionalDetails = item.details.filter((detail) => !["action", "query", "status"].includes(detail.label.toLowerCase()));
|
||||
const visibleLinks = showAllResults ? item.links : item.links.slice(0, COMPACT_RESEARCH_RESULT_LIMIT);
|
||||
const hiddenResultCount = item.links.length - visibleLinks.length;
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-2.5">
|
||||
{query ? (
|
||||
<div className="min-w-0 rounded-sm bg-muted/40 px-2.5 py-2" data-testid="task-chat-research-query">
|
||||
<p className="text-(length:--text-nano) font-medium uppercase tracking-wide text-muted-foreground">Query</p>
|
||||
<p className="mt-0.5 min-w-0 break-words font-mono text-(length:--text-micro) text-foreground">{query.value}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{item.links.length > 0 ? (
|
||||
<div className="min-w-0">
|
||||
<p className="mb-1 text-(length:--text-nano) font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{item.links.length} {item.links.length === 1 ? "result" : "results"}
|
||||
</p>
|
||||
<ol className="divide-y divide-border/60" aria-label="Research sources">
|
||||
{visibleLinks.map((link, index) => (
|
||||
<li className="min-w-0 py-1.5 first:pt-0 last:pb-0" key={`${link.href}:${index}`}>
|
||||
<a className="flex min-w-0 items-center gap-1 font-medium text-foreground hover:underline" href={link.href} target="_blank" rel="noreferrer">
|
||||
<span className="truncate">{link.label}</span>
|
||||
<ExternalLink className="h-3 w-3 shrink-0" aria-hidden />
|
||||
</a>
|
||||
<p className="mt-0.5 flex min-w-0 gap-2 text-muted-foreground">
|
||||
<span className="shrink-0">{sourceHostname(link.href)}</span>
|
||||
{link.description ? <span className="min-w-0 truncate">{link.description}</span> : null}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
{hiddenResultCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setShowAllResults(true)}
|
||||
>
|
||||
Show {hiddenResultCount} more {hiddenResultCount === 1 ? "result" : "results"}
|
||||
</button>
|
||||
) : showAllResults && item.links.length > COMPACT_RESEARCH_RESULT_LIMIT ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-1.5 text-muted-foreground hover:text-foreground"
|
||||
onClick={() => setShowAllResults(false)}
|
||||
>
|
||||
Show fewer results
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<DetailList details={additionalDetails} />
|
||||
{item.output || item.outputTruncated ? (
|
||||
<p className="text-muted-foreground">Additional provider output is available in Runner Inspector.</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProviderDetails({ item }: { item: TaskChatProviderActivityItem }) {
|
||||
if (item.family === "research") return <ResearchDetails item={item} />;
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
{item.steps.length > 0 ? (
|
||||
@@ -78,14 +166,7 @@ function ProviderDetails({ item }: { item: TaskChatProviderActivityItem }) {
|
||||
</ul>
|
||||
) : null}
|
||||
{item.details.length > 0 ? (
|
||||
<dl className="grid gap-x-3 gap-y-1 sm:grid-cols-(--gtc-2)">
|
||||
{item.details.map((detail) => (
|
||||
<div className="contents" key={`${detail.label}:${detail.value}`}>
|
||||
<dt className="text-muted-foreground">{detail.label}</dt>
|
||||
<dd className={cn("min-w-0 break-words text-foreground", detail.mono && "font-mono")}>{detail.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<DetailList details={item.details} />
|
||||
) : null}
|
||||
{item.output ? (
|
||||
<pre className="max-h-(--sz-64) overflow-auto whitespace-pre-wrap rounded-sm bg-muted/50 p-2 font-mono text-(length:--text-micro) text-foreground">{item.output}</pre>
|
||||
@@ -96,12 +177,15 @@ function ProviderDetails({ item }: { item: TaskChatProviderActivityItem }) {
|
||||
}
|
||||
|
||||
function WorkspaceChangeDetails({ item }: { item: TaskChatWorkspaceChangeItem }) {
|
||||
return item.files.length > 0 ? (
|
||||
<ul className="flex min-w-0 flex-col gap-2" aria-label="Changed files">
|
||||
{item.files.map((file) => (
|
||||
<li className="flex min-w-0 flex-col gap-1" key={`${file.operation}:${file.path}`}>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-foreground">
|
||||
if (item.files.length === 0) return <p className="text-muted-foreground">No changed-file details were reported.</p>;
|
||||
const visibleFiles = item.files.slice(0, COMPACT_WORKSPACE_FILE_LIMIT);
|
||||
const hiddenFileCount = item.files.length - visibleFiles.length;
|
||||
return (
|
||||
<div className="min-w-0" data-testid="task-chat-workspace-change-details">
|
||||
<ul className="flex min-w-0 flex-col divide-y divide-border/60" aria-label="Changed files">
|
||||
{visibleFiles.map((file) => (
|
||||
<li className="flex min-w-0 items-center gap-2 py-1.5 first:pt-0 last:pb-0" key={`${file.operation}:${file.path}`}>
|
||||
<span className="min-w-0 flex-1 truncate font-mono text-foreground" title={file.previousPath ? `${file.previousPath} → ${file.path}` : file.path}>
|
||||
{file.previousPath ? `${file.previousPath} → ${file.path}` : file.path}
|
||||
</span>
|
||||
<span className="shrink-0 capitalize text-muted-foreground">{file.operation.replaceAll("_", " ")}</span>
|
||||
@@ -110,12 +194,12 @@ function WorkspaceChangeDetails({ item }: { item: TaskChatWorkspaceChangeItem })
|
||||
{file.additions == null ? "" : `+${file.additions}`} {file.deletions == null ? "" : `−${file.deletions}`}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{file.diff ? <pre className="max-h-(--sz-64) overflow-auto whitespace-pre-wrap rounded-sm bg-muted/50 p-2 font-mono text-(length:--text-micro) text-foreground">{file.diff}</pre> : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : <p className="text-muted-foreground">No changed-file details were reported.</p>;
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{hiddenFileCount > 0 ? <p className="mt-1.5 text-muted-foreground">{hiddenFileCount} more {hiddenFileCount === 1 ? "file" : "files"} not shown</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceFileDetails({ item }: { item: TaskChatWorkspaceFileItem }) {
|
||||
|
||||
@@ -167,9 +167,9 @@ function ProviderActivityCard({ item }: { item: TaskChatProviderActivityItem })
|
||||
<ChevronDown aria-hidden className="h-3.5 w-3.5" /> Details
|
||||
</summary>
|
||||
{item.details.length > 0 ? (
|
||||
<dl className="mt-2 grid gap-x-3 gap-y-1 text-xs sm:grid-cols-(--gtc-2)">
|
||||
<dl className="mt-2 flex min-w-0 flex-col gap-1.5 text-xs">
|
||||
{item.details.map((detail) => (
|
||||
<div key={`${detail.label}:${detail.value}`} className="contents">
|
||||
<div key={`${detail.label}:${detail.value}`} className="grid min-w-0 grid-cols-1 gap-0.5 sm:grid-cols-(--gtc-task-chat-details) sm:gap-3">
|
||||
<dt className="text-muted-foreground">{detail.label}</dt>
|
||||
<dd className={cn("min-w-0 break-words text-foreground", detail.mono && "font-mono")}>{detail.value}</dd>
|
||||
</div>
|
||||
|
||||
@@ -21,14 +21,14 @@ const STATUS_ICON = {
|
||||
/**
|
||||
* 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 the mono result/detail block (left-rail inset);
|
||||
* diffs render as an inset panel below the row.
|
||||
* content. Clicking toggles a compact result/change summary (left-rail
|
||||
* inset). Full diff bodies stay out of the activity feed.
|
||||
*/
|
||||
export function TaskChatToolCard({ item }: { item: TaskChatToolItem }) {
|
||||
const { Icon, spin, tone } = STATUS_ICON[item.status];
|
||||
const RowIcon = toolTaxonomy(item.rawName ?? item.name).icon;
|
||||
const [showDetail, setShowDetail] = useState(false);
|
||||
const expandable = Boolean(item.detail);
|
||||
const expandable = Boolean(item.detail || item.diff);
|
||||
|
||||
return (
|
||||
<div className="tc-enter-tool flex flex-col text-xs">
|
||||
@@ -70,35 +70,20 @@ export function TaskChatToolCard({ item }: { item: TaskChatToolItem }) {
|
||||
<Icon className={cn("h-3.5 w-3.5", tone, spin && "animate-spin")} aria-hidden />
|
||||
</span>
|
||||
</button>
|
||||
{expandable && showDetail ? (
|
||||
{item.detail && showDetail ? (
|
||||
<pre className="ml-2.5 mt-0.5 overflow-x-auto 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 ? (
|
||||
<div className="tc-reveal-diff ml-2.5 mt-1 border-l-2 border-border pl-2.5">
|
||||
<div className="mb-1 flex items-center gap-2 text-(length:--text-micro) text-muted-foreground">
|
||||
{item.diff.path ? <span className="truncate font-mono">{item.diff.path}</span> : null}
|
||||
<span className="ml-auto shrink-0 font-mono font-semibold">
|
||||
{item.diff && showDetail ? (
|
||||
<div className="ml-2.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}
|
||||
<span className="ml-auto shrink-0 font-mono">
|
||||
+{item.diff.added} −{item.diff.removed}
|
||||
</span>
|
||||
</div>
|
||||
{item.diff.lines?.length ? (
|
||||
<pre className="overflow-x-auto rounded-sm bg-muted/50 p-2 text-(length:--text-micro) leading-relaxed">
|
||||
{item.diff.lines.map((l, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={cn(
|
||||
l.kind === "add" && "text-(--status-task-icon-done)",
|
||||
l.kind === "remove" && "text-destructive",
|
||||
l.kind === "context" && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{l.kind === "add" ? "+" : l.kind === "remove" ? "−" : " "} {l.text}
|
||||
</div>
|
||||
))}
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -2291,6 +2291,7 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] {
|
||||
:root {
|
||||
--gtc-1: 56px 56px 24px minmax(0,1fr); /* Extracted from ui/src/components/DocumentDiffModal.tsx (grid-cols-[56px_56px_24px_minmax(0,1fr)]). */
|
||||
--gtc-case-revisions: 16rem 1fr; /* Extracted from ui/src/components/CaseRevisionRail.tsx (grid-cols-[16rem_1fr]). */
|
||||
--gtc-task-chat-details: 6rem minmax(0,1fr);
|
||||
--gtc-2: auto minmax(0,1fr) 2.25rem; /* Extracted from ui/src/components/FileTree.tsx (grid-cols-[auto_minmax(0,1fr)_2.25rem]). */
|
||||
--gtc-3: minmax(0,1fr) 2.25rem; /* Extracted from ui/src/components/FileTree.tsx (grid-cols-[minmax(0,1fr)_2.25rem]). */
|
||||
--gtc-4: minmax(0,1fr); /* Extracted from ui/src/components/FileTree.tsx (grid-cols-[minmax(0,1fr)]). */
|
||||
|
||||
@@ -234,6 +234,40 @@ const allProviderActivity: TaskChatItem[] = [
|
||||
providerActivity("provider_notice", "provider.notice.recorded", "informational", { summary: "Throughput temporarily reduced" }),
|
||||
];
|
||||
|
||||
const searchResultsAndFileChanges: TaskChatItem[] = [
|
||||
providerActivity("research", "research.completed", "completed", {
|
||||
summary: "site:nextjs.org/docs/app/api-reference/config/eslint Next.js 16 eslint.config.mjs core-web-vitals typescript",
|
||||
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 },
|
||||
],
|
||||
links: [
|
||||
{ label: "Configuration: ESLint | Next.js", href: "https://nextjs.org/docs/app/api-reference/config/eslint", description: "Configure ESLint in the App Router and understand the current defaults." },
|
||||
{ label: "next.config.js: eslint | Next.js", href: "https://nextjs.org/docs/pages/api-reference/config/next-config-js/eslint", description: "Legacy configuration behavior and migration notes." },
|
||||
{ label: "Configuration: TypeScript | Next.js", href: "https://nextjs.org/docs/app/api-reference/config/typescript", description: "TypeScript configuration options for modern Next.js applications." },
|
||||
{ label: "Upgrading: Codemods | Next.js", href: "https://nextjs.org/docs/app/guides/upgrading/codemods", description: "Automated migrations for eslint.config.mjs and current conventions." },
|
||||
{ label: "API Reference: Configuration | Next.js", href: "https://nextjs.org/docs/app/api-reference/config", description: "The complete application configuration reference." },
|
||||
{ label: "Next.js 16 upgrade guide", href: "https://nextjs.org/docs/app/guides/upgrading/version-16", description: "Breaking changes and recommended upgrade steps for version 16." },
|
||||
{ label: "Core Web Vitals", href: "https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals", description: "Measure and report application performance metrics." },
|
||||
],
|
||||
}),
|
||||
{
|
||||
id: "file-change-search-story",
|
||||
kind: "tool",
|
||||
name: "File change",
|
||||
rawName: "file_change",
|
||||
target: "/Users/runner/workspaces/example/tsconfig.json",
|
||||
status: "completed",
|
||||
diff: {
|
||||
path: "tsconfig.json",
|
||||
added: 228,
|
||||
removed: 0,
|
||||
lines: Array.from({ length: 24 }, (_, index) => ({ kind: "add" as const, text: `generated diff line ${index + 1}` })),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function RunnerTransitionStory() {
|
||||
const [phase, setPhase] = useState(0);
|
||||
const items: TaskChatItem[] = phase === 0
|
||||
@@ -454,6 +488,28 @@ export const RunnerLiveAllProviderFamilies: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const RunnerLiveSearchResultsAndFileChanges: Story = {
|
||||
name: "Runner Activity / Live / Search Results and File Changes",
|
||||
render: () => <RunnerTurnStory items={searchResultsAndFileChanges} />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(canvas.getByRole("button", { name: /expand run activity/i }));
|
||||
const list = canvas.getByTestId("task-chat-runner-activity-list");
|
||||
const researchRow = list.querySelector<HTMLElement>('[data-activity-family="research"]');
|
||||
if (!researchRow) throw new Error("Research activity row did not render");
|
||||
await userEvent.click(within(researchRow).getByRole("button"));
|
||||
const toolRow = Array.from(list.querySelectorAll<HTMLElement>(".tc-enter-tool")).find((row) => row.textContent?.includes("File change"));
|
||||
if (!toolRow) throw new Error("File change tool row did not render");
|
||||
await userEvent.click(within(toolRow).getByRole("button"));
|
||||
|
||||
await expect(canvas.getByTestId("task-chat-research-query")).toHaveTextContent("eslint.config.mjs");
|
||||
await expect(canvas.getByRole("list", { name: "Research sources" }).querySelectorAll("li")).toHaveLength(5);
|
||||
await expect(canvas.getByText("Show 2 more results")).toBeVisible();
|
||||
await expect(canvas.getByTestId("task-chat-tool-change-summary")).toHaveTextContent("+228 −0");
|
||||
await expect(canvas.queryByText("generated diff line 1")).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const RunnerLiveCommentaryAndStreamingUpdates: Story = {
|
||||
name: "Runner Activity / Live / Commentary and Streaming Updates",
|
||||
render: () => <RunnerTurnStory items={mixedLiveActivity} />,
|
||||
|
||||
Reference in new issue
Block a user