feat(web): wire up MCP task support — Tasks screen, Run-as-task, status toasts (#1422) (#1424)

Wires the ported Tasks screen + core task subsystem through App.tsx: sticky Clear Completed, cancel/refresh error toasts, core-side per-task progress correlation, Run-as-task on the Tools screen (gated on server support), per-server Task TTL, and live task-status toasts.
This commit is contained in:
Cliff Hall 2026-06-04 08:49:47 -04:00 • committed by GitHub
commit ed6eda7492
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 1192 additions and 40 deletions

View file

@ -319,6 +319,7 @@ describe("InspectorClient", () => {
metadata: [],
connectionTimeout: 50,
requestTimeout: 0,
taskTtl: 0,
},
},
);
@ -3607,6 +3608,75 @@ describe("InspectorClient", () => {
expect(task).toHaveProperty("ttl");
});
it("emits requestorTaskProgress tagged with the owning taskId during a task-augmented call", async () => {
// Core-side progress→task correlation (#1422): the wrapped onprogress in
// callToolStream tags each tick with the taskId it owns, so App can map
// progress to the right TaskCard. The progress_task tool emits 5 ticks.
const progressEvents: Array<{ taskId: string; progress: Progress }> = [];
client!.addEventListener(
"requestorTaskProgress",
(event: TypedEvent<"requestorTaskProgress">) => {
progressEvents.push(event.detail);
},
);
// Capture the created taskId from the toolCall event stream for comparison.
let createdTaskId: string | undefined;
client!.addEventListener(
"toolCallTaskUpdated",
(event: TypedEvent<"toolCallTaskUpdated">) => {
createdTaskId ??= event.detail.taskId;
},
);
const progressTool = await getTool(client!, "progress_task");
const result = await client!.callToolStream(progressTool, {
message: "with progress",
});
expect(result.success).toBe(true);
expect(progressEvents.length).toBeGreaterThan(0);
// Every tick is tagged with the same (and the correct) task id.
const taskIds = new Set(progressEvents.map((e) => e.taskId));
expect(taskIds.size).toBe(1);
const [taggedTaskId] = [...taskIds];
expect(typeof taggedTaskId).toBe("string");
expect(taggedTaskId!.length).toBeGreaterThan(0);
expect(taggedTaskId).toBe(createdTaskId);
// The payload carries the server's progress units.
expect(progressEvents.some((e) => e.progress.total === 5)).toBe(true);
});
it("does not emit requestorTaskProgress when progress is globally disabled", async () => {
// The callToolStream onprogress wrapper is gated on `this.progress`, so a
// client with progress disabled neither requests a progress token nor
// emits requestorTaskProgress — task calls respect the same toggle as
// every other call path. The task still completes.
const noProgressClient = new InspectorClient(
{ type: "sse", url: server!.url },
{ environment: { transport: createTransportNode }, progress: false },
);
await noProgressClient.connect();
try {
const progressEvents: Array<{ taskId: string; progress: Progress }> =
[];
noProgressClient.addEventListener(
"requestorTaskProgress",
(event: TypedEvent<"requestorTaskProgress">) => {
progressEvents.push(event.detail);
},
);
const progressTool = await getTool(noProgressClient, "progress_task");
const result = await noProgressClient.callToolStream(progressTool, {
message: "no progress",
});
expect(result.success).toBe(true);
expect(progressEvents).toHaveLength(0);
} finally {
await noProgressClient.disconnect();
}
});
it("should get task by taskId", async () => {
// First create a task
const simpleTaskByIdTool = await getTool(client!, "simple_task");

View file

@ -216,6 +216,7 @@ describe("Transport", () => {
metadata: [],
connectionTimeout: 0,
requestTimeout: 0,
taskTtl: 0,
};
const fetchRequests: FetchRequestEntryBase[] = [];
@ -267,6 +268,7 @@ describe("Transport", () => {
metadata: [],
connectionTimeout: 0,
requestTimeout: 0,
taskTtl: 0,
};
const fetchRequests: FetchRequestEntryBase[] = [];
@ -308,6 +310,7 @@ describe("Transport", () => {
metadata: [],
connectionTimeout: 0,
requestTimeout: 0,
taskTtl: 0,
};
const result = createTransportNode(config, { settings });
// Just exercise the empty-headers path — no transport construction

View file

@ -410,6 +410,7 @@ describe("Remote transport e2e", () => {
metadata: [],
connectionTimeout: 0,
requestTimeout: 0,
taskTtl: 0,
};
const createTransport = createRemoteTransport({ baseUrl, authToken });