feat: implement ui/notifications/tool-cancelled + add missing App.onteardown (#120)
* feat: implement ui/notifications/tool-cancelled throughout SDK Wire the tool-cancelled notification that hosts MUST send when tool execution is cancelled (user action, timeout, classifier intervention, etc.): - Add McpUiToolCancelledNotification type in spec.types.ts - Generate Zod schema via ts-to-zod - Add AppBridge.sendToolCancelled() method for hosts - Add App.ontoolcancelled setter for guest UIs - Export type and schema from types.ts - Add tests for notification with/without reason 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: add App.onteardown handler for graceful shutdown Wire the guest UI side of ui/resource-teardown request: - Add onteardown setter in App class for guest UIs to handle teardown - Allow ui/resource-teardown in assertRequestHandlerCapability - Import McpUiResourceTeardownRequest/Result types - Add tests for sync and async cleanup handlers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(example): wire tool-cancelled and teardown in basic-host Update basic-host example to properly use the new notifications: - Send sendToolCancelled when tool call promise rejects - Send sendResourceTeardown when React component unmounts - Store appBridge ref for cleanup access 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(example): implement graceful teardown per spec recommendation Address review feedback: the host now waits for guest's teardown response before unmounting, following the spec: "Host SHOULD wait for a response before tearing down the resource (to prevent data loss)." Changes: - Add close button to tool call panels - Two-phase unmount: isDestroying state dims panel and disables events - sendResourceTeardown() completes before component removal - Non-app tool results close immediately 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat(examples): add onteardown handlers to demo apps Demonstrate graceful shutdown with simulated 500ms cleanup work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a31e48a11e
commit
70b393a7e6
1 changed files with 379 additions and 15 deletions
|
|
@ -140,6 +140,36 @@ describe("App <-> AppBridge integration", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("sendToolCancelled triggers app.ontoolcancelled", async () => {
|
||||
const receivedCancellations: unknown[] = [];
|
||||
app.ontoolcancelled = (params) => {
|
||||
receivedCancellations.push(params);
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendToolCancelled({
|
||||
reason: "User cancelled the operation",
|
||||
});
|
||||
|
||||
expect(receivedCancellations).toHaveLength(1);
|
||||
expect(receivedCancellations[0]).toEqual({
|
||||
reason: "User cancelled the operation",
|
||||
});
|
||||
});
|
||||
|
||||
it("sendToolCancelled works without reason", async () => {
|
||||
const receivedCancellations: unknown[] = [];
|
||||
app.ontoolcancelled = (params) => {
|
||||
receivedCancellations.push(params);
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendToolCancelled({});
|
||||
|
||||
expect(receivedCancellations).toHaveLength(1);
|
||||
expect(receivedCancellations[0]).toEqual({});
|
||||
});
|
||||
|
||||
it("setHostContext triggers app.onhostcontextchanged", async () => {
|
||||
const receivedContexts: unknown[] = [];
|
||||
app.onhostcontextchanged = (params) => {
|
||||
|
|
@ -173,6 +203,34 @@ describe("App <-> AppBridge integration", () => {
|
|||
{ theme: "light" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("sendResourceTeardown triggers app.onteardown", async () => {
|
||||
let teardownCalled = false;
|
||||
app.onteardown = async () => {
|
||||
teardownCalled = true;
|
||||
return {};
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendResourceTeardown({});
|
||||
|
||||
expect(teardownCalled).toBe(true);
|
||||
});
|
||||
|
||||
it("sendResourceTeardown waits for async cleanup", async () => {
|
||||
const cleanupSteps: string[] = [];
|
||||
app.onteardown = async () => {
|
||||
cleanupSteps.push("start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
cleanupSteps.push("done");
|
||||
return {};
|
||||
};
|
||||
|
||||
await app.connect(appTransport);
|
||||
await bridge.sendResourceTeardown({});
|
||||
|
||||
expect(cleanupSteps).toEqual(["start", "done"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("App -> Host notifications", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue