@runtypelabs/persona 3.34.0 → 3.35.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 +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +67 -12
- package/src/client.ts +40 -32
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Session-level wiring for the realtime (runtype) voice path (Option B).
|
|
2
|
+
//
|
|
3
|
+
// The provider's protocol behavior is covered in voice/voice.test.ts; here we
|
|
4
|
+
// mock the voice factory to a fake provider and drive its onTranscript/onMetrics
|
|
5
|
+
// callbacks to verify how session.setupVoice() feeds the chat thread.
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import type { AgentWidgetMessage, VoiceMetrics } from './types';
|
|
8
|
+
|
|
9
|
+
// vi.mock is hoisted above module init, so the shared fake must be hoisted too.
|
|
10
|
+
const h = vi.hoisted(() => {
|
|
11
|
+
const state: {
|
|
12
|
+
transcriptCb:
|
|
13
|
+
| ((role: 'user' | 'assistant', text: string, isFinal: boolean) => void)
|
|
14
|
+
| null;
|
|
15
|
+
metricsCb: ((m: VoiceMetrics) => void) | null;
|
|
16
|
+
} = { transcriptCb: null, metricsCb: null };
|
|
17
|
+
|
|
18
|
+
const fakeProvider = {
|
|
19
|
+
type: 'runtype' as const,
|
|
20
|
+
connect: async () => {},
|
|
21
|
+
disconnect: async () => {},
|
|
22
|
+
startListening: async () => {},
|
|
23
|
+
stopListening: async () => {},
|
|
24
|
+
onResult: () => {},
|
|
25
|
+
onError: () => {},
|
|
26
|
+
onStatusChange: () => {},
|
|
27
|
+
onTranscript: (cb: typeof state.transcriptCb) => {
|
|
28
|
+
state.transcriptCb = cb;
|
|
29
|
+
},
|
|
30
|
+
onMetrics: (cb: typeof state.metricsCb) => {
|
|
31
|
+
state.metricsCb = cb;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return { state, fakeProvider };
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
vi.mock('./voice', async (importOriginal) => {
|
|
39
|
+
// Keep the real exports (ReadAloudController, BrowserSpeechEngine, …) and
|
|
40
|
+
// override only the provider factories so the realtime voice flow uses the
|
|
41
|
+
// fake provider.
|
|
42
|
+
const actual = await importOriginal<typeof import('./voice')>();
|
|
43
|
+
return {
|
|
44
|
+
...actual,
|
|
45
|
+
createVoiceProvider: () => h.fakeProvider,
|
|
46
|
+
createBestAvailableVoiceProvider: () => h.fakeProvider,
|
|
47
|
+
isVoiceSupported: () => true,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
import { AgentWidgetSession } from './session';
|
|
52
|
+
|
|
53
|
+
describe('AgentWidgetSession - realtime voice onTranscript (Option B)', () => {
|
|
54
|
+
let session: AgentWidgetSession;
|
|
55
|
+
let messages: AgentWidgetMessage[] = [];
|
|
56
|
+
let streaming = false;
|
|
57
|
+
let metricsSeen: VoiceMetrics[] = [];
|
|
58
|
+
|
|
59
|
+
const drive = (
|
|
60
|
+
role: 'user' | 'assistant',
|
|
61
|
+
text: string,
|
|
62
|
+
isFinal: boolean,
|
|
63
|
+
) => h.state.transcriptCb!(role, text, isFinal);
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
h.state.transcriptCb = null;
|
|
67
|
+
h.state.metricsCb = null;
|
|
68
|
+
messages = [];
|
|
69
|
+
streaming = false;
|
|
70
|
+
metricsSeen = [];
|
|
71
|
+
|
|
72
|
+
session = new AgentWidgetSession(
|
|
73
|
+
{
|
|
74
|
+
apiUrl: 'http://localhost:8000',
|
|
75
|
+
voiceRecognition: {
|
|
76
|
+
enabled: true,
|
|
77
|
+
provider: { type: 'runtype', runtype: { agentId: 'a1' } },
|
|
78
|
+
onMetrics: (m) => {
|
|
79
|
+
metricsSeen.push(m);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
onMessagesChanged: (m) => {
|
|
85
|
+
messages = m;
|
|
86
|
+
},
|
|
87
|
+
onStatusChanged: () => {},
|
|
88
|
+
onStreamingChanged: (s) => {
|
|
89
|
+
streaming = s;
|
|
90
|
+
},
|
|
91
|
+
onError: () => {},
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
session.setupVoice();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('registers an onTranscript handler', () => {
|
|
98
|
+
expect(h.state.transcriptCb).toBeTypeOf('function');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('grows the user bubble live, then finalizes and shows a typing indicator', () => {
|
|
102
|
+
drive('user', 'what are', false);
|
|
103
|
+
expect(messages).toHaveLength(1);
|
|
104
|
+
expect(messages[0].role).toBe('user');
|
|
105
|
+
expect(messages[0].content).toBe('what are');
|
|
106
|
+
expect(messages[0].voiceProcessing).toBe(true);
|
|
107
|
+
|
|
108
|
+
drive('user', 'what are your hours', false);
|
|
109
|
+
expect(messages).toHaveLength(1); // upsert in place, not a new bubble
|
|
110
|
+
expect(messages[0].content).toBe('what are your hours');
|
|
111
|
+
|
|
112
|
+
drive('user', 'what are your hours?', true);
|
|
113
|
+
expect(messages).toHaveLength(2);
|
|
114
|
+
expect(messages[0].content).toBe('what are your hours?');
|
|
115
|
+
expect(messages[0].voiceProcessing).toBe(false); // cleared on final
|
|
116
|
+
expect(messages[1].role).toBe('assistant');
|
|
117
|
+
expect(messages[1].content).toBe('');
|
|
118
|
+
expect(messages[1].streaming).toBe(true); // typing indicator
|
|
119
|
+
expect(streaming).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('fills the assistant reply on its final frame and clears streaming', () => {
|
|
123
|
+
drive('user', 'hi', true);
|
|
124
|
+
drive('assistant', 'Hello! How can I help?', true);
|
|
125
|
+
|
|
126
|
+
const assistant = messages.find((m) => m.role === 'assistant');
|
|
127
|
+
expect(assistant?.content).toBe('Hello! How can I help?');
|
|
128
|
+
expect(assistant?.streaming).toBe(false);
|
|
129
|
+
expect(assistant?.voiceProcessing).toBe(false);
|
|
130
|
+
expect(streaming).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('starts a fresh user bubble on the next turn', () => {
|
|
134
|
+
drive('user', 'first', true);
|
|
135
|
+
drive('assistant', 'reply one', true);
|
|
136
|
+
drive('user', 'second', false);
|
|
137
|
+
|
|
138
|
+
const userMessages = messages.filter((m) => m.role === 'user');
|
|
139
|
+
expect(userMessages.map((m) => m.content)).toEqual(['first', 'second']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('forwards metrics to the config hook', () => {
|
|
143
|
+
h.state.metricsCb!({ llmMs: 100, totalMs: 250 });
|
|
144
|
+
expect(metricsSeen).toEqual([{ llmMs: 100, totalMs: 250 }]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -33,7 +33,7 @@ const makeSession = (overrides?: {
|
|
|
33
33
|
content: [{ type: "text", text: "ok" }],
|
|
34
34
|
}),
|
|
35
35
|
);
|
|
36
|
-
// Mimic AgentWidgetClient.executeWebMcpToolCall
|
|
36
|
+
// Mimic AgentWidgetClient.executeWebMcpToolCall: returns null when bridge
|
|
37
37
|
// not configured. We toggle via isOperational below.
|
|
38
38
|
client.executeWebMcpToolCall = vi.fn(
|
|
39
39
|
() =>
|
|
@@ -75,7 +75,7 @@ const awaitingMessage = (
|
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
describe("AgentWidgetSession
|
|
78
|
+
describe("AgentWidgetSession: WebMCP resolve", () => {
|
|
79
79
|
beforeEach(() => {
|
|
80
80
|
vi.clearAllMocks();
|
|
81
81
|
});
|
|
@@ -130,7 +130,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
130
130
|
|
|
131
131
|
it("still resumes (with isError) when the bridge is not operational", async () => {
|
|
132
132
|
// BugBot finding #1: previously, handleEvent skipped resolveWebMcpToolCall
|
|
133
|
-
// entirely when `isWebMcpOperational()` was false
|
|
133
|
+
// entirely when `isWebMcpOperational()` was false: leaving the dispatch
|
|
134
134
|
// hung. The session must surface an actionable error to /resume instead.
|
|
135
135
|
const { session, resumeSpy } = makeSession({
|
|
136
136
|
isOperational: false,
|
|
@@ -190,8 +190,8 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
190
190
|
expect(afterFailedResume?.toolCall?.result).toBeUndefined();
|
|
191
191
|
expect(afterFailedResume?.toolCall?.durationMs).toBeUndefined();
|
|
192
192
|
|
|
193
|
-
await session.resolveWebMcpToolCall(msg); // retry
|
|
194
|
-
await session.resolveWebMcpToolCall(msg); // post-success
|
|
193
|
+
await session.resolveWebMcpToolCall(msg); // retry: must be allowed
|
|
194
|
+
await session.resolveWebMcpToolCall(msg); // post-success: must be blocked
|
|
195
195
|
|
|
196
196
|
expect(executeSpy).toHaveBeenCalledTimes(2);
|
|
197
197
|
expect(resumeSpy).toHaveBeenCalledTimes(2);
|
|
@@ -233,7 +233,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
233
233
|
});
|
|
234
234
|
|
|
235
235
|
it("aborts an in-flight resolve when cancel() is called", async () => {
|
|
236
|
-
// BugBot finding #6 (cont.)
|
|
236
|
+
// BugBot finding #6 (cont.): the bridge execute race should reject on
|
|
237
237
|
// cancel so the dispatch doesn't fire a stale /resume after the user
|
|
238
238
|
// stops.
|
|
239
239
|
let release: () => void = () => undefined;
|
|
@@ -263,8 +263,8 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
263
263
|
it("does NOT abort the shared session abortController", async () => {
|
|
264
264
|
// The chained-turn fix: a webmcp resolve must leave `this.abortController`
|
|
265
265
|
// untouched. In a chain (tool A → /resume → tool B) that shared controller
|
|
266
|
-
// is still piping A's resume SSE
|
|
267
|
-
// step_await
|
|
266
|
+
// is still piping A's resume SSE: the very stream that just delivered B's
|
|
267
|
+
// step_await, so aborting it strands B (it never executes; its /resume is
|
|
268
268
|
// never POSTed; the dispatch hangs forever). Resolves use a dedicated
|
|
269
269
|
// per-call controller tracked in `webMcpResolveControllers` instead.
|
|
270
270
|
const { session } = makeSession();
|
|
@@ -547,7 +547,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
547
547
|
});
|
|
548
548
|
|
|
549
549
|
it("an error event does not clear streaming while a webmcp resolve is in flight", () => {
|
|
550
|
-
// BugBot: the error handler mirrors the idle handler
|
|
550
|
+
// BugBot: the error handler mirrors the idle handler: it must not tear
|
|
551
551
|
// down streaming while a sibling/successor resolve is still executing.
|
|
552
552
|
const stuck = new Promise<WebMcpToolResult>(() => undefined);
|
|
553
553
|
const session = makeSession({ executeImpl: () => stuck }).session;
|
|
@@ -559,7 +559,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
559
559
|
});
|
|
560
560
|
|
|
561
561
|
it("connectStream error does not clear streaming while a webmcp resolve is in flight", async () => {
|
|
562
|
-
// BugBot: connectStream's catch mirrors the error/idle handlers
|
|
562
|
+
// BugBot: connectStream's catch mirrors the error/idle handlers: a failed
|
|
563
563
|
// resume stream must not tear down streaming while another resolve runs.
|
|
564
564
|
const session = new AgentWidgetSession(
|
|
565
565
|
{ apiUrl: "http://test", webmcp: { enabled: true } },
|
|
@@ -603,7 +603,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
603
603
|
// BugBot finding #14: previously the bridge was constructed whenever a
|
|
604
604
|
// `webmcp` block existed, regardless of `enabled`. That left
|
|
605
605
|
// `executeWebMcpToolCall` returning a non-null promise even when WebMCP
|
|
606
|
-
// was explicitly disabled
|
|
606
|
+
// was explicitly disabled: making the session's "WebMCP not enabled"
|
|
607
607
|
// resume branch dead code. Constructor now gates on `enabled === true`.
|
|
608
608
|
const session = new AgentWidgetSession(
|
|
609
609
|
{ apiUrl: "http://test", webmcp: { enabled: false } },
|
|
@@ -652,7 +652,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
652
652
|
|
|
653
653
|
it("marks resolved on HTTP /resume success, not on stream completion", async () => {
|
|
654
654
|
// BugBot finding #8: if the resume HTTP response is OK but the downstream
|
|
655
|
-
// SSE stream errors, we still want dedupe to block re-emits
|
|
655
|
+
// SSE stream errors, we still want dedupe to block re-emits: the server
|
|
656
656
|
// has already accepted the answer.
|
|
657
657
|
const { session, resumeSpy, executeSpy } = makeSession();
|
|
658
658
|
// Make connectStream throw to simulate a broken downstream SSE.
|
|
@@ -663,7 +663,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
663
663
|
|
|
664
664
|
const msg = awaitingMessage("tool-1", "webmcp:search");
|
|
665
665
|
await session.resolveWebMcpToolCall(msg);
|
|
666
|
-
await session.resolveWebMcpToolCall(msg); // re-emit
|
|
666
|
+
await session.resolveWebMcpToolCall(msg); // re-emit: must be blocked
|
|
667
667
|
|
|
668
668
|
expect(executeSpy).toHaveBeenCalledTimes(1);
|
|
669
669
|
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
|
@@ -678,7 +678,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
678
678
|
await session.resolveWebMcpToolCall(
|
|
679
679
|
awaitingMessage("tool-1", "webmcp:search", "exec-1"),
|
|
680
680
|
);
|
|
681
|
-
// Different execution, same toolCall.id
|
|
681
|
+
// Different execution, same toolCall.id: must be allowed.
|
|
682
682
|
await session.resolveWebMcpToolCall(
|
|
683
683
|
awaitingMessage("tool-1", "webmcp:search", "exec-2"),
|
|
684
684
|
);
|
|
@@ -690,12 +690,12 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
690
690
|
// BugBot finding #11: clearing the resolved set on sendMessage would
|
|
691
691
|
// let a stale step_await from the prior /resume's still-active SSE
|
|
692
692
|
// re-trigger execute(). With executionId-scoped keys, the prior
|
|
693
|
-
// execution's resolved entries persist
|
|
693
|
+
// execution's resolved entries persist, so stale re-emits stay blocked.
|
|
694
694
|
const { session, executeSpy, resumeSpy } = makeSession();
|
|
695
695
|
await session.resolveWebMcpToolCall(
|
|
696
696
|
awaitingMessage("tool-1", "webmcp:search", "exec-1"),
|
|
697
697
|
);
|
|
698
|
-
// Stale re-emit from exec-1 after a new turn started
|
|
698
|
+
// Stale re-emit from exec-1 after a new turn started: still blocked.
|
|
699
699
|
await session.resolveWebMcpToolCall(
|
|
700
700
|
awaitingMessage("tool-1", "webmcp:search", "exec-1"),
|
|
701
701
|
);
|
|
@@ -781,7 +781,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
781
781
|
|
|
782
782
|
it("keys a single call's /resume by webMcpToolCallId when present", async () => {
|
|
783
783
|
// core#3878: when the server emits a per-call id, the single-call path keys
|
|
784
|
-
// /resume by it (server prefers id over name)
|
|
784
|
+
// /resume by it (server prefers id over name): not by the wire tool name.
|
|
785
785
|
const { session, resumeSpy } = makeSession({
|
|
786
786
|
executeReturn: { content: [{ type: "text", text: "added" }] },
|
|
787
787
|
});
|
|
@@ -812,7 +812,7 @@ describe("AgentWidgetSession — WebMCP resolve", () => {
|
|
|
812
812
|
});
|
|
813
813
|
});
|
|
814
814
|
|
|
815
|
-
describe("AgentWidgetSession
|
|
815
|
+
describe("AgentWidgetSession: WebMCP parallel batched resume (core#3878)", () => {
|
|
816
816
|
beforeEach(() => {
|
|
817
817
|
vi.clearAllMocks();
|
|
818
818
|
});
|
|
@@ -853,8 +853,7 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
853
853
|
message: msg,
|
|
854
854
|
});
|
|
855
855
|
|
|
856
|
-
// The batch flushes only when the stream that delivered the awaits ENDS
|
|
857
|
-
// the client emits `status: idle` at stream end. Simulate that.
|
|
856
|
+
// The batch flushes only when the stream that delivered the awaits ENDS: // the client emits `status: idle` at stream end. Simulate that.
|
|
858
857
|
const endStream = (session: AgentWidgetSession) =>
|
|
859
858
|
(session as unknown as { handleEvent: (e: unknown) => void }).handleEvent({
|
|
860
859
|
type: "status",
|
|
@@ -893,7 +892,7 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
893
892
|
// Both page tools ran.
|
|
894
893
|
expect(executeSpy).toHaveBeenCalledTimes(2);
|
|
895
894
|
|
|
896
|
-
// Exactly ONE /resume for the shared execution
|
|
895
|
+
// Exactly ONE /resume for the shared execution: not one per tool.
|
|
897
896
|
expect(resumeSpy).toHaveBeenCalledTimes(1);
|
|
898
897
|
const [execId, toolOutputs] = resumeSpy.mock.calls[0]! as unknown as [
|
|
899
898
|
string,
|
|
@@ -906,7 +905,7 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
906
905
|
expect(toolOutputs["toolu_B"].content[0].text).toBe("added SHOE-007");
|
|
907
906
|
});
|
|
908
907
|
|
|
909
|
-
it("executes siblings concurrently
|
|
908
|
+
it("executes siblings concurrently: one call's gate Promise does not block the other", async () => {
|
|
910
909
|
// The native approval bubble parks each call's execute on a Promise. A
|
|
911
910
|
// gated sibling must not head-of-line-block the others: both executes
|
|
912
911
|
// should be in flight before either completes.
|
|
@@ -1078,7 +1077,7 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
1078
1077
|
it("a UI-only updateConfig preserves the client, bridge, and in-flight WebMCP state", async () => {
|
|
1079
1078
|
// Self-styling widget: a `webmcp:*` theme tool mutates config and re-renders
|
|
1080
1079
|
// mid-turn. updateConfig must NOT swap the client when only display fields
|
|
1081
|
-
// (theme/copy/…) change
|
|
1080
|
+
// (theme/copy/…) change: doing so would abort the very turn that's
|
|
1082
1081
|
// restyling the widget and strand the paused execution. (Inverse of the
|
|
1083
1082
|
// teardown test above.)
|
|
1084
1083
|
const { session } = makeSession();
|
|
@@ -1128,7 +1127,7 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
1128
1127
|
feed(session, parallelAwait("toolu_A", "SHOE-001"));
|
|
1129
1128
|
feed(session, parallelAwait("toolu_B", "SHOE-007"));
|
|
1130
1129
|
// Teardown clears the buffered batch (and bumps the epoch) BEFORE the
|
|
1131
|
-
// stream-end flush
|
|
1130
|
+
// stream-end flush: even if a late idle arrives, nothing should resolve.
|
|
1132
1131
|
session.clearMessages();
|
|
1133
1132
|
endStream(session);
|
|
1134
1133
|
await flushMicrotasks();
|
|
@@ -1160,7 +1159,7 @@ describe("AgentWidgetSession — WebMCP parallel batched resume (core#3878)", ()
|
|
|
1160
1159
|
// The parked confirm Promise resolves false (declined) and the map clears.
|
|
1161
1160
|
await expect(pending).resolves.toBe(false);
|
|
1162
1161
|
expect(s.webMcpApprovalResolvers.size).toBe(0);
|
|
1163
|
-
// The bubble must not be left visually "pending"
|
|
1162
|
+
// The bubble must not be left visually "pending": it flips to denied so no
|
|
1164
1163
|
// stale Approve/Deny remains clickable.
|
|
1165
1164
|
const bubble = s.messages.find((m) => m.variant === "approval");
|
|
1166
1165
|
expect(bubble?.approval?.status).toBe("denied");
|
package/src/smart-dom-reader.ts
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
* Adapts `@mcp-b/smart-dom-reader` into Persona's enriched page-context pipeline and
|
|
5
5
|
* exposes a ready-made {@link AgentWidgetContextProvider} you can drop into
|
|
6
6
|
* `config.contextProviders`. This is the ONLY module that imports the smart-dom-reader
|
|
7
|
-
* runtime value, so the library never reaches the main bundle or the IIFE/CDN build
|
|
8
|
-
* importing this subpath is opt-in.
|
|
7
|
+
* runtime value, so the library never reaches the main bundle or the IIFE/CDN build: * importing this subpath is opt-in.
|
|
9
8
|
*
|
|
10
9
|
* The library is **vendored** under `src/vendor/smart-dom-reader/` (it is mis-published
|
|
11
|
-
* on npm and cannot be imported by name
|
|
10
|
+
* on npm and cannot be imported by name: see that directory's README). Vendoring it
|
|
12
11
|
* here means consumers need no extra install: the code is bundled into this entry, and
|
|
13
12
|
* consumers who never import this subpath pay nothing.
|
|
14
13
|
*
|
|
@@ -65,7 +64,7 @@ export interface SmartDomContextOptions extends SmartDomAdapterOptions {
|
|
|
65
64
|
/** Document to extract from. Default: the global `document`. Ignored when `root` is set. */
|
|
66
65
|
document?: Document;
|
|
67
66
|
/**
|
|
68
|
-
* Scope extraction to this element's subtree instead of the whole document
|
|
67
|
+
* Scope extraction to this element's subtree instead of the whole document: parity
|
|
69
68
|
* with `collectEnrichedPageContext`'s `root`. Useful to read only a main-content region
|
|
70
69
|
* and skip site chrome (nav, sidebars). Shadow DOM inside the subtree is still pierced.
|
|
71
70
|
* When set, `document` is ignored.
|