feat(coding-agent): filtered non-partial poll previews to show only settled jobs

- Added a list-mode flag to job rendering and used it to keep background job listing behavior distinct.
- Filtered non-partial poll call results to exclude running jobs and return no output when only running jobs remain.
- Added tests covering partial rendering, poll-based filtering, and list/cancel paths for the updated preview behavior.
This commit is contained in:
can1357
2026-06-15 18:57:59 +02:00
parent 013ff7f3ab
commit edb83bd37b
2 changed files with 137 additions and 1 deletions
+14 -1
View File
@@ -372,6 +372,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
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]++;
@@ -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<typeof jobToolRenderer.renderResult>[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<typeof jobToolRenderer.renderResult>[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<typeof jobToolRenderer.renderResult>[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<typeof jobToolRenderer.renderResult>[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<typeof jobToolRenderer.renderResult>[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");
});
});
});