@runtypelabs/persona 4.28.2 → 4.29.1
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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.cjs +3 -3
- package/dist/approval-ui.d.cts +1 -1
- package/dist/approval-ui.d.ts +1 -1
- package/dist/approval-ui.js +3 -3
- package/dist/artifacts-ui.d.cts +2 -2
- package/dist/artifacts-ui.d.ts +2 -2
- package/dist/chunk-B1-_pp-Y.js +4 -0
- package/dist/{chunk-DrYAfpvk.js → chunk-BcLIgi1h.js} +1 -1
- package/dist/chunk-BkW8s3fj.js +2 -0
- package/dist/chunk-Bm2-bemv.js +1 -0
- package/dist/chunk-BvhTpG6k.js +1 -0
- package/dist/chunk-Csks3Qw1.js +1 -0
- package/dist/chunk-D5B629Mj.js +1 -0
- package/dist/chunk-DGlFKA0r.js +9 -0
- package/dist/chunk-DO9_WSpy.js +1 -0
- package/dist/chunk-DTDHeUsG.js +1 -0
- package/dist/chunk-DjFpGFGA.js +1 -0
- package/dist/{chunk-dRBlrLd7.js → chunk-DtmtfIkd.js} +1 -1
- package/dist/chunk-lA5CJCgK.js +1 -0
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{composer-input-C39npJaz.d.cts → composer-input-B0P4OtNi.d.cts} +1 -1
- package/dist/{composer-input-C39npJaz.d.ts → composer-input-B0P4OtNi.d.ts} +1 -1
- package/dist/{context-mention-manager-DLCB1l9E.d.cts → context-mention-manager-DmLI_RM6.d.cts} +1 -1
- package/dist/{context-mention-manager-DLCB1l9E.d.ts → context-mention-manager-DmLI_RM6.d.ts} +1 -1
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/{defaults-DiAgWHXy.d.cts → defaults-DVo1wbZs.d.cts} +1 -1
- package/dist/{defaults-DiAgWHXy.d.ts → defaults-DVo1wbZs.d.ts} +1 -1
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-C5_PUZjH.d.cts → fallback-speech-engine-BMnlmKnY.d.cts} +1 -1
- package/dist/{fallback-speech-engine-C5_PUZjH.d.ts → fallback-speech-engine-BMnlmKnY.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/index.cjs +50 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.global.js +2 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +38 -37
- package/dist/index.js.map +1 -1
- package/dist/{markdown-parsers-loader-BBdyIEU7.d.cts → markdown-parsers-loader-CeovcyYf.d.cts} +1 -1
- package/dist/{markdown-parsers-loader-BBdyIEU7.d.ts → markdown-parsers-loader-CeovcyYf.d.ts} +1 -1
- package/dist/{preview-Chjb0bmI.d.cts → preview-Bkb4UrQC.d.cts} +2 -2
- package/dist/{preview-Chjb0bmI.d.ts → preview-Bkb4UrQC.d.ts} +2 -2
- package/dist/session-actions.js +1 -1
- package/dist/{session-a_qIp4xp.d.ts → session-iLyKipoM.d.cts} +28 -10
- package/dist/{session-a_qIp4xp.d.cts → session-iLyKipoM.d.ts} +28 -10
- package/dist/session-reconnect.cjs +1 -1
- package/dist/session-reconnect.d.cts +8 -2
- package/dist/session-reconnect.d.ts +8 -2
- package/dist/session-reconnect.js +1 -1
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +61 -60
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +40 -40
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-reference.d.cts +2 -2
- package/dist/theme-reference.d.ts +2 -2
- package/dist/{types-BdO1jNRm.d.cts → types-BuDirJkQ.d.cts} +200 -23
- package/dist/{types-BdO1jNRm.d.ts → types-BuDirJkQ.d.ts} +200 -23
- package/dist/voice-client-tools.cjs +1 -0
- package/dist/voice-client-tools.d.cts +13 -0
- package/dist/voice-client-tools.d.ts +13 -0
- package/dist/voice-client-tools.js +1 -0
- package/dist/voice-runtime.cjs +3 -3
- package/dist/voice-runtime.d.cts +15 -5
- package/dist/voice-runtime.d.ts +15 -5
- package/dist/voice-runtime.js +5 -5
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-B6bpVWSa.d.ts → webmcp-runtime-entry-CXLjRt1k.d.cts} +26 -5
- package/dist/{webmcp-runtime-entry-B6bpVWSa.d.cts → webmcp-runtime-entry-CXLjRt1k.d.ts} +26 -5
- package/dist/webmcp-runtime.cjs +2 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/webmcp-runtime.js +2 -1
- package/dist/widget.css +1 -1
- package/package.json +6 -1
- package/src/client-approve.test.ts +17 -1
- package/src/client-resume.ts +15 -3
- package/src/client.interrupt.test.ts +21 -0
- package/src/client.ts +61 -4
- package/src/client.voice-tools.test.ts +65 -0
- package/src/components/activity-row.ts +3 -2
- package/src/components/approval-actions.test.ts +8 -0
- package/src/components/approval-actions.ts +6 -2
- package/src/components/approval-bubble.test.ts +14 -0
- package/src/components/approval-bubble.ts +8 -1
- package/src/components/tool-bubble.ts +8 -5
- package/src/generated/runtype-openapi-contract.ts +39 -1
- package/src/index-core.ts +11 -0
- package/src/index-global.ts +6 -0
- package/src/resume-dedupe-local-only.test.ts +273 -0
- package/src/runtime/persist-state.test.ts +86 -0
- package/src/session-actions.ts +20 -6
- package/src/session-reconnect.ts +18 -1
- package/src/session.restored-browser-tools.test.ts +272 -0
- package/src/session.test.ts +26 -0
- package/src/session.ts +185 -31
- package/src/session.voice.test.ts +212 -0
- package/src/styles/widget.css +4 -2
- package/src/types.ts +178 -22
- package/src/ui.restored-browser-tools.test.ts +128 -0
- package/src/ui.ts +32 -18
- package/src/utils/formatting.test.ts +9 -1
- package/src/utils/formatting.ts +4 -0
- package/src/utils/local-only-message.ts +15 -0
- package/src/utils/restored-browser-state.ts +50 -0
- package/src/utils/storage.ts +12 -1
- package/src/utils/voice-prior-tool-results.ts +37 -0
- package/src/voice/runtype-voice-provider.ts +94 -7
- package/src/voice/session-voice-wiring.ts +27 -3
- package/src/voice/voice-client-tools-session.ts +116 -0
- package/src/voice/voice-delegation.ts +19 -3
- package/src/voice/voice.test.ts +396 -0
- package/src/voice-client-tools-bundle.test.ts +37 -0
- package/src/voice-client-tools-loader.ts +24 -0
- package/src/voice-client-tools.ts +9 -0
- package/src/webmcp-bridge.test.ts +152 -1
- package/src/webmcp-bridge.ts +5 -6
- package/src/webmcp-polyfill.integration.test.ts +5 -1
- package/src/webmcp-runtime-entry.ts +151 -26
- package/dist/chunk-B6zZsT6F.js +0 -1
- package/dist/chunk-CztSjKHK.js +0 -9
- package/dist/chunk-DbhxjyOB.js +0 -4
- package/dist/chunk-DsdqLStH.js +0 -1
- package/dist/chunk-GIqbL2Eg.js +0 -1
- package/dist/chunk-HUOUif-P.js +0 -1
- package/dist/chunk-UlcxdHef.js +0 -1
package/src/session.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type SSEEventCallback,
|
|
5
5
|
} from "./client";
|
|
6
6
|
import { isWebMcpToolName } from "./webmcp-bridge";
|
|
7
|
+
import { isBrowserLocalApproval, restoreBrowserToolState } from "./utils/restored-browser-state";
|
|
7
8
|
import type { SessionActionsHost, SessionActionsInternals } from "./session-actions";
|
|
8
9
|
import {
|
|
9
10
|
getSessionActionsSync,
|
|
@@ -42,12 +43,14 @@ import {
|
|
|
42
43
|
generateAssistantMessageId
|
|
43
44
|
} from "./utils/message-id";
|
|
44
45
|
import { IMAGE_ONLY_MESSAGE_FALLBACK_TEXT, createTextPart, hasImages } from "./utils/content";
|
|
46
|
+
import { voicePriorToolRows } from "./utils/voice-prior-tool-results";
|
|
45
47
|
import {
|
|
46
48
|
buildArtifactRefRawContent,
|
|
47
49
|
resolveArtifactDisplayMode
|
|
48
50
|
} from "./utils/artifact-display";
|
|
49
51
|
import type {
|
|
50
52
|
VoiceProvider,
|
|
53
|
+
VoicePriorToolResult,
|
|
51
54
|
VoiceStatus,
|
|
52
55
|
VoiceConfig,
|
|
53
56
|
ReadAloudState,
|
|
@@ -437,7 +440,7 @@ export class AgentWidgetSession {
|
|
|
437
440
|
) {
|
|
438
441
|
this.historyInternals = this.#composeHistoryInternals(historyInternals);
|
|
439
442
|
this.messages = [...(config.initialMessages ?? [])].map((message) => ({
|
|
440
|
-
...message,
|
|
443
|
+
...restoreBrowserToolState(message),
|
|
441
444
|
sequence: message.sequence ?? this.#nextSequence()
|
|
442
445
|
}));
|
|
443
446
|
this.messages = this.#sortMessages(this.messages);
|
|
@@ -1343,7 +1346,8 @@ export class AgentWidgetSession {
|
|
|
1343
1346
|
if (this.#activeConversationId && this.#activeConversationId !== id) {
|
|
1344
1347
|
return { messages: [], nextMessageCursor: page.nextCursor };
|
|
1345
1348
|
}
|
|
1346
|
-
const
|
|
1349
|
+
const restored = this.#restoreHistoryPage(page.messages);
|
|
1350
|
+
const merged = mergeWireMessagesById(this.messages, restored);
|
|
1347
1351
|
this.messages = this.#sortMessages(
|
|
1348
1352
|
merged.map((message) => this.#ensureSequence(message))
|
|
1349
1353
|
);
|
|
@@ -1354,7 +1358,30 @@ export class AgentWidgetSession {
|
|
|
1354
1358
|
this.historyInternals.setStoredMessageCursor?.(page.nextCursor);
|
|
1355
1359
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
1356
1360
|
this.#emitHistoryState();
|
|
1357
|
-
return { messages:
|
|
1361
|
+
return { messages: restored, nextMessageCursor: page.nextCursor };
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** Normalize incoming history without retiring callbacks owned by this chat. */
|
|
1365
|
+
#restoreHistoryPage(messages: AgentWidgetMessage[]): AgentWidgetMessage[] {
|
|
1366
|
+
const current = new Map(this.messages.map(message => [message.id, message]));
|
|
1367
|
+
return messages.map(message => {
|
|
1368
|
+
const live = current.get(message.id);
|
|
1369
|
+
if (live) {
|
|
1370
|
+
if (this.webMcpApprovalResolvers.has(live.id)) return live;
|
|
1371
|
+
// A decision made while the history fetch was in flight must not be
|
|
1372
|
+
// replaced by the page's stale pending copy of the same browser card.
|
|
1373
|
+
if (live.approval && isBrowserLocalApproval(live.approval) &&
|
|
1374
|
+
live.approval.id === message.approval?.id &&
|
|
1375
|
+
live.approval.status !== "pending" && message.approval?.status === "pending") return live;
|
|
1376
|
+
const executionId = live.agentMetadata?.executionId;
|
|
1377
|
+
const tools = [...(live.toolCall ? [live.toolCall] : []), ...(live.tools ?? [])];
|
|
1378
|
+
if (executionId && tools.some(tool =>
|
|
1379
|
+
this.webMcpInflightKeys.has(`${executionId}:${tool.id}`) ||
|
|
1380
|
+
this.webMcpResolvedKeys.has(`${executionId}:${tool.id}`)
|
|
1381
|
+
)) return live;
|
|
1382
|
+
}
|
|
1383
|
+
return restoreBrowserToolState(message);
|
|
1384
|
+
});
|
|
1358
1385
|
}
|
|
1359
1386
|
|
|
1360
1387
|
/**
|
|
@@ -1800,6 +1827,7 @@ export class AgentWidgetSession {
|
|
|
1800
1827
|
}
|
|
1801
1828
|
|
|
1802
1829
|
const page = await provider.getPage(conversationId, { context });
|
|
1830
|
+
const restored = this.#restoreHistoryPage(page.messages);
|
|
1803
1831
|
const serverIds = new Set(page.messages.map((message) => message.id));
|
|
1804
1832
|
const overlaps = this.messages.some((message) => serverIds.has(message.id));
|
|
1805
1833
|
const pageOldest = page.messages[0]?.createdAt;
|
|
@@ -1807,7 +1835,7 @@ export class AgentWidgetSession {
|
|
|
1807
1835
|
let reconciled: AgentWidgetMessage[];
|
|
1808
1836
|
if (!overlaps) {
|
|
1809
1837
|
// No shared id: the local transcript is not this record's newest window.
|
|
1810
|
-
reconciled =
|
|
1838
|
+
reconciled = restored.map((message) => this.#ensureSequence(message));
|
|
1811
1839
|
} else {
|
|
1812
1840
|
const durableId = this.#resumable ? this.#activeAssistantMessageId : null;
|
|
1813
1841
|
const survivors = this.messages.filter((message) => {
|
|
@@ -1816,7 +1844,7 @@ export class AgentWidgetSession {
|
|
|
1816
1844
|
// Older than the server window is legitimately absent from this page.
|
|
1817
1845
|
return pageOldest !== undefined && message.createdAt < pageOldest;
|
|
1818
1846
|
});
|
|
1819
|
-
reconciled = mergeWireMessagesById(survivors,
|
|
1847
|
+
reconciled = mergeWireMessagesById(survivors, restored).map(
|
|
1820
1848
|
(message) => this.#ensureSequence(message)
|
|
1821
1849
|
);
|
|
1822
1850
|
}
|
|
@@ -2042,7 +2070,12 @@ export class AgentWidgetSession {
|
|
|
2042
2070
|
controller: AbortController,
|
|
2043
2071
|
assistantMessageId: string,
|
|
2044
2072
|
userMessageId: string,
|
|
2045
|
-
turnOptions?: {
|
|
2073
|
+
turnOptions?: {
|
|
2074
|
+
composerOptions?: ComposerOptionsPayload;
|
|
2075
|
+
interrupt?: boolean;
|
|
2076
|
+
voiceSpoken?: boolean;
|
|
2077
|
+
voicePriorToolResults?: VoicePriorToolResult[];
|
|
2078
|
+
}
|
|
2046
2079
|
): Promise<void> {
|
|
2047
2080
|
this.#recordDispatchedProjections(snapshot);
|
|
2048
2081
|
try {
|
|
@@ -2053,7 +2086,8 @@ export class AgentWidgetSession {
|
|
|
2053
2086
|
assistantMessageId,
|
|
2054
2087
|
composerOptions: turnOptions?.composerOptions,
|
|
2055
2088
|
interrupt: turnOptions?.interrupt,
|
|
2056
|
-
voiceSpoken: turnOptions?.voiceSpoken
|
|
2089
|
+
voiceSpoken: turnOptions?.voiceSpoken,
|
|
2090
|
+
voicePriorToolResults: turnOptions?.voicePriorToolResults
|
|
2057
2091
|
},
|
|
2058
2092
|
this.handleEvent
|
|
2059
2093
|
);
|
|
@@ -2072,6 +2106,7 @@ export class AgentWidgetSession {
|
|
|
2072
2106
|
assistantMessageId,
|
|
2073
2107
|
composerOptions: turnOptions?.composerOptions,
|
|
2074
2108
|
voiceSpoken: turnOptions?.voiceSpoken,
|
|
2109
|
+
voicePriorToolResults: turnOptions?.voicePriorToolResults,
|
|
2075
2110
|
},
|
|
2076
2111
|
this.handleEvent
|
|
2077
2112
|
);
|
|
@@ -2535,9 +2570,11 @@ export class AgentWidgetSession {
|
|
|
2535
2570
|
* Internal (voice client delegation): this is the delegated voice turn.
|
|
2536
2571
|
* `userMessageId` names an existing transcript bubble to submit as the
|
|
2537
2572
|
* turn's user message instead of appending a new one, so the spoken
|
|
2538
|
-
* request appears (and is sent) exactly once.
|
|
2573
|
+
* request appears (and is sent) exactly once. `priorToolResults`: page
|
|
2574
|
+
* tools the voice server already ran for this turn, recorded once as
|
|
2575
|
+
* completed tool rows after the request (see voicePriorToolRows).
|
|
2539
2576
|
*/
|
|
2540
|
-
voiceTurn?: { userMessageId?: string };
|
|
2577
|
+
voiceTurn?: { userMessageId?: string | null; priorToolResults?: unknown };
|
|
2541
2578
|
}
|
|
2542
2579
|
) {
|
|
2543
2580
|
const input = rawInput.trim();
|
|
@@ -2692,12 +2729,25 @@ export class AgentWidgetSession {
|
|
|
2692
2729
|
|
|
2693
2730
|
// A submitted voice bubble may sit above captions or a turn that finished
|
|
2694
2731
|
// while it waited: the request still ends on it.
|
|
2732
|
+
const sentAt = new Date().toISOString();
|
|
2695
2733
|
const snapshot = voiceBubble
|
|
2696
|
-
? [
|
|
2697
|
-
...this.messages.filter((m) => m !== voiceBubble),
|
|
2698
|
-
{ ...voiceBubble, createdAt: new Date().toISOString() }
|
|
2699
|
-
]
|
|
2734
|
+
? [...this.messages.filter((m) => m !== voiceBubble), { ...voiceBubble, createdAt: sentAt }]
|
|
2700
2735
|
: [...this.messages];
|
|
2736
|
+
// Tools the voice server already ran answer this request and are never run
|
|
2737
|
+
// again. Stored at the request's own time, so every later replay keeps them
|
|
2738
|
+
// right after it; this request (moved to `sentAt`) sends them at `sentAt`.
|
|
2739
|
+
const requestAt = (voiceBubble ?? this.messages.find((m) => m.id === userMessageId))?.createdAt ?? sentAt;
|
|
2740
|
+
const priorRows = voicePriorToolRows(options?.voiceTurn?.priorToolResults, requestAt);
|
|
2741
|
+
for (const row of priorRows) {
|
|
2742
|
+
if (this.messages.some((m) => m.id === row.id)) continue;
|
|
2743
|
+
this.#appendMessage(row);
|
|
2744
|
+
snapshot.push({ ...row, createdAt: sentAt });
|
|
2745
|
+
}
|
|
2746
|
+
// Client-token chat replays from the server's transcript: it gets them as a field.
|
|
2747
|
+
const voicePriorToolResults = priorRows.map(({ agentMetadata }) => {
|
|
2748
|
+
const { toolCallId, toolName, args, result } = agentMetadata!.clientToolAnswer!;
|
|
2749
|
+
return { toolCallId, name: toolName, arguments: args as Record<string, unknown>, result };
|
|
2750
|
+
});
|
|
2701
2751
|
|
|
2702
2752
|
try {
|
|
2703
2753
|
await this.#dispatchWithDeletedRecovery(
|
|
@@ -2709,7 +2759,8 @@ export class AgentWidgetSession {
|
|
|
2709
2759
|
composerOptions: options?.composerOptions,
|
|
2710
2760
|
interrupt: options?.interrupt,
|
|
2711
2761
|
// The delegated voice turn's reply is read aloud (spoken-reply hint).
|
|
2712
|
-
voiceSpoken: !!options?.voiceTurn
|
|
2762
|
+
voiceSpoken: !!options?.voiceTurn,
|
|
2763
|
+
...(voicePriorToolResults.length > 0 && { voicePriorToolResults })
|
|
2713
2764
|
}
|
|
2714
2765
|
);
|
|
2715
2766
|
} catch (error) {
|
|
@@ -2740,6 +2791,7 @@ export class AgentWidgetSession {
|
|
|
2740
2791
|
role: "assistant",
|
|
2741
2792
|
createdAt: new Date().toISOString(),
|
|
2742
2793
|
content,
|
|
2794
|
+
localOnly: true,
|
|
2743
2795
|
sequence: this.#nextSequence()
|
|
2744
2796
|
};
|
|
2745
2797
|
|
|
@@ -2928,6 +2980,7 @@ export class AgentWidgetSession {
|
|
|
2928
2980
|
role: "assistant",
|
|
2929
2981
|
createdAt: new Date().toISOString(),
|
|
2930
2982
|
content,
|
|
2983
|
+
localOnly: true,
|
|
2931
2984
|
sequence: this.#nextSequence()
|
|
2932
2985
|
};
|
|
2933
2986
|
|
|
@@ -3076,9 +3129,16 @@ export class AgentWidgetSession {
|
|
|
3076
3129
|
} catch {
|
|
3077
3130
|
// fall through to explicit approval
|
|
3078
3131
|
}
|
|
3079
|
-
|
|
3132
|
+
if (info.signal?.aborted) return Promise.resolve(false);
|
|
3133
|
+
|
|
3134
|
+
let approvalId: string;
|
|
3135
|
+
do {
|
|
3136
|
+
approvalId = `webmcp-${++this.#webMcpApprovalSeq}`;
|
|
3137
|
+
} while (this.messages.some(message =>
|
|
3138
|
+
message.id === `approval-${approvalId}` || message.approval?.id === approvalId
|
|
3139
|
+
));
|
|
3080
3140
|
const approval: AgentWidgetApproval = {
|
|
3081
|
-
id:
|
|
3141
|
+
id: approvalId,
|
|
3082
3142
|
status: "pending",
|
|
3083
3143
|
agentId: "",
|
|
3084
3144
|
executionId: "",
|
|
@@ -3102,6 +3162,12 @@ export class AgentWidgetSession {
|
|
|
3102
3162
|
|
|
3103
3163
|
return new Promise<boolean>((resolve) => {
|
|
3104
3164
|
this.webMcpApprovalResolvers.set(approvalMessageId, resolve);
|
|
3165
|
+
// The call gave up (deadline or cancel): close the card so a late click cannot look like it ran the tool.
|
|
3166
|
+
info.signal?.addEventListener(
|
|
3167
|
+
"abort",
|
|
3168
|
+
() => this.resolveWebMcpApproval(approvalMessageId, "timeout"),
|
|
3169
|
+
{ once: true }
|
|
3170
|
+
);
|
|
3105
3171
|
});
|
|
3106
3172
|
}
|
|
3107
3173
|
|
|
@@ -3113,7 +3179,7 @@ export class AgentWidgetSession {
|
|
|
3113
3179
|
*/
|
|
3114
3180
|
public resolveWebMcpApproval(
|
|
3115
3181
|
approvalMessageId: string,
|
|
3116
|
-
decision: "approved" | "denied"
|
|
3182
|
+
decision: "approved" | "denied" | "timeout"
|
|
3117
3183
|
): void {
|
|
3118
3184
|
const resolve = this.webMcpApprovalResolvers.get(approvalMessageId);
|
|
3119
3185
|
if (!resolve) return;
|
|
@@ -3659,7 +3725,7 @@ export class AgentWidgetSession {
|
|
|
3659
3725
|
this.webMcpResolvedKeys.clear();
|
|
3660
3726
|
this.messages = this.#sortMessages(
|
|
3661
3727
|
messages.map((message) => ({
|
|
3662
|
-
...message,
|
|
3728
|
+
...restoreBrowserToolState(message),
|
|
3663
3729
|
streaming: false,
|
|
3664
3730
|
sequence: message.sequence ?? this.#nextSequence()
|
|
3665
3731
|
}))
|
|
@@ -3766,7 +3832,9 @@ export class AgentWidgetSession {
|
|
|
3766
3832
|
resolveFollowUpsFeature(this.config).enabled));
|
|
3767
3833
|
if (
|
|
3768
3834
|
event.message.agentMetadata?.awaitingLocalTool === true &&
|
|
3769
|
-
autoResolvable
|
|
3835
|
+
autoResolvable &&
|
|
3836
|
+
this.messages.find(message => message.id === event.message.id)
|
|
3837
|
+
?.agentMetadata?.awaitingLocalTool === true
|
|
3770
3838
|
) {
|
|
3771
3839
|
// Collect the await into its executionId's batch instead of resolving
|
|
3772
3840
|
// it on the spot. Parallel same-tool calls (core#3878) arrive as
|
|
@@ -4022,6 +4090,7 @@ export class AgentWidgetSession {
|
|
|
4022
4090
|
seedContent,
|
|
4023
4091
|
}),
|
|
4024
4092
|
appendMessage: (message) => session.#appendMessage(message),
|
|
4093
|
+
settleParked: (executionId) => session.#settleParkedReconnect(executionId),
|
|
4025
4094
|
nextSequence: () => session.#nextSequence(),
|
|
4026
4095
|
emitReconnect: (event) => session.callbacks.onReconnect?.(event),
|
|
4027
4096
|
buildErrorContent: (message) =>
|
|
@@ -4053,7 +4122,28 @@ export class AgentWidgetSession {
|
|
|
4053
4122
|
if (typeof this.config.reconnectStream !== "function") return;
|
|
4054
4123
|
if (this.#reconnecting) return;
|
|
4055
4124
|
|
|
4056
|
-
|
|
4125
|
+
// A run parked on an await the restored transcript can still answer (an
|
|
4126
|
+
// ask_user_question card, a server approval) has no stream to replay, and
|
|
4127
|
+
// reconnecting would spin into a "Connection lost" bubble. Keep the cursor
|
|
4128
|
+
// so the answer's `/resume` attaches after the await frame, as in-session.
|
|
4129
|
+
if (this.#isAwaitPending()) {
|
|
4130
|
+
this.#adoptBootExecution(resume.executionId);
|
|
4131
|
+
this.#resumable = {
|
|
4132
|
+
executionId: resume.executionId,
|
|
4133
|
+
lastEventId: resume.after,
|
|
4134
|
+
assistantMessageId: this.#activeAssistantMessageId ?? "",
|
|
4135
|
+
status: "running",
|
|
4136
|
+
};
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
// Parked on a WebMCP call the reload interrupted: still reconnect once, so
|
|
4141
|
+
// a replayed `await` re-dispatches the call on this page, but don't reopen
|
|
4142
|
+
// the pre-tool text bubble or paint an empty one. The reconnect loop ends
|
|
4143
|
+
// the turn quietly when nothing replays (`#settleParkedReconnect`).
|
|
4144
|
+
let assistantId = this.#hasInterruptedBrowserTool(resume.executionId)
|
|
4145
|
+
? generateAssistantMessageId()
|
|
4146
|
+
: this.#reopenTrailingAssistant();
|
|
4057
4147
|
if (!assistantId) {
|
|
4058
4148
|
assistantId = generateAssistantMessageId();
|
|
4059
4149
|
this.#appendMessage({
|
|
@@ -4066,16 +4156,7 @@ export class AgentWidgetSession {
|
|
|
4066
4156
|
});
|
|
4067
4157
|
}
|
|
4068
4158
|
this.#activeAssistantMessageId = assistantId;
|
|
4069
|
-
|
|
4070
|
-
this.agentExecution = {
|
|
4071
|
-
executionId: resume.executionId,
|
|
4072
|
-
agentId: "",
|
|
4073
|
-
agentName: "",
|
|
4074
|
-
status: "running",
|
|
4075
|
-
currentIteration: 0,
|
|
4076
|
-
maxTurns: 0,
|
|
4077
|
-
};
|
|
4078
|
-
}
|
|
4159
|
+
this.#adoptBootExecution(resume.executionId);
|
|
4079
4160
|
this.#resumable = {
|
|
4080
4161
|
executionId: resume.executionId,
|
|
4081
4162
|
lastEventId: resume.after,
|
|
@@ -4085,6 +4166,63 @@ export class AgentWidgetSession {
|
|
|
4085
4166
|
this.#beginReconnect();
|
|
4086
4167
|
}
|
|
4087
4168
|
|
|
4169
|
+
/**
|
|
4170
|
+
* A reconnect attempt left the run parked rather than dropped: the turn is
|
|
4171
|
+
* waiting on the visitor or a page tool (keep the cursor, as an in-session
|
|
4172
|
+
* await pause does), or on a WebMCP call the reload interrupted (nothing can
|
|
4173
|
+
* answer it, so drop the handle). Either way stop reconnecting without a
|
|
4174
|
+
* failure bubble. Returns false for an ordinary drop, which keeps retrying.
|
|
4175
|
+
*/
|
|
4176
|
+
#settleParkedReconnect(executionId: string): boolean {
|
|
4177
|
+
const pageToolBusy =
|
|
4178
|
+
this.webMcpResolveControllers.size > 0 || this.webMcpAwaitBatches.size > 0;
|
|
4179
|
+
if (!pageToolBusy && !this.#isAwaitPending()) {
|
|
4180
|
+
if (!this.#hasInterruptedBrowserTool(executionId)) return false;
|
|
4181
|
+
this.#clearResumable();
|
|
4182
|
+
}
|
|
4183
|
+
this.#reconnecting = false;
|
|
4184
|
+
if (!pageToolBusy) {
|
|
4185
|
+
let changed = false;
|
|
4186
|
+
for (const msg of this.messages) {
|
|
4187
|
+
if (msg.streaming) {
|
|
4188
|
+
msg.streaming = false;
|
|
4189
|
+
changed = true;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
if (changed) this.callbacks.onMessagesChanged([...this.messages]);
|
|
4193
|
+
this.#setStreaming(false);
|
|
4194
|
+
this.abortController = null;
|
|
4195
|
+
}
|
|
4196
|
+
this.#setStatus("idle");
|
|
4197
|
+
return true;
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
/** Restore the running execution a boot resume refers to, if none is live. */
|
|
4201
|
+
#adoptBootExecution(executionId: string): void {
|
|
4202
|
+
if (this.agentExecution) return;
|
|
4203
|
+
this.agentExecution = {
|
|
4204
|
+
executionId,
|
|
4205
|
+
agentId: "",
|
|
4206
|
+
agentName: "",
|
|
4207
|
+
status: "running",
|
|
4208
|
+
currentIteration: 0,
|
|
4209
|
+
maxTurns: 0,
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
/**
|
|
4214
|
+
* True when restore settled one of this execution's WebMCP calls as
|
|
4215
|
+
* interrupted (see `restoreBrowserToolState`): the run is parked on a
|
|
4216
|
+
* browser callback that no longer exists.
|
|
4217
|
+
*/
|
|
4218
|
+
#hasInterruptedBrowserTool(executionId: string): boolean {
|
|
4219
|
+
return this.messages.some(
|
|
4220
|
+
(m) =>
|
|
4221
|
+
m.agentMetadata?.executionId === executionId &&
|
|
4222
|
+
[m.toolCall, ...(m.tools ?? [])].some((tool) => tool?.interrupted === true)
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4088
4226
|
/**
|
|
4089
4227
|
* Reopen the trailing assistant bubble (the persisted partial) so replayed
|
|
4090
4228
|
* deltas append to it. Returns its id, or null if the last turn doesn't end
|
|
@@ -4328,6 +4466,21 @@ export class AgentWidgetSession {
|
|
|
4328
4466
|
|
|
4329
4467
|
this.messages = this.messages.map((existing, idx) => {
|
|
4330
4468
|
if (idx !== index) return existing;
|
|
4469
|
+
// An unnamed skeleton precedes tool_start/await on a fresh parser. Keep
|
|
4470
|
+
// history until the populated event identifies the continuation. Also
|
|
4471
|
+
// retain actual terminal browser outcomes across replayed starts/deltas:
|
|
4472
|
+
// those events must never turn a completed mutation back into an await.
|
|
4473
|
+
if (
|
|
4474
|
+
existing.toolCall?.name && isWebMcpToolName(existing.toolCall.name) &&
|
|
4475
|
+
existing.toolCall.status === "complete" &&
|
|
4476
|
+
withSequence.toolCall?.id === existing.toolCall.id &&
|
|
4477
|
+
withSequence.toolCall.status !== "complete" &&
|
|
4478
|
+
((withSequence.toolCall.status === "pending" && !withSequence.toolCall.name) ||
|
|
4479
|
+
(!existing.toolCall.interrupted &&
|
|
4480
|
+
(existing.toolCall.result !== undefined || existing.toolCall.success !== undefined) &&
|
|
4481
|
+
(!withSequence.agentMetadata?.executionId ||
|
|
4482
|
+
withSequence.agentMetadata.executionId === existing.agentMetadata?.executionId)))
|
|
4483
|
+
) return existing;
|
|
4331
4484
|
const merged = { ...existing, ...withSequence };
|
|
4332
4485
|
// Preserve `ask_user_question` answered state across re-emissions.
|
|
4333
4486
|
// Top-level merge would otherwise replace `agentMetadata` wholesale: // post-resume events (e.g. `tool_complete` re-emitted from a stale
|
|
@@ -4434,7 +4587,8 @@ export class AgentWidgetSession {
|
|
|
4434
4587
|
existing.toolCall?.id === reTcId &&
|
|
4435
4588
|
existingToolName !== undefined &&
|
|
4436
4589
|
isAutoResolvedLocalToolName(existingToolName) &&
|
|
4437
|
-
existing.toolCall?.status === "complete"
|
|
4590
|
+
existing.toolCall?.status === "complete" &&
|
|
4591
|
+
!existing.toolCall.interrupted;
|
|
4438
4592
|
if (isInflight || isResolved || hasCompletedTool) {
|
|
4439
4593
|
merged.agentMetadata = {
|
|
4440
4594
|
...(merged.agentMetadata ?? {}),
|
|
@@ -623,6 +623,218 @@ describe('AgentWidgetSession - voice client delegation bridge', () => {
|
|
|
623
623
|
]);
|
|
624
624
|
});
|
|
625
625
|
|
|
626
|
+
it('offers no client tools when WebMCP is off', () => {
|
|
627
|
+
expect(h.state.bridge!.clientTools!()).toBeNull();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('records a server-answered (direct) turn: the caption becomes the user turn, the readout an answer', async () => {
|
|
631
|
+
const caption = (role: 'user' | 'assistant', text: string, turnId: string) =>
|
|
632
|
+
h.state.transcriptCb!(role, text, true, { turnId, caption: true });
|
|
633
|
+
caption('user', 'Next slide', 'u1');
|
|
634
|
+
h.state.bridge!.recordDirectTurn!({
|
|
635
|
+
delegationId: 'd1',
|
|
636
|
+
userText: 'Next slide',
|
|
637
|
+
userUtteranceIds: ['u1'],
|
|
638
|
+
assistantText: 'Slide 2: Agenda.',
|
|
639
|
+
});
|
|
640
|
+
expect(view()).toEqual([
|
|
641
|
+
['assistant', 'Welcome! How can I help?'],
|
|
642
|
+
['user', 'Next slide'],
|
|
643
|
+
['assistant', 'Slide 2: Agenda.'],
|
|
644
|
+
]);
|
|
645
|
+
expect(messages.find((m) => m.content === 'Next slide')!.voiceCaption).toBeUndefined();
|
|
646
|
+
expect(spoken('voice-direct-d1')).toBe(true);
|
|
647
|
+
expect(dispatch).not.toHaveBeenCalled();
|
|
648
|
+
|
|
649
|
+
// The next typed turn carries both as conversation.
|
|
650
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Sure.', 'assistant-r2'));
|
|
651
|
+
await session.sendMessage('and the one after?');
|
|
652
|
+
const sent = dispatch.mock.calls[0][0].messages.filter((m) => !m.voiceCaption);
|
|
653
|
+
expect(sent.map((m) => m.content)).toEqual([
|
|
654
|
+
'Welcome! How can I help?',
|
|
655
|
+
'Next slide',
|
|
656
|
+
'Slide 2: Agenda.',
|
|
657
|
+
'and the one after?',
|
|
658
|
+
]);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('records a direct turn with no caption and no readout as just the request', () => {
|
|
662
|
+
h.state.bridge!.recordDirectTurn!({ delegationId: 'd2', userText: 'Next slide', userUtteranceIds: [], assistantText: '' });
|
|
663
|
+
expect(view()).toEqual([
|
|
664
|
+
['assistant', 'Welcome! How can I help?'],
|
|
665
|
+
['user', 'Next slide'],
|
|
666
|
+
]);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
describe('prior tool results on a hand-back', () => {
|
|
670
|
+
const jump = {
|
|
671
|
+
toolCallId: 'vtc_1',
|
|
672
|
+
name: 'webmcp:jump_to_slide',
|
|
673
|
+
arguments: { position: 5 },
|
|
674
|
+
result: { slidePosition: 5, slideTitle: 'Pricing' },
|
|
675
|
+
};
|
|
676
|
+
const toolRows = () => messages.filter((m) => m.variant === 'tool');
|
|
677
|
+
|
|
678
|
+
it('records the executed call once, under the request and before the reply, as a completed call', async () => {
|
|
679
|
+
h.state.transcriptCb!('user', 'Jump to the pricing slide', true, { turnId: 'u1', caption: true });
|
|
680
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'You are on Pricing.'));
|
|
681
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
682
|
+
req({ delegationId: 'd1', userText: 'Jump to the pricing slide', userUtteranceIds: ['u1'], priorToolResults: [jump] }),
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
expect(messages.map((m) => [m.role, m.variant ?? '', m.content])).toEqual([
|
|
686
|
+
['assistant', '', 'Welcome! How can I help?'],
|
|
687
|
+
['user', '', 'Jump to the pricing slide'],
|
|
688
|
+
['assistant', 'tool', ''],
|
|
689
|
+
['assistant', '', 'You are on Pricing.'],
|
|
690
|
+
]);
|
|
691
|
+
const [row] = toolRows();
|
|
692
|
+
expect(row.toolCall).toEqual({
|
|
693
|
+
id: 'vtc_1',
|
|
694
|
+
name: 'webmcp:jump_to_slide',
|
|
695
|
+
status: 'complete',
|
|
696
|
+
args: { position: 5 },
|
|
697
|
+
result: { slidePosition: 5, slideTitle: 'Pricing' },
|
|
698
|
+
});
|
|
699
|
+
expect(row.agentMetadata).toEqual({
|
|
700
|
+
clientToolAnswer: {
|
|
701
|
+
toolCallId: 'vtc_1',
|
|
702
|
+
toolName: 'webmcp:jump_to_slide',
|
|
703
|
+
args: { position: 5 },
|
|
704
|
+
result: { slidePosition: 5, slideTitle: 'Pricing' },
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
// Nothing marks it awaiting, so the WebMCP resolve path never runs it.
|
|
708
|
+
expect(row.agentMetadata?.awaitingLocalTool).toBeUndefined();
|
|
709
|
+
|
|
710
|
+
// The request ends on the user turn followed by the executed call.
|
|
711
|
+
const sent = dispatch.mock.calls[0][0].messages.filter((m) => !m.voiceCaption);
|
|
712
|
+
expect(sent.slice(-2).map((m) => [m.role, m.variant ?? '', m.content])).toEqual([
|
|
713
|
+
['user', '', 'Jump to the pricing slide'],
|
|
714
|
+
['assistant', 'tool', ''],
|
|
715
|
+
]);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('records nothing when the field is absent or holds no well-formed entry', async () => {
|
|
719
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Done.'));
|
|
720
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'next slide' }));
|
|
721
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
722
|
+
req({
|
|
723
|
+
delegationId: 'd2',
|
|
724
|
+
userText: 'next slide',
|
|
725
|
+
priorToolResults: [{ name: 'webmcp:next_slide' }, { toolCallId: 'vtc_2' }, 'junk', null],
|
|
726
|
+
}),
|
|
727
|
+
);
|
|
728
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd3', userText: 'next slide', priorToolResults: 'nope' }));
|
|
729
|
+
expect(toolRows()).toEqual([]);
|
|
730
|
+
expect(dispatch).toHaveBeenCalledTimes(3);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('defaults missing arguments and result, and never duplicates a call already recorded', async () => {
|
|
734
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Done.'));
|
|
735
|
+
const bare = { toolCallId: 'vtc_3', name: 'webmcp:next_slide', arguments: 'bad' };
|
|
736
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'next slide', priorToolResults: [bare] }));
|
|
737
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd1', userText: 'next slide', priorToolResults: [bare] }));
|
|
738
|
+
await session.sendMessage('thanks');
|
|
739
|
+
expect(toolRows()).toHaveLength(1);
|
|
740
|
+
expect(toolRows()[0].toolCall).toMatchObject({ args: {}, result: null });
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it('hands the client-token request the executed calls only when the hand-back carried some', async () => {
|
|
744
|
+
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Done.'));
|
|
745
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
746
|
+
req({ delegationId: 'd1', userText: 'jump', priorToolResults: [jump, 'junk', { ...jump, result: 'repeat' }] }),
|
|
747
|
+
);
|
|
748
|
+
await h.state.bridge!.runDelegatedTurn(req({ delegationId: 'd2', userText: 'next slide' }));
|
|
749
|
+
await session.sendMessage('typed');
|
|
750
|
+
const options = dispatch.mock.calls.map(([o]) => o as { voicePriorToolResults?: unknown });
|
|
751
|
+
expect(options[0].voicePriorToolResults).toEqual([jump]);
|
|
752
|
+
expect(toolRows()).toHaveLength(1);
|
|
753
|
+
expect(options[1].voicePriorToolResults).toBeUndefined();
|
|
754
|
+
expect(options[2].voicePriorToolResults).toBeUndefined();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('keeps the executed call right after its request on later replays, even past a reply that finished first', async () => {
|
|
758
|
+
const bodies: Array<{ messages: Array<Record<string, unknown>> }> = [];
|
|
759
|
+
vi.stubGlobal(
|
|
760
|
+
'fetch',
|
|
761
|
+
vi.fn(async (_url: string, init?: RequestInit) => {
|
|
762
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
763
|
+
return new Response('', { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
|
|
764
|
+
}),
|
|
765
|
+
);
|
|
766
|
+
vi.useFakeTimers({ toFake: ['Date'] });
|
|
767
|
+
try {
|
|
768
|
+
delete (session as unknown as { client: { dispatch?: Dispatch } }).client.dispatch;
|
|
769
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:00.000Z'));
|
|
770
|
+
h.state.transcriptCb!('user', 'Jump to the pricing slide', true, { turnId: 'u1', caption: true });
|
|
771
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:01.000Z'));
|
|
772
|
+
session.injectMessage({ role: 'assistant', content: 'An earlier answer.' });
|
|
773
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:02.000Z'));
|
|
774
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
775
|
+
req({ delegationId: 'd1', userText: 'Jump to the pricing slide', userUtteranceIds: ['u1'], priorToolResults: [jump] }),
|
|
776
|
+
);
|
|
777
|
+
vi.setSystemTime(new Date('2026-10-10T10:00:03.000Z'));
|
|
778
|
+
await session.sendMessage('and the next one?');
|
|
779
|
+
} finally {
|
|
780
|
+
vi.useRealTimers();
|
|
781
|
+
vi.unstubAllGlobals();
|
|
782
|
+
}
|
|
783
|
+
const roles = (body: { messages: Array<Record<string, unknown>> }) =>
|
|
784
|
+
body.messages.map((m) => (m.toolCalls ? 'calls' : m.toolResults ? 'results' : m.content));
|
|
785
|
+
expect(roles(bodies[0])).toEqual([
|
|
786
|
+
'Welcome! How can I help?',
|
|
787
|
+
'An earlier answer.',
|
|
788
|
+
'Jump to the pricing slide',
|
|
789
|
+
'calls',
|
|
790
|
+
'results',
|
|
791
|
+
]);
|
|
792
|
+
expect(roles(bodies[1])).toEqual([
|
|
793
|
+
'Welcome! How can I help?',
|
|
794
|
+
'Jump to the pricing slide',
|
|
795
|
+
'calls',
|
|
796
|
+
'results',
|
|
797
|
+
'An earlier answer.',
|
|
798
|
+
'and the next one?',
|
|
799
|
+
]);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('replays the executed call to the model as a toolCalls/toolResults pair after the request (proxy mode)', async () => {
|
|
803
|
+
const bodies: Array<{ messages: Array<Record<string, unknown>> }> = [];
|
|
804
|
+
const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => {
|
|
805
|
+
bodies.push(JSON.parse(String(init?.body)));
|
|
806
|
+
return new Response('', { status: 200, headers: { 'Content-Type': 'text/event-stream' } });
|
|
807
|
+
});
|
|
808
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
809
|
+
try {
|
|
810
|
+
delete (session as unknown as { client: { dispatch?: Dispatch } }).client.dispatch;
|
|
811
|
+
h.state.transcriptCb!('user', 'Jump to the pricing slide', true, { turnId: 'u1', caption: true });
|
|
812
|
+
await h.state.bridge!.runDelegatedTurn(
|
|
813
|
+
req({ delegationId: 'd1', userText: 'Jump to the pricing slide', userUtteranceIds: ['u1'], priorToolResults: [jump] }),
|
|
814
|
+
);
|
|
815
|
+
} finally {
|
|
816
|
+
vi.unstubAllGlobals();
|
|
817
|
+
}
|
|
818
|
+
expect(bodies).toHaveLength(1);
|
|
819
|
+
expect(bodies[0].messages.map(({ createdAt: _at, ...m }) => m)).toEqual([
|
|
820
|
+
{ role: 'assistant', content: 'Welcome! How can I help?' },
|
|
821
|
+
{ role: 'user', content: 'Jump to the pricing slide' },
|
|
822
|
+
{
|
|
823
|
+
role: 'assistant',
|
|
824
|
+
content: '',
|
|
825
|
+
toolCalls: [{ toolCallId: 'vtc_1', toolName: 'webmcp_jump_to_slide', args: { position: 5 } }],
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
role: 'tool',
|
|
829
|
+
content: '',
|
|
830
|
+
toolResults: [
|
|
831
|
+
{ toolCallId: 'vtc_1', toolName: 'webmcp_jump_to_slide', result: { slidePosition: 5, slideTitle: 'Pricing' } },
|
|
832
|
+
],
|
|
833
|
+
},
|
|
834
|
+
]);
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
626
838
|
it('trims GPT-Live\'s leading space from bubbles and the submitted request', async () => {
|
|
627
839
|
h.state.transcriptCb!('user', ' What are your opening hours', true, { turnId: 'u1', caption: true });
|
|
628
840
|
dispatch.mockImplementation(async (_options, onEvent) => reply(onEvent, 'Nine to five.'));
|
package/src/styles/widget.css
CHANGED
|
@@ -4750,14 +4750,16 @@
|
|
|
4750
4750
|
|
|
4751
4751
|
.persona-approval-resolved {
|
|
4752
4752
|
display: flex;
|
|
4753
|
-
align-items:
|
|
4753
|
+
align-items: flex-start;
|
|
4754
4754
|
gap: 0.45rem;
|
|
4755
4755
|
margin: 0.5rem 0;
|
|
4756
4756
|
padding: 0.3rem 0.15rem;
|
|
4757
4757
|
font-size: 0.8rem;
|
|
4758
|
+
line-height: 1.4;
|
|
4758
4759
|
color: var(--persona-muted, #6b7280);
|
|
4759
4760
|
}
|
|
4760
|
-
.persona-approval-resolved svg { width: 0.95rem; height: 0.95rem; display: block; flex-shrink: 0; }
|
|
4761
|
+
.persona-approval-resolved svg { width: 0.95rem; height: 0.95rem; display: block; flex-shrink: 0; margin-top: 0.085rem; }
|
|
4762
|
+
.persona-approval-resolved-text { min-width: 0; overflow-wrap: anywhere; }
|
|
4761
4763
|
.persona-approval-resolved-name { font-weight: 600; }
|
|
4762
4764
|
|
|
4763
4765
|
.persona-approval-actions {
|