diff --git a/packages/coding-agent/src/tools/job.ts b/packages/coding-agent/src/tools/job.ts index 716bb3987..f72d2e48f 100644 --- a/packages/coding-agent/src/tools/job.ts +++ b/packages/coding-agent/src/tools/job.ts @@ -372,6 +372,7 @@ export class JobTool implements AgentTool { interface JobRenderArgs { poll?: string[]; cancel?: string[]; + list?: boolean; } const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS; @@ -433,6 +434,7 @@ function flattenStructuredPreview(text: string): string { } function describeTarget(args: JobRenderArgs | undefined): string { + if (args?.list) return "background jobs"; const poll = args?.poll ?? []; const cancel = args?.cancel ?? []; const parts: string[] = []; @@ -460,7 +462,7 @@ export const jobToolRenderer = { uiTheme: Theme, args?: JobRenderArgs, ): Component { - const jobs = result.details?.jobs ?? []; + let jobs = result.details?.jobs ?? []; if (jobs.length === 0) { const fallback = result.content?.find(c => c.type === "text")?.text || "No jobs to process"; @@ -468,6 +470,17 @@ export const jobToolRenderer = { return new Text([header, formatEmptyMessage(fallback, uiTheme)].join("\n"), 0, 0); } + const isPollCall = args + ? !args.list && (!args.cancel || args.cancel.length === 0 || args.poll !== undefined) + : true; + + if (!options.isPartial && isPollCall) { + jobs = jobs.filter(job => job.status !== "running"); + if (jobs.length === 0) { + return new Text("", 0, 0); + } + } + const counts = { completed: 0, failed: 0, cancelled: 0, running: 0 }; for (const job of jobs) counts[job.status]++; diff --git a/packages/coding-agent/test/job-renderer-preview.test.ts b/packages/coding-agent/test/job-renderer-preview.test.ts index 33e538c37..e453c1c8e 100644 --- a/packages/coding-agent/test/job-renderer-preview.test.ts +++ b/packages/coding-agent/test/job-renderer-preview.test.ts @@ -110,4 +110,127 @@ describe("job renderer task-result preview", () => { expect(header).toBeDefined(); expect(header!.match(/SpawnProbe/g)).toHaveLength(1); }); + + describe("collapse and filter when turned into a result", () => { + const jobsData = [ + { + id: "Job1", + type: "task" as const, + status: "running" as const, + label: "Job1 running", + durationMs: 1200, + }, + { + id: "Job2", + type: "task" as const, + status: "completed" as const, + label: "Job2 completed", + durationMs: 3400, + resultText: "Job2 result", + }, + { + id: "Job3", + type: "task" as const, + status: "running" as const, + label: "Job3 running", + durationMs: 500, + }, + ]; + + it("shows all jobs when isPartial is true", () => { + const result = { + content: [{ type: "text" as const, text: "" }], + details: { jobs: jobsData }, + }; + const component = jobToolRenderer.renderResult( + result, + { expanded: true, isPartial: true } as Parameters[1], + theme, + { poll: [] }, + ); + const output = Bun.stripANSI((component.render(120) as readonly string[]).join("\n")); + expect(output).toContain("Job1 running"); + expect(output).toContain("Job2 completed"); + expect(output).toContain("Job3 running"); + expect(output).toContain("waiting on 2 of 3 jobs"); + }); + + it("shows only finished jobs when isPartial is false and it is a poll call", () => { + const result = { + content: [{ type: "text" as const, text: "" }], + details: { jobs: jobsData }, + }; + const component = jobToolRenderer.renderResult( + result, + { expanded: true, isPartial: false } as Parameters[1], + theme, + { poll: [] }, + ); + const output = Bun.stripANSI((component.render(120) as readonly string[]).join("\n")); + expect(output).not.toContain("Job1 running"); + expect(output).toContain("Job2 completed"); + expect(output).not.toContain("Job3 running"); + expect(output).toContain("1 job settled"); + }); + + it("shows nothing when isPartial is false and all jobs are running and it is a poll call", () => { + const runningJobsOnly = [ + { + id: "Job1", + type: "task" as const, + status: "running" as const, + label: "Job1 running", + durationMs: 1200, + }, + ]; + const result = { + content: [{ type: "text" as const, text: "" }], + details: { jobs: runningJobsOnly }, + }; + const component = jobToolRenderer.renderResult( + result, + { expanded: true, isPartial: false } as Parameters[1], + theme, + { poll: [] }, + ); + const lines = component.render(120) as readonly string[]; + expect(lines).toHaveLength(0); + }); + + it("does not collapse running jobs when isPartial is false and list is true", () => { + const result = { + content: [{ type: "text" as const, text: "" }], + details: { jobs: jobsData }, + }; + const component = jobToolRenderer.renderResult( + result, + { expanded: true, isPartial: false } as Parameters[1], + theme, + { list: true }, + ); + const output = Bun.stripANSI((component.render(120) as readonly string[]).join("\n")); + expect(output).toContain("Job1 running"); + expect(output).toContain("Job2 completed"); + expect(output).toContain("Job3 running"); + expect(output).toContain("waiting on 2 of 3 jobs"); + }); + + it("does not collapse running jobs when isPartial is false and cancel-only is true", () => { + const result = { + content: [{ type: "text" as const, text: "" }], + details: { jobs: jobsData }, + }; + const component = jobToolRenderer.renderResult( + result, + { expanded: true, isPartial: false } as Parameters[1], + theme, + { cancel: ["Job1"] }, + ); + const output = Bun.stripANSI((component.render(120) as readonly string[]).join("\n")); + expect(output).toContain("Job1 running"); + expect(output).toContain("Job2 completed"); + expect(output).toContain("Job3 running"); + expect(output).toContain("waiting on 2 of 3 jobs"); + }); + }); });