@runtypelabs/persona 3.29.0 → 3.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -0
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CxvHw0X6.d.cts → types-B_Qazlm4.d.cts} +6 -0
- package/dist/animations/{types-CxvHw0X6.d.ts → types-B_Qazlm4.d.ts} +6 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +6 -6
- package/dist/codegen.js +4 -4
- package/dist/index.cjs +47 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +127 -7
- package/dist/index.d.ts +127 -7
- package/dist/index.global.js +60 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +47 -47
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/plugin-kit.cjs +1 -0
- package/dist/plugin-kit.d.cts +98 -0
- package/dist/plugin-kit.d.ts +98 -0
- package/dist/plugin-kit.js +1 -0
- package/dist/smart-dom-reader.d.cts +113 -4
- package/dist/smart-dom-reader.d.ts +113 -4
- package/dist/theme-editor.cjs +38 -38
- package/dist/theme-editor.d.cts +117 -6
- package/dist/theme-editor.d.ts +117 -6
- package/dist/theme-editor.js +38 -38
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +2 -0
- package/dist/theme-reference.d.ts +2 -0
- package/dist/theme-reference.js +1 -1
- package/package.json +8 -2
- package/src/client.test.ts +40 -0
- package/src/client.ts +18 -4
- package/src/components/approval-bubble.test.ts +268 -0
- package/src/components/approval-bubble.ts +170 -20
- package/src/components/launcher.ts +6 -3
- package/src/components/tool-bubble.test.ts +39 -0
- package/src/components/tool-bubble.ts +4 -0
- package/src/generated/runtype-openapi-contract.ts +12 -0
- package/src/index-core.ts +1 -0
- package/src/plugin-kit.test.ts +230 -0
- package/src/plugin-kit.ts +294 -0
- package/src/plugins/types.ts +49 -2
- package/src/session.test.ts +99 -0
- package/src/session.ts +204 -32
- package/src/session.webmcp.test.ts +326 -6
- package/src/theme-editor/preview-utils.test.ts +10 -0
- package/src/theme-editor/preview-utils.ts +29 -1
- package/src/theme-editor/sections.test.ts +17 -0
- package/src/theme-editor/sections.ts +31 -0
- package/src/theme-reference.ts +1 -1
- package/src/types/theme.ts +2 -0
- package/src/types.ts +59 -2
- package/src/ui.approval-plugin.test.ts +204 -0
- package/src/ui.ts +149 -16
- package/src/utils/theme.test.ts +6 -2
- package/src/utils/theme.ts +0 -8
- package/src/utils/tokens.ts +6 -1
- package/src/webmcp-bridge.test.ts +66 -0
- package/src/webmcp-bridge.ts +49 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { AgentWidgetSession } from "./session";
|
|
4
4
|
import type {
|
|
@@ -80,6 +80,10 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
80
80
|
vi.clearAllMocks();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
vi.useRealTimers();
|
|
85
|
+
});
|
|
86
|
+
|
|
83
87
|
it("posts result to /resume on the happy path", async () => {
|
|
84
88
|
const { session, executeSpy, resumeSpy } = makeSession({
|
|
85
89
|
executeReturn: {
|
|
@@ -97,6 +101,33 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
97
101
|
);
|
|
98
102
|
});
|
|
99
103
|
|
|
104
|
+
it("records the elapsed time of the resolved browser WebMCP promise", async () => {
|
|
105
|
+
vi.useFakeTimers();
|
|
106
|
+
vi.setSystemTime(1_000);
|
|
107
|
+
const { session } = makeSession({
|
|
108
|
+
executeImpl: async () => {
|
|
109
|
+
vi.advanceTimersByTime(1_250);
|
|
110
|
+
return { content: [{ type: "text", text: "loaded jade" }] };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await session.resolveWebMcpToolCall(
|
|
115
|
+
awaitingMessage("tool-1", "webmcp:get_product_by_url"),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const stored = (
|
|
119
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
120
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
121
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
122
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
123
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
124
|
+
expect(stored?.toolCall?.completedAt).toBe(2_250);
|
|
125
|
+
expect(stored?.toolCall?.durationMs).toBe(1_250);
|
|
126
|
+
expect(stored?.toolCall?.result).toEqual({
|
|
127
|
+
content: [{ type: "text", text: "loaded jade" }],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
100
131
|
it("still resumes (with isError) when the bridge is not operational", async () => {
|
|
101
132
|
// BugBot finding #1: previously, handleEvent skipped resolveWebMcpToolCall
|
|
102
133
|
// entirely when `isWebMcpOperational()` was false — leaving the dispatch
|
|
@@ -152,6 +183,13 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
152
183
|
|
|
153
184
|
const msg = awaitingMessage("tool-1", "webmcp:search");
|
|
154
185
|
await session.resolveWebMcpToolCall(msg);
|
|
186
|
+
const afterFailedResume = (
|
|
187
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
188
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
189
|
+
expect(afterFailedResume?.toolCall?.status).toBe("running");
|
|
190
|
+
expect(afterFailedResume?.toolCall?.result).toBeUndefined();
|
|
191
|
+
expect(afterFailedResume?.toolCall?.durationMs).toBeUndefined();
|
|
192
|
+
|
|
155
193
|
await session.resolveWebMcpToolCall(msg); // retry — must be allowed
|
|
156
194
|
await session.resolveWebMcpToolCall(msg); // post-success — must be blocked
|
|
157
195
|
|
|
@@ -159,6 +197,29 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
159
197
|
expect(resumeSpy).toHaveBeenCalledTimes(2);
|
|
160
198
|
});
|
|
161
199
|
|
|
200
|
+
it("marks the bubble complete with an error when browser execution throws before /resume", async () => {
|
|
201
|
+
const { session, resumeSpy } = makeSession({
|
|
202
|
+
executeImpl: async () => {
|
|
203
|
+
throw new Error("page tool exploded");
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await session.resolveWebMcpToolCall(
|
|
208
|
+
awaitingMessage("tool-1", "webmcp:search"),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const stored = (
|
|
212
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
213
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
214
|
+
expect(resumeSpy).not.toHaveBeenCalled();
|
|
215
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
216
|
+
expect(stored?.toolCall?.durationMs).toBeGreaterThanOrEqual(0);
|
|
217
|
+
expect(stored?.toolCall?.result).toMatchObject({
|
|
218
|
+
isError: true,
|
|
219
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
162
223
|
it("threads an AbortSignal into resumeFlow", async () => {
|
|
163
224
|
// BugBot finding #6: cancel() needs to propagate into /resume.
|
|
164
225
|
const { session, resumeSpy } = makeSession();
|
|
@@ -189,6 +250,14 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
189
250
|
release();
|
|
190
251
|
await inflight;
|
|
191
252
|
expect(resumeSpy).not.toHaveBeenCalled();
|
|
253
|
+
const stored = (
|
|
254
|
+
session as unknown as { messages: AgentWidgetMessage[] }
|
|
255
|
+
).messages.find((m) => m.toolCall?.id === "tool-1");
|
|
256
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
257
|
+
expect(stored?.toolCall?.result).toMatchObject({
|
|
258
|
+
isError: true,
|
|
259
|
+
content: [{ type: "text", text: "Aborted by cancel()" }],
|
|
260
|
+
});
|
|
192
261
|
});
|
|
193
262
|
|
|
194
263
|
it("does NOT abort the shared session abortController", async () => {
|
|
@@ -282,11 +351,12 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
282
351
|
expect(executeSpy).not.toHaveBeenCalled();
|
|
283
352
|
});
|
|
284
353
|
|
|
285
|
-
it("a stale step_await re-emit does not resurrect awaitingLocalTool once resolved", () => {
|
|
354
|
+
it("a stale step_await re-emit does not resurrect awaitingLocalTool or reset completed tool state once resolved", () => {
|
|
286
355
|
// BugBot: a duplicate step_await (awaitingLocalTool:true) for an
|
|
287
356
|
// already-resolved webmcp tool must not flip the message back to awaiting
|
|
288
|
-
// and show a stuck local-tool wait
|
|
289
|
-
// tool's `${executionId}:${toolCallId}`
|
|
357
|
+
// or running and show a stuck local-tool wait / lose the measured duration.
|
|
358
|
+
// upsertMessage clears it when the tool's `${executionId}:${toolCallId}`
|
|
359
|
+
// key is inflight/resolved.
|
|
290
360
|
const session = makeSession().session;
|
|
291
361
|
const s = session as unknown as {
|
|
292
362
|
webMcpResolvedKeys: Set<string>;
|
|
@@ -298,11 +368,182 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
298
368
|
s.upsertMessage({
|
|
299
369
|
...awaitingMessage("tool-1", "webmcp:search"),
|
|
300
370
|
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
371
|
+
streaming: false,
|
|
372
|
+
toolCall: {
|
|
373
|
+
id: "tool-1",
|
|
374
|
+
name: "webmcp:search",
|
|
375
|
+
status: "complete",
|
|
376
|
+
args: { q: "shoes" },
|
|
377
|
+
result: { content: [{ type: "text", text: "ok" }] },
|
|
378
|
+
startedAt: 1_000,
|
|
379
|
+
completedAt: 2_200,
|
|
380
|
+
durationMs: 1_200,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
// Stale re-emit flips awaiting back to true and carries the running state
|
|
384
|
+
// emitted by client.ts for WebMCP step_await events.
|
|
385
|
+
s.upsertMessage({
|
|
386
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
387
|
+
streaming: false,
|
|
388
|
+
toolCall: {
|
|
389
|
+
id: "tool-1",
|
|
390
|
+
name: "webmcp:search",
|
|
391
|
+
status: "running",
|
|
392
|
+
args: { q: "shoes" },
|
|
393
|
+
startedAt: 3_000,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const stored = s.messages.find((m) => m.toolCall?.id === "tool-1");
|
|
397
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
398
|
+
expect(stored?.streaming).toBe(false);
|
|
399
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
400
|
+
expect(stored?.toolCall?.result).toEqual({
|
|
401
|
+
content: [{ type: "text", text: "ok" }],
|
|
402
|
+
});
|
|
403
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
404
|
+
expect(stored?.toolCall?.completedAt).toBe(2_200);
|
|
405
|
+
expect(stored?.toolCall?.durationMs).toBe(1_200);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("a stale step_await re-emit does not reset in-flight WebMCP tool timing", () => {
|
|
409
|
+
const session = makeSession().session;
|
|
410
|
+
const s = session as unknown as {
|
|
411
|
+
webMcpInflightKeys: Set<string>;
|
|
412
|
+
upsertMessage: (m: AgentWidgetMessage) => void;
|
|
413
|
+
messages: AgentWidgetMessage[];
|
|
414
|
+
};
|
|
415
|
+
s.webMcpInflightKeys.add("exec-1:tool-1");
|
|
416
|
+
s.upsertMessage({
|
|
417
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
418
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
419
|
+
streaming: true,
|
|
420
|
+
toolCall: {
|
|
421
|
+
id: "tool-1",
|
|
422
|
+
name: "webmcp:search",
|
|
423
|
+
status: "running",
|
|
424
|
+
args: { q: "shoes" },
|
|
425
|
+
startedAt: 1_000,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
s.upsertMessage({
|
|
430
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
431
|
+
streaming: false,
|
|
432
|
+
toolCall: {
|
|
433
|
+
id: "tool-1",
|
|
434
|
+
name: "webmcp:search",
|
|
435
|
+
status: "running",
|
|
436
|
+
args: { q: "shoes" },
|
|
437
|
+
startedAt: 3_000,
|
|
438
|
+
},
|
|
301
439
|
});
|
|
302
|
-
|
|
303
|
-
s.upsertMessage(awaitingMessage("tool-1", "webmcp:search"));
|
|
440
|
+
|
|
304
441
|
const stored = s.messages.find((m) => m.toolCall?.id === "tool-1");
|
|
305
442
|
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
443
|
+
expect(stored?.streaming).toBe(true);
|
|
444
|
+
expect(stored?.toolCall?.status).toBe("running");
|
|
445
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
446
|
+
expect(stored?.toolCall?.durationMs).toBeUndefined();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("a stale step_await re-emit does not reset completed WebMCP local-error state", () => {
|
|
450
|
+
const session = makeSession().session;
|
|
451
|
+
const s = session as unknown as {
|
|
452
|
+
upsertMessage: (m: AgentWidgetMessage) => void;
|
|
453
|
+
messages: AgentWidgetMessage[];
|
|
454
|
+
};
|
|
455
|
+
s.upsertMessage({
|
|
456
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
457
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
458
|
+
streaming: false,
|
|
459
|
+
toolCall: {
|
|
460
|
+
id: "tool-1",
|
|
461
|
+
name: "webmcp:search",
|
|
462
|
+
status: "complete",
|
|
463
|
+
args: { q: "shoes" },
|
|
464
|
+
result: {
|
|
465
|
+
isError: true,
|
|
466
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
467
|
+
},
|
|
468
|
+
startedAt: 1_000,
|
|
469
|
+
completedAt: 1_500,
|
|
470
|
+
durationMs: 500,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
s.upsertMessage({
|
|
475
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
476
|
+
streaming: false,
|
|
477
|
+
toolCall: {
|
|
478
|
+
id: "tool-1",
|
|
479
|
+
name: "webmcp:search",
|
|
480
|
+
status: "running",
|
|
481
|
+
args: { q: "shoes" },
|
|
482
|
+
startedAt: 3_000,
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const stored = s.messages.find((m) => m.toolCall?.id === "tool-1");
|
|
487
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(false);
|
|
488
|
+
expect(stored?.streaming).toBe(false);
|
|
489
|
+
expect(stored?.toolCall?.status).toBe("complete");
|
|
490
|
+
expect(stored?.toolCall?.result).toEqual({
|
|
491
|
+
isError: true,
|
|
492
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
493
|
+
});
|
|
494
|
+
expect(stored?.toolCall?.startedAt).toBe(1_000);
|
|
495
|
+
expect(stored?.toolCall?.completedAt).toBe(1_500);
|
|
496
|
+
expect(stored?.toolCall?.durationMs).toBe(500);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("a new execution reusing a completed tool id is not treated as stale", () => {
|
|
500
|
+
const session = makeSession().session;
|
|
501
|
+
const s = session as unknown as {
|
|
502
|
+
upsertMessage: (m: AgentWidgetMessage) => void;
|
|
503
|
+
messages: AgentWidgetMessage[];
|
|
504
|
+
};
|
|
505
|
+
s.upsertMessage({
|
|
506
|
+
...awaitingMessage("tool-1", "webmcp:search"),
|
|
507
|
+
id: "tool-tool-1",
|
|
508
|
+
agentMetadata: { executionId: "exec-1", awaitingLocalTool: false },
|
|
509
|
+
streaming: false,
|
|
510
|
+
toolCall: {
|
|
511
|
+
id: "tool-1",
|
|
512
|
+
name: "webmcp:search",
|
|
513
|
+
status: "complete",
|
|
514
|
+
args: { q: "shoes" },
|
|
515
|
+
result: {
|
|
516
|
+
isError: true,
|
|
517
|
+
content: [{ type: "text", text: "page tool exploded" }],
|
|
518
|
+
},
|
|
519
|
+
startedAt: 1_000,
|
|
520
|
+
completedAt: 1_500,
|
|
521
|
+
durationMs: 500,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
s.upsertMessage({
|
|
526
|
+
...awaitingMessage("tool-1", "webmcp:search", "exec-2"),
|
|
527
|
+
id: "tool-tool-1",
|
|
528
|
+
streaming: false,
|
|
529
|
+
toolCall: {
|
|
530
|
+
id: "tool-1",
|
|
531
|
+
name: "webmcp:search",
|
|
532
|
+
status: "running",
|
|
533
|
+
args: { q: "boots" },
|
|
534
|
+
startedAt: 3_000,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const stored = s.messages.find((m) => m.id === "tool-tool-1");
|
|
539
|
+
expect(stored?.agentMetadata?.executionId).toBe("exec-2");
|
|
540
|
+
expect(stored?.agentMetadata?.awaitingLocalTool).toBe(true);
|
|
541
|
+
expect(stored?.toolCall?.status).toBe("running");
|
|
542
|
+
expect(stored?.toolCall?.args).toEqual({ q: "boots" });
|
|
543
|
+
expect(stored?.toolCall?.startedAt).toBe(3_000);
|
|
544
|
+
expect(stored?.toolCall?.result).toBeUndefined();
|
|
545
|
+
expect(stored?.toolCall?.completedAt).toBeUndefined();
|
|
546
|
+
expect(stored?.toolCall?.durationMs).toBeUndefined();
|
|
306
547
|
});
|
|
307
548
|
|
|
308
549
|
it("an error event does not clear streaming while a webmcp resolve is in flight", () => {
|
|
@@ -576,6 +817,10 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
576
817
|
vi.clearAllMocks();
|
|
577
818
|
});
|
|
578
819
|
|
|
820
|
+
afterEach(() => {
|
|
821
|
+
vi.useRealTimers();
|
|
822
|
+
});
|
|
823
|
+
|
|
579
824
|
// A `step_await(local_tool_required)` message as client.ts emits it for a
|
|
580
825
|
// PARALLEL local-tool call: the per-call `toolCallId` is both the toolCall.id
|
|
581
826
|
// AND `agentMetadata.webMcpToolCallId`. Two of these for one executionId share
|
|
@@ -705,6 +950,81 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
705
950
|
expect(Object.keys(toolOutputs).sort()).toEqual(["toolu_A", "toolu_B"]);
|
|
706
951
|
});
|
|
707
952
|
|
|
953
|
+
it("records elapsed time for each resolved tool in a batched WebMCP resume", async () => {
|
|
954
|
+
vi.useFakeTimers();
|
|
955
|
+
vi.setSystemTime(1_000);
|
|
956
|
+
|
|
957
|
+
let releaseA: (r: WebMcpToolResult) => void = () => undefined;
|
|
958
|
+
let releaseB: (r: WebMcpToolResult) => void = () => undefined;
|
|
959
|
+
const pA = new Promise<WebMcpToolResult>((r) => (releaseA = r));
|
|
960
|
+
const pB = new Promise<WebMcpToolResult>((r) => (releaseB = r));
|
|
961
|
+
const { session, resumeSpy } = makeSession();
|
|
962
|
+
const client = (session as unknown as { client: Record<string, unknown> })
|
|
963
|
+
.client;
|
|
964
|
+
(client.executeWebMcpToolCall as ReturnType<typeof vi.fn>).mockImplementation(
|
|
965
|
+
(_name: string, args: { sku: string }) =>
|
|
966
|
+
args.sku === "SHOE-001" ? pA : pB,
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
970
|
+
feed(session, parallelAwait("toolu_B", "SHOE-007"));
|
|
971
|
+
endStream(session);
|
|
972
|
+
await flushMicrotasks();
|
|
973
|
+
|
|
974
|
+
vi.setSystemTime(1_800);
|
|
975
|
+
releaseA({ content: [{ type: "text", text: "a" }] });
|
|
976
|
+
await flushMicrotasks();
|
|
977
|
+
|
|
978
|
+
let messages = (session as unknown as { messages: AgentWidgetMessage[] })
|
|
979
|
+
.messages;
|
|
980
|
+
const toolA = messages.find((m) => m.toolCall?.id === "toolu_A");
|
|
981
|
+
const toolBWhileRunning = messages.find((m) => m.toolCall?.id === "toolu_B");
|
|
982
|
+
expect(toolA?.toolCall?.status).toBe("running");
|
|
983
|
+
expect(toolA?.toolCall?.durationMs).toBeUndefined();
|
|
984
|
+
expect(toolBWhileRunning?.toolCall?.status).toBe("running");
|
|
985
|
+
expect(resumeSpy).not.toHaveBeenCalled();
|
|
986
|
+
|
|
987
|
+
vi.setSystemTime(2_600);
|
|
988
|
+
releaseB({ content: [{ type: "text", text: "b" }] });
|
|
989
|
+
await flushMicrotasks();
|
|
990
|
+
|
|
991
|
+
messages = (session as unknown as { messages: AgentWidgetMessage[] })
|
|
992
|
+
.messages;
|
|
993
|
+
const completedToolA = messages.find((m) => m.toolCall?.id === "toolu_A");
|
|
994
|
+
const toolB = messages.find((m) => m.toolCall?.id === "toolu_B");
|
|
995
|
+
expect(completedToolA?.toolCall?.status).toBe("complete");
|
|
996
|
+
expect(completedToolA?.toolCall?.durationMs).toBe(800);
|
|
997
|
+
expect(toolB?.toolCall?.status).toBe("complete");
|
|
998
|
+
expect(toolB?.toolCall?.durationMs).toBe(1_600);
|
|
999
|
+
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
it("does not mark batched WebMCP bubbles complete when the shared /resume fails", async () => {
|
|
1003
|
+
const { session, client, resumeSpy } = makeSession();
|
|
1004
|
+
(client.resumeFlow as ReturnType<typeof vi.fn>).mockImplementationOnce(
|
|
1005
|
+
async () => {
|
|
1006
|
+
throw new Error("resume failed");
|
|
1007
|
+
},
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
1011
|
+
feed(session, parallelAwait("toolu_B", "SHOE-007"));
|
|
1012
|
+
endStream(session);
|
|
1013
|
+
await flushMicrotasks();
|
|
1014
|
+
|
|
1015
|
+
const messages = (session as unknown as { messages: AgentWidgetMessage[] })
|
|
1016
|
+
.messages;
|
|
1017
|
+
const toolA = messages.find((m) => m.toolCall?.id === "toolu_A");
|
|
1018
|
+
const toolB = messages.find((m) => m.toolCall?.id === "toolu_B");
|
|
1019
|
+
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
|
1020
|
+
expect(toolA?.toolCall?.status).toBe("running");
|
|
1021
|
+
expect(toolA?.toolCall?.result).toBeUndefined();
|
|
1022
|
+
expect(toolA?.toolCall?.durationMs).toBeUndefined();
|
|
1023
|
+
expect(toolB?.toolCall?.status).toBe("running");
|
|
1024
|
+
expect(toolB?.toolCall?.result).toBeUndefined();
|
|
1025
|
+
expect(toolB?.toolCall?.durationMs).toBeUndefined();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
708
1028
|
it("dedupes a duplicate parallel await within the same batch", async () => {
|
|
709
1029
|
const { session, executeSpy, resumeSpy } = makeSession();
|
|
710
1030
|
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
@@ -49,6 +49,16 @@ describe("theme editor preview demo data", () => {
|
|
|
49
49
|
expect(reasoningMessage.reasoning?.status).toBe("streaming");
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
+
it("creates a pending approval preview entry for theming the approval bubble", () => {
|
|
53
|
+
const message = createPreviewTranscriptEntry("approval-request", 3);
|
|
54
|
+
|
|
55
|
+
expect(message.variant).toBe("approval");
|
|
56
|
+
expect(message.approval?.status).toBe("pending");
|
|
57
|
+
expect(message.approval?.toolName).toBe("add_to_cart");
|
|
58
|
+
// Includes parameters so the params block is exercised when theming.
|
|
59
|
+
expect(message.approval?.parameters).toBeTruthy();
|
|
60
|
+
});
|
|
61
|
+
|
|
52
62
|
it("appends public preview transcript entries to an existing conversation", () => {
|
|
53
63
|
const messages = appendPreviewTranscriptEntry([], "tool-running");
|
|
54
64
|
const updated = appendPreviewTranscriptEntry(messages, "reasoning-complete");
|
|
@@ -206,7 +206,8 @@ export type PreviewTranscriptEntryPreset =
|
|
|
206
206
|
| 'reasoning-streaming'
|
|
207
207
|
| 'reasoning-complete'
|
|
208
208
|
| 'tool-running'
|
|
209
|
-
| 'tool-complete'
|
|
209
|
+
| 'tool-complete'
|
|
210
|
+
| 'approval-request';
|
|
210
211
|
|
|
211
212
|
const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, string> = {
|
|
212
213
|
'user-message': 'User message',
|
|
@@ -218,6 +219,7 @@ const PREVIEW_TRANSCRIPT_PRESET_LABELS: Record<PreviewTranscriptEntryPreset, str
|
|
|
218
219
|
'reasoning-complete': 'Reasoning (complete)',
|
|
219
220
|
'tool-running': 'Tool call (running)',
|
|
220
221
|
'tool-complete': 'Tool call (complete)',
|
|
222
|
+
'approval-request': 'Approval request',
|
|
221
223
|
};
|
|
222
224
|
|
|
223
225
|
export function getPreviewTranscriptPresetLabel(preset: PreviewTranscriptEntryPreset): string {
|
|
@@ -325,6 +327,32 @@ export function createPreviewTranscriptEntry(
|
|
|
325
327
|
durationMs: 1200,
|
|
326
328
|
},
|
|
327
329
|
};
|
|
330
|
+
case 'approval-request': {
|
|
331
|
+
// Use the `approval-<approvalId>` id convention so the optimistic status
|
|
332
|
+
// flip in `session.resolveApproval` updates this bubble in place when the
|
|
333
|
+
// Approve/Deny buttons are clicked.
|
|
334
|
+
const approvalId = `preview-approval-${suffix}`;
|
|
335
|
+
return {
|
|
336
|
+
id: `approval-${approvalId}`,
|
|
337
|
+
role: 'assistant',
|
|
338
|
+
content: '',
|
|
339
|
+
createdAt,
|
|
340
|
+
streaming: false,
|
|
341
|
+
variant: 'approval',
|
|
342
|
+
approval: {
|
|
343
|
+
id: approvalId,
|
|
344
|
+
status: 'pending',
|
|
345
|
+
agentId: 'preview-agent',
|
|
346
|
+
executionId: `preview-exec-${suffix}`,
|
|
347
|
+
toolName: 'add_to_cart',
|
|
348
|
+
description:
|
|
349
|
+
'Add the selected item to the shopping cart. Approve to let the assistant continue.',
|
|
350
|
+
parameters: {
|
|
351
|
+
items: [{ productEntityId: 129, quantity: 1 }],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
328
356
|
case 'tool-complete':
|
|
329
357
|
return {
|
|
330
358
|
id: `preview-seq-tool-complete-${suffix}`,
|
|
@@ -27,6 +27,23 @@ describe("theme editor scroll-to-bottom controls", () => {
|
|
|
27
27
|
expect(fieldPaths).toContain("theme.components.scrollToBottom.iconSize");
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("exposes a shadow control for every themeable component", () => {
|
|
31
|
+
const fieldPaths = COMPONENTS_SECTIONS.flatMap((section) => section.fields.map((field) => field.path));
|
|
32
|
+
|
|
33
|
+
// Pre-existing shadow controls.
|
|
34
|
+
expect(fieldPaths).toContain("theme.components.launcher.shadow");
|
|
35
|
+
expect(fieldPaths).toContain("theme.components.panel.shadow");
|
|
36
|
+
expect(fieldPaths).toContain("theme.components.scrollToBottom.shadow");
|
|
37
|
+
// Newly added component shadow controls.
|
|
38
|
+
expect(fieldPaths).toContain("theme.components.message.user.shadow");
|
|
39
|
+
expect(fieldPaths).toContain("theme.components.message.assistant.shadow");
|
|
40
|
+
expect(fieldPaths).toContain("theme.components.toolBubble.shadow");
|
|
41
|
+
expect(fieldPaths).toContain("theme.components.reasoningBubble.shadow");
|
|
42
|
+
expect(fieldPaths).toContain("theme.components.approval.requested.shadow");
|
|
43
|
+
expect(fieldPaths).toContain("theme.components.introCard.shadow");
|
|
44
|
+
expect(fieldPaths).toContain("theme.components.composer.shadow");
|
|
45
|
+
});
|
|
46
|
+
|
|
30
47
|
it("adds a scroll-to-bottom interface role mapping", () => {
|
|
31
48
|
const role = ALL_ROLES.find((entry) => entry.roleId === "role-scroll-to-bottom");
|
|
32
49
|
|
|
@@ -455,6 +455,36 @@ const scrollToBottomSectionDef: SectionDef = {
|
|
|
455
455
|
],
|
|
456
456
|
};
|
|
457
457
|
|
|
458
|
+
// Reusable shadow-scale options matching the launcher / panel / scroll-to-bottom selects.
|
|
459
|
+
const SHADOW_SCALE_OPTIONS: { value: string; label: string }[] = [
|
|
460
|
+
{ value: 'palette.shadows.none', label: 'None' },
|
|
461
|
+
{ value: 'palette.shadows.sm', label: 'Small' },
|
|
462
|
+
{ value: 'palette.shadows.md', label: 'Medium' },
|
|
463
|
+
{ value: 'palette.shadows.lg', label: 'Large' },
|
|
464
|
+
{ value: 'palette.shadows.xl', label: 'Extra Large' },
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
// The approval and intro-card defaults are a bespoke "card" shadow rather than a
|
|
468
|
+
// palette step, so expose it as a leading "Default" option users can keep.
|
|
469
|
+
const CARD_SHADOW = '0 5px 15px rgba(15, 23, 42, 0.08)';
|
|
470
|
+
const CARD_SHADOW_OPTIONS = [{ value: CARD_SHADOW, label: 'Default' }, ...SHADOW_SCALE_OPTIONS];
|
|
471
|
+
|
|
472
|
+
const componentShadowsSectionDef: SectionDef = {
|
|
473
|
+
id: 'comp-shadows',
|
|
474
|
+
title: 'Component Shadows',
|
|
475
|
+
description: 'Box-shadow for chat bubbles and surfaces.',
|
|
476
|
+
collapsed: true,
|
|
477
|
+
fields: [
|
|
478
|
+
{ id: 'shadow-msg-user', label: 'User Message', type: 'select', path: 'theme.components.message.user.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
479
|
+
{ id: 'shadow-msg-assistant', label: 'Assistant Message', type: 'select', path: 'theme.components.message.assistant.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
480
|
+
{ id: 'shadow-tool-bubble', label: 'Tool Call Bubble', type: 'select', path: 'theme.components.toolBubble.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
481
|
+
{ id: 'shadow-reasoning-bubble', label: 'Reasoning Bubble', type: 'select', path: 'theme.components.reasoningBubble.shadow', defaultValue: 'palette.shadows.sm', options: SHADOW_SCALE_OPTIONS },
|
|
482
|
+
{ id: 'shadow-approval', label: 'Approval Bubble', type: 'select', path: 'theme.components.approval.requested.shadow', defaultValue: CARD_SHADOW, options: CARD_SHADOW_OPTIONS },
|
|
483
|
+
{ id: 'shadow-intro-card', label: 'Intro Card', type: 'select', path: 'theme.components.introCard.shadow', defaultValue: CARD_SHADOW, options: CARD_SHADOW_OPTIONS },
|
|
484
|
+
{ id: 'shadow-composer', label: 'Composer', type: 'select', path: 'theme.components.composer.shadow', defaultValue: 'palette.shadows.none', options: SHADOW_SCALE_OPTIONS },
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
|
|
458
488
|
/** Shared shape sections (not scoped to light/dark) */
|
|
459
489
|
export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
|
|
460
490
|
panelLayoutSectionDef,
|
|
@@ -462,6 +492,7 @@ export const COMPONENT_SHAPE_SECTIONS: SectionDef[] = [
|
|
|
462
492
|
messageShapeSectionDef,
|
|
463
493
|
inputShapeSectionDef,
|
|
464
494
|
buttonShapeSectionDef,
|
|
495
|
+
componentShadowsSectionDef,
|
|
465
496
|
];
|
|
466
497
|
|
|
467
498
|
/** Component color sections (can be scoped for light/dark) */
|
package/src/theme-reference.ts
CHANGED
|
@@ -136,7 +136,7 @@ export const THEME_TOKEN_DOCS = {
|
|
|
136
136
|
voice:
|
|
137
137
|
'recording (indicator, background, border), processing (icon, background), speaking (icon).',
|
|
138
138
|
approval:
|
|
139
|
-
'requested (background, border, text), approve (background, foreground), deny (background, foreground).',
|
|
139
|
+
'requested (background, border, text, shadow), approve (background, foreground), deny (background, foreground).',
|
|
140
140
|
attachment: 'image (background, border).',
|
|
141
141
|
scrollToBottom:
|
|
142
142
|
'Floating scroll-to-bottom affordance shared by transcript and event stream: background, foreground, border, size, borderRadius, shadow, padding, gap, fontSize, iconSize.',
|
package/src/types/theme.ts
CHANGED
|
@@ -330,6 +330,8 @@ export interface ApprovalTokens {
|
|
|
330
330
|
background: TokenReference<'color'>;
|
|
331
331
|
border: TokenReference<'color'>;
|
|
332
332
|
text: TokenReference<'color'>;
|
|
333
|
+
/** Box-shadow for the approval bubble (token ref or raw CSS, e.g. `none`). */
|
|
334
|
+
shadow: string;
|
|
333
335
|
};
|
|
334
336
|
approve: ComponentTokenSet;
|
|
335
337
|
deny: ComponentTokenSet;
|
package/src/types.ts
CHANGED
|
@@ -255,6 +255,12 @@ export type WebMcpConfirmInfo = {
|
|
|
255
255
|
toolName: string;
|
|
256
256
|
args: unknown;
|
|
257
257
|
description?: string;
|
|
258
|
+
/**
|
|
259
|
+
* Display title the tool declared via the WebMCP spec's
|
|
260
|
+
* `ToolDescriptor.title` (e.g. `"Add to Cart"`). Absent when the tool
|
|
261
|
+
* didn't declare one.
|
|
262
|
+
*/
|
|
263
|
+
title?: string;
|
|
258
264
|
annotations?: {
|
|
259
265
|
readOnlyHint?: boolean;
|
|
260
266
|
untrustedContentHint?: boolean;
|
|
@@ -1816,6 +1822,23 @@ export interface VoiceProvider {
|
|
|
1816
1822
|
stopPlayback?(): void;
|
|
1817
1823
|
}
|
|
1818
1824
|
|
|
1825
|
+
/**
|
|
1826
|
+
* Extra context for an approval decision, surfaced to `onDecision` and to the
|
|
1827
|
+
* `approve`/`deny` callbacks passed to the `renderApproval` plugin hook.
|
|
1828
|
+
*/
|
|
1829
|
+
export type AgentWidgetApprovalDecisionOptions = {
|
|
1830
|
+
/**
|
|
1831
|
+
* The user chose a "remember this" affordance (e.g. an "Always allow"
|
|
1832
|
+
* button) rather than a one-time decision. The widget resolves the *current*
|
|
1833
|
+
* approval identically whether or not this is set — an approval bubble is a
|
|
1834
|
+
* single binary gate (`approved`/`denied`). Persisting a don't-ask-again
|
|
1835
|
+
* policy for *future* approvals (auto-resolving them, or not surfacing them)
|
|
1836
|
+
* is the integrator's responsibility, typically inside `onDecision`.
|
|
1837
|
+
* Defaults to absent/`false`.
|
|
1838
|
+
*/
|
|
1839
|
+
remember?: boolean;
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1819
1842
|
/**
|
|
1820
1843
|
* Configuration for tool approval bubbles.
|
|
1821
1844
|
* Controls styling, labels, and behavior of the approval UI.
|
|
@@ -1825,6 +1848,8 @@ export type AgentWidgetApprovalConfig = {
|
|
|
1825
1848
|
backgroundColor?: string;
|
|
1826
1849
|
/** Border color of the approval bubble */
|
|
1827
1850
|
borderColor?: string;
|
|
1851
|
+
/** Box-shadow for the approval bubble; pass `"none"` to remove it. Overrides the default `persona-shadow-sm`. */
|
|
1852
|
+
shadow?: string;
|
|
1828
1853
|
/** Color for the title text */
|
|
1829
1854
|
titleColor?: string;
|
|
1830
1855
|
/** Color for the description text */
|
|
@@ -1847,19 +1872,51 @@ export type AgentWidgetApprovalConfig = {
|
|
|
1847
1872
|
approveLabel?: string;
|
|
1848
1873
|
/** Label for the deny button */
|
|
1849
1874
|
denyLabel?: string;
|
|
1875
|
+
/**
|
|
1876
|
+
* How the technical details (the tool's agent-facing description and the
|
|
1877
|
+
* raw parameters JSON) are presented:
|
|
1878
|
+
* - `"collapsed"` (default) — hidden behind a "Show details" toggle
|
|
1879
|
+
* - `"expanded"` — visible, with the toggle available to hide them
|
|
1880
|
+
* - `"hidden"` — never rendered
|
|
1881
|
+
*/
|
|
1882
|
+
detailsDisplay?: "collapsed" | "expanded" | "hidden";
|
|
1883
|
+
/** Label for the toggle that reveals the technical details */
|
|
1884
|
+
showDetailsLabel?: string;
|
|
1885
|
+
/** Label for the toggle that hides the technical details */
|
|
1886
|
+
hideDetailsLabel?: string;
|
|
1887
|
+
/**
|
|
1888
|
+
* Build the user-facing summary line for an approval request. Overrides the
|
|
1889
|
+
* default "The assistant wants to use “Tool name”." copy. Return a falsy
|
|
1890
|
+
* value to fall back to the default for that approval. `displayTitle` is
|
|
1891
|
+
* the display name the tool declared via the WebMCP spec's
|
|
1892
|
+
* `ToolDescriptor.title`, when one exists.
|
|
1893
|
+
*/
|
|
1894
|
+
formatDescription?: (approval: {
|
|
1895
|
+
toolName: string;
|
|
1896
|
+
toolType?: string;
|
|
1897
|
+
description: string;
|
|
1898
|
+
parameters?: unknown;
|
|
1899
|
+
displayTitle?: string;
|
|
1900
|
+
}) => string | undefined;
|
|
1850
1901
|
/**
|
|
1851
1902
|
* Custom handler for approval decisions.
|
|
1852
1903
|
* Return void to let the SDK auto-resolve via the API,
|
|
1853
1904
|
* or return a Response/ReadableStream for custom handling.
|
|
1905
|
+
*
|
|
1906
|
+
* `options.remember` is `true` when the decision came from a "remember this"
|
|
1907
|
+
* affordance (e.g. an "Always allow" button in a custom `renderApproval`
|
|
1908
|
+
* plugin). Use it to persist a don't-ask-again policy for future approvals;
|
|
1909
|
+
* the current approval resolves the same way regardless.
|
|
1854
1910
|
*/
|
|
1855
1911
|
onDecision?: (
|
|
1856
1912
|
data: { approvalId: string; executionId: string; agentId: string; toolName: string },
|
|
1857
|
-
decision: 'approved' | 'denied'
|
|
1913
|
+
decision: 'approved' | 'denied',
|
|
1914
|
+
options?: AgentWidgetApprovalDecisionOptions
|
|
1858
1915
|
) => Promise<Response | ReadableStream<Uint8Array> | void>;
|
|
1859
1916
|
};
|
|
1860
1917
|
|
|
1861
1918
|
export type AgentWidgetToolCallConfig = {
|
|
1862
|
-
/** Box-shadow for tool-call bubbles;
|
|
1919
|
+
/** Box-shadow for tool-call bubbles; pass `"none"` to remove it. Overrides the `components.toolBubble.shadow` token / `--persona-tool-bubble-shadow`. */
|
|
1863
1920
|
shadow?: string;
|
|
1864
1921
|
/** Background color of the tool call bubble container. */
|
|
1865
1922
|
backgroundColor?: string;
|