@runtypelabs/persona 4.29.0 → 4.29.2
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-B2HCRERn.js +9 -0
- package/dist/chunk-Bm2-bemv.js +1 -0
- package/dist/chunk-BvhTpG6k.js +1 -0
- package/dist/{chunk-DjOvgFsn.js → chunk-Car1mORT.js} +1 -1
- package/dist/chunk-D5B629Mj.js +1 -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-DrpuvY5W.js +1 -0
- package/dist/{chunk-dRBlrLd7.js → chunk-DtmtfIkd.js} +1 -1
- 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-D3sB8_P_.d.cts → composer-input-C3Xh1Qzj.d.cts} +1 -1
- package/dist/{composer-input-D3sB8_P_.d.ts → composer-input-C3Xh1Qzj.d.ts} +1 -1
- package/dist/{context-mention-manager-D2qxE5op.d.cts → context-mention-manager-BsEiGObk.d.cts} +1 -1
- package/dist/{context-mention-manager-D2qxE5op.d.ts → context-mention-manager-BsEiGObk.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-Dsw66XqE.d.cts → defaults-DN-L67ak.d.cts} +1 -1
- package/dist/{defaults-Dsw66XqE.d.ts → defaults-DN-L67ak.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-DJpJcWAk.d.cts → fallback-speech-engine-Cph8FTux.d.cts} +1 -1
- package/dist/{fallback-speech-engine-DJpJcWAk.d.ts → fallback-speech-engine-Cph8FTux.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 +56 -56
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.global.js +2 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +42 -42
- package/dist/index.js.map +1 -1
- package/dist/{markdown-parsers-loader-CCY7T4Up.d.cts → markdown-parsers-loader-C4Xze-Jw.d.cts} +1 -1
- package/dist/{markdown-parsers-loader-CCY7T4Up.d.ts → markdown-parsers-loader-C4Xze-Jw.d.ts} +1 -1
- package/dist/{preview-DKKpfTo7.d.cts → preview-m9QlqsZi.d.cts} +2 -2
- package/dist/{preview-DKKpfTo7.d.ts → preview-m9QlqsZi.d.ts} +2 -2
- package/dist/{session-DleMr9dZ.d.ts → session-BtvvmWnw.d.cts} +21 -9
- package/dist/{session-DleMr9dZ.d.cts → session-BtvvmWnw.d.ts} +21 -9
- package/dist/session-actions.js +1 -1
- 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 +62 -62
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +41 -41
- 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-oAZ1VReS.d.cts → types-DL-oBHE1.d.cts} +41 -3
- package/dist/{types-oAZ1VReS.d.ts → types-DL-oBHE1.d.ts} +41 -3
- package/dist/voice-client-tools.d.cts +1 -1
- package/dist/voice-client-tools.d.ts +1 -1
- package/dist/voice-runtime.cjs +3 -3
- package/dist/voice-runtime.d.cts +10 -4
- package/dist/voice-runtime.d.ts +10 -4
- 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-Deg0NfmB.d.cts → webmcp-runtime-entry-T6SU_TlL.d.cts} +1 -1
- package/dist/{webmcp-runtime-entry-Deg0NfmB.d.ts → webmcp-runtime-entry-T6SU_TlL.d.ts} +1 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -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 +24 -4
- 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 +7 -1
- 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 +23 -0
- package/src/session.ts +211 -46
- package/src/session.voice.test.ts +169 -0
- package/src/styles/widget.css +4 -2
- package/src/types.ts +37 -2
- package/src/ui.restored-browser-tools.test.ts +128 -0
- package/src/ui.scroll.test.ts +7 -2
- package/src/ui.stream-replay.test.ts +86 -0
- package/src/ui.streaming-coalescing.test.ts +183 -0
- package/src/ui.ts +152 -124
- package/src/ui.typing-indicator.test.ts +21 -0
- 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/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +18 -5
- package/src/utils/morph.ts +10 -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 +13 -8
- package/src/voice/session-voice-wiring.ts +1 -2
- package/src/voice/voice-delegation.ts +10 -3
- package/src/voice/voice.test.ts +38 -2
- package/dist/chunk-B6zZsT6F.js +0 -1
- package/dist/chunk-CIVsNcrk.js +0 -1
- package/dist/chunk-CKdlNyzM.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/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
|
|
|
@@ -3078,8 +3131,14 @@ export class AgentWidgetSession {
|
|
|
3078
3131
|
}
|
|
3079
3132
|
if (info.signal?.aborted) return Promise.resolve(false);
|
|
3080
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
|
+
));
|
|
3081
3140
|
const approval: AgentWidgetApproval = {
|
|
3082
|
-
id:
|
|
3141
|
+
id: approvalId,
|
|
3083
3142
|
status: "pending",
|
|
3084
3143
|
agentId: "",
|
|
3085
3144
|
executionId: "",
|
|
@@ -3666,7 +3725,7 @@ export class AgentWidgetSession {
|
|
|
3666
3725
|
this.webMcpResolvedKeys.clear();
|
|
3667
3726
|
this.messages = this.#sortMessages(
|
|
3668
3727
|
messages.map((message) => ({
|
|
3669
|
-
...message,
|
|
3728
|
+
...restoreBrowserToolState(message),
|
|
3670
3729
|
streaming: false,
|
|
3671
3730
|
sequence: message.sequence ?? this.#nextSequence()
|
|
3672
3731
|
}))
|
|
@@ -3773,7 +3832,9 @@ export class AgentWidgetSession {
|
|
|
3773
3832
|
resolveFollowUpsFeature(this.config).enabled));
|
|
3774
3833
|
if (
|
|
3775
3834
|
event.message.agentMetadata?.awaitingLocalTool === true &&
|
|
3776
|
-
autoResolvable
|
|
3835
|
+
autoResolvable &&
|
|
3836
|
+
this.messages.find(message => message.id === event.message.id)
|
|
3837
|
+
?.agentMetadata?.awaitingLocalTool === true
|
|
3777
3838
|
) {
|
|
3778
3839
|
// Collect the await into its executionId's batch instead of resolving
|
|
3779
3840
|
// it on the spot. Parallel same-tool calls (core#3878) arrive as
|
|
@@ -4029,6 +4090,7 @@ export class AgentWidgetSession {
|
|
|
4029
4090
|
seedContent,
|
|
4030
4091
|
}),
|
|
4031
4092
|
appendMessage: (message) => session.#appendMessage(message),
|
|
4093
|
+
settleParked: (executionId) => session.#settleParkedReconnect(executionId),
|
|
4032
4094
|
nextSequence: () => session.#nextSequence(),
|
|
4033
4095
|
emitReconnect: (event) => session.callbacks.onReconnect?.(event),
|
|
4034
4096
|
buildErrorContent: (message) =>
|
|
@@ -4060,7 +4122,28 @@ export class AgentWidgetSession {
|
|
|
4060
4122
|
if (typeof this.config.reconnectStream !== "function") return;
|
|
4061
4123
|
if (this.#reconnecting) return;
|
|
4062
4124
|
|
|
4063
|
-
|
|
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();
|
|
4064
4147
|
if (!assistantId) {
|
|
4065
4148
|
assistantId = generateAssistantMessageId();
|
|
4066
4149
|
this.#appendMessage({
|
|
@@ -4073,16 +4156,7 @@ export class AgentWidgetSession {
|
|
|
4073
4156
|
});
|
|
4074
4157
|
}
|
|
4075
4158
|
this.#activeAssistantMessageId = assistantId;
|
|
4076
|
-
|
|
4077
|
-
this.agentExecution = {
|
|
4078
|
-
executionId: resume.executionId,
|
|
4079
|
-
agentId: "",
|
|
4080
|
-
agentName: "",
|
|
4081
|
-
status: "running",
|
|
4082
|
-
currentIteration: 0,
|
|
4083
|
-
maxTurns: 0,
|
|
4084
|
-
};
|
|
4085
|
-
}
|
|
4159
|
+
this.#adoptBootExecution(resume.executionId);
|
|
4086
4160
|
this.#resumable = {
|
|
4087
4161
|
executionId: resume.executionId,
|
|
4088
4162
|
lastEventId: resume.after,
|
|
@@ -4092,6 +4166,63 @@ export class AgentWidgetSession {
|
|
|
4092
4166
|
this.#beginReconnect();
|
|
4093
4167
|
}
|
|
4094
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
|
+
|
|
4095
4226
|
/**
|
|
4096
4227
|
* Reopen the trailing assistant bubble (the persisted partial) so replayed
|
|
4097
4228
|
* deltas append to it. Returns its id, or null if the last turn doesn't end
|
|
@@ -4321,7 +4452,13 @@ export class AgentWidgetSession {
|
|
|
4321
4452
|
|
|
4322
4453
|
#appendMessage(message: AgentWidgetMessage) {
|
|
4323
4454
|
const withSequence = this.#ensureSequence(message);
|
|
4324
|
-
|
|
4455
|
+
const last = this.messages[this.messages.length - 1];
|
|
4456
|
+
// Streamed messages almost always arrive in order. Re-sorting the whole
|
|
4457
|
+
// transcript on every append makes a long tool run O(N²) in date parses.
|
|
4458
|
+
this.messages =
|
|
4459
|
+
!last || this.#compareMessages(last, withSequence) <= 0
|
|
4460
|
+
? [...this.messages, withSequence]
|
|
4461
|
+
: this.#sortMessages([...this.messages, withSequence]);
|
|
4325
4462
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
4326
4463
|
}
|
|
4327
4464
|
|
|
@@ -4335,6 +4472,21 @@ export class AgentWidgetSession {
|
|
|
4335
4472
|
|
|
4336
4473
|
this.messages = this.messages.map((existing, idx) => {
|
|
4337
4474
|
if (idx !== index) return existing;
|
|
4475
|
+
// An unnamed skeleton precedes tool_start/await on a fresh parser. Keep
|
|
4476
|
+
// history until the populated event identifies the continuation. Also
|
|
4477
|
+
// retain actual terminal browser outcomes across replayed starts/deltas:
|
|
4478
|
+
// those events must never turn a completed mutation back into an await.
|
|
4479
|
+
if (
|
|
4480
|
+
existing.toolCall?.name && isWebMcpToolName(existing.toolCall.name) &&
|
|
4481
|
+
existing.toolCall.status === "complete" &&
|
|
4482
|
+
withSequence.toolCall?.id === existing.toolCall.id &&
|
|
4483
|
+
withSequence.toolCall.status !== "complete" &&
|
|
4484
|
+
((withSequence.toolCall.status === "pending" && !withSequence.toolCall.name) ||
|
|
4485
|
+
(!existing.toolCall.interrupted &&
|
|
4486
|
+
(existing.toolCall.result !== undefined || existing.toolCall.success !== undefined) &&
|
|
4487
|
+
(!withSequence.agentMetadata?.executionId ||
|
|
4488
|
+
withSequence.agentMetadata.executionId === existing.agentMetadata?.executionId)))
|
|
4489
|
+
) return existing;
|
|
4338
4490
|
const merged = { ...existing, ...withSequence };
|
|
4339
4491
|
// Preserve `ask_user_question` answered state across re-emissions.
|
|
4340
4492
|
// Top-level merge would otherwise replace `agentMetadata` wholesale: // post-resume events (e.g. `tool_complete` re-emitted from a stale
|
|
@@ -4441,7 +4593,8 @@ export class AgentWidgetSession {
|
|
|
4441
4593
|
existing.toolCall?.id === reTcId &&
|
|
4442
4594
|
existingToolName !== undefined &&
|
|
4443
4595
|
isAutoResolvedLocalToolName(existingToolName) &&
|
|
4444
|
-
existing.toolCall?.status === "complete"
|
|
4596
|
+
existing.toolCall?.status === "complete" &&
|
|
4597
|
+
!existing.toolCall.interrupted;
|
|
4445
4598
|
if (isInflight || isResolved || hasCompletedTool) {
|
|
4446
4599
|
merged.agentMetadata = {
|
|
4447
4600
|
...(merged.agentMetadata ?? {}),
|
|
@@ -4457,7 +4610,17 @@ export class AgentWidgetSession {
|
|
|
4457
4610
|
}
|
|
4458
4611
|
return merged;
|
|
4459
4612
|
});
|
|
4460
|
-
|
|
4613
|
+
// The transcript is kept sorted, so only the merged entry can be out of
|
|
4614
|
+
// place; skip the full re-sort when it still sits between its neighbors.
|
|
4615
|
+
const updated = this.messages[index];
|
|
4616
|
+
const before = this.messages[index - 1];
|
|
4617
|
+
const after = this.messages[index + 1];
|
|
4618
|
+
if (
|
|
4619
|
+
(before && this.#compareMessages(before, updated) > 0) ||
|
|
4620
|
+
(after && this.#compareMessages(updated, after) > 0)
|
|
4621
|
+
) {
|
|
4622
|
+
this.messages = this.#sortMessages(this.messages);
|
|
4623
|
+
}
|
|
4461
4624
|
this.callbacks.onMessagesChanged([...this.messages]);
|
|
4462
4625
|
}
|
|
4463
4626
|
|
|
@@ -4475,22 +4638,24 @@ export class AgentWidgetSession {
|
|
|
4475
4638
|
return this.#sequenceCounter++;
|
|
4476
4639
|
}
|
|
4477
4640
|
|
|
4478
|
-
#
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
}
|
|
4641
|
+
#compareMessages(a: AgentWidgetMessage, b: AgentWidgetMessage) {
|
|
4642
|
+
// Sort by createdAt timestamp first (chronological order)
|
|
4643
|
+
const timeA = new Date(a.createdAt).getTime();
|
|
4644
|
+
const timeB = new Date(b.createdAt).getTime();
|
|
4645
|
+
if (!Number.isNaN(timeA) && !Number.isNaN(timeB) && timeA !== timeB) {
|
|
4646
|
+
return timeA - timeB;
|
|
4647
|
+
}
|
|
4486
4648
|
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4649
|
+
// Fall back to sequence if timestamps are equal or invalid
|
|
4650
|
+
const seqA = a.sequence ?? 0;
|
|
4651
|
+
const seqB = b.sequence ?? 0;
|
|
4652
|
+
if (seqA !== seqB) return seqA - seqB;
|
|
4491
4653
|
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4654
|
+
// Final fallback to ID
|
|
4655
|
+
return a.id.localeCompare(b.id);
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
#sortMessages(messages: AgentWidgetMessage[]) {
|
|
4659
|
+
return [...messages].sort((a, b) => this.#compareMessages(a, b));
|
|
4495
4660
|
}
|
|
4496
4661
|
}
|
|
@@ -666,6 +666,175 @@ describe('AgentWidgetSession - voice client delegation bridge', () => {
|
|
|
666
666
|
]);
|
|
667
667
|
});
|
|
668
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
|
+
|
|
669
838
|
it('trims GPT-Live\'s leading space from bubbles and the submitted request', async () => {
|
|
670
839
|
h.state.transcriptCb!('user', ' What are your opening hours', true, { turnId: 'u1', caption: true });
|
|
671
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 {
|