@runtypelabs/persona 4.22.0 → 4.23.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
- package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +136 -6
- package/dist/animations-extra.d.ts +136 -6
- package/dist/approval-ui.d.cts +148 -14
- package/dist/approval-ui.d.ts +148 -14
- package/dist/artifacts-ui.d.cts +148 -14
- package/dist/artifacts-ui.d.ts +148 -14
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +148 -14
- package/dist/context-mentions.d.ts +148 -14
- package/dist/event-stream-view-GL3ADRY2.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +148 -14
- package/dist/event-stream-view.d.ts +148 -14
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +161 -23
- package/dist/forms-ui.d.ts +161 -23
- package/dist/history-view.d.cts +136 -6
- package/dist/history-view.d.ts +136 -6
- package/dist/index.cjs +67 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -27
- package/dist/index.d.ts +172 -27
- package/dist/index.global.js +51 -51
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +67 -67
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +148 -14
- package/dist/session-reconnect.d.ts +148 -14
- package/dist/smart-dom-reader.d.cts +148 -14
- package/dist/smart-dom-reader.d.ts +148 -14
- package/dist/testing.cjs +3 -3
- package/dist/testing.d.cts +3 -4
- package/dist/testing.d.ts +3 -4
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +71 -71
- package/dist/theme-editor-preview.d.cts +154 -14
- package/dist/theme-editor-preview.d.ts +154 -14
- package/dist/theme-editor-preview.js +48 -48
- package/dist/theme-editor.d.cts +148 -14
- package/dist/theme-editor.d.ts +148 -14
- package/dist/voice-runtime.d.cts +6 -2
- package/dist/voice-runtime.d.ts +6 -2
- package/package.json +1 -1
- package/src/ask-user-question-tool.ts +1 -1
- package/src/client-history-rest.test.ts +2 -1
- package/src/client-visitor-history.test.ts +106 -4
- package/src/client.composer-options.test.ts +1 -1
- package/src/client.interrupt.test.ts +2 -2
- package/src/client.test.ts +477 -915
- package/src/client.ts +134 -494
- package/src/client.unified-tools.test.ts +76 -0
- package/src/components/event-stream-badge-theming.test.ts +3 -0
- package/src/components/event-stream-view.test.ts +76 -76
- package/src/components/event-stream-view.ts +5 -0
- package/src/components/tool-bubble.test.ts +12 -0
- package/src/components/tool-bubble.ts +13 -2
- package/src/event-stream-view-bundle.test.ts +2 -2
- package/src/generated/runtype-openapi-contract.ts +129 -1
- package/src/internal/runtype-history-provider.test.ts +2 -2
- package/src/launcher-global.ts +1 -1
- package/src/session-history.test.ts +1 -1
- package/src/session.quote.test.ts +1 -1
- package/src/session.resubmit-from.test.ts +1 -1
- package/src/session.test.ts +5 -5
- package/src/session.ts +95 -36
- package/src/suggest-replies-tool.ts +1 -1
- package/src/testing/mock-stream.test.ts +37 -16
- package/src/testing/mock-stream.ts +14 -9
- package/src/types.ts +19 -14
- package/src/ui.ask-user-question-plugin.test.ts +7 -7
- package/src/ui.composer-defer-one.test.ts +6 -34
- package/src/ui.composer-draft-persistence.test.ts +1 -1
- package/src/ui.composer-quote.test.ts +1 -1
- package/src/ui.message-history-actions.test.ts +1 -1
- package/src/ui.streaming-coalescing.test.ts +10 -13
- package/src/ui.ts +80 -32
- package/src/ui.typing-indicator.test.ts +216 -0
- package/src/ui.voice-custom-provider.test.ts +211 -0
- package/src/utils/event-stream-buffer.test.ts +13 -13
- package/src/utils/event-stream-capture.test.ts +20 -20
- package/src/utils/throughput-tracker.test.ts +100 -142
- package/src/utils/throughput-tracker.ts +18 -132
- package/src/utils/voice-support.ts +18 -1
- package/src/voice/voice-turn-tracker.ts +18 -0
- package/src/webmcp-bridge.ts +1 -1
- package/dist/event-stream-view-FKWDY3UB.js +0 -1
- package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
package/src/types.ts
CHANGED
|
@@ -991,15 +991,15 @@ export type AgentMessageMetadata = {
|
|
|
991
991
|
*/
|
|
992
992
|
parentStepId?: string;
|
|
993
993
|
/**
|
|
994
|
-
* Set to `true` on a tool-variant message produced from
|
|
994
|
+
* Set to `true` on a tool-variant message produced from an `await`
|
|
995
995
|
* event (`awaitReason: "local_tool_required"`). Signals to UI code that
|
|
996
996
|
* the tool call is a LOCAL tool and the server is paused waiting for a
|
|
997
997
|
* `POST /v1/dispatch/resume` with the user's answer keyed by tool name.
|
|
998
998
|
*/
|
|
999
999
|
awaitingLocalTool?: boolean;
|
|
1000
1000
|
/**
|
|
1001
|
-
* The provider per-call id (`toolu_…`) carried on the `
|
|
1002
|
-
*
|
|
1001
|
+
* The provider per-call id (`toolu_…`) carried on the `await`
|
|
1002
|
+
* event for a LOCAL tool (core#3878). Present only when the
|
|
1003
1003
|
* server emits it. Two PARALLEL calls to the same tool in one turn share a
|
|
1004
1004
|
* `toolName` (and a collapsed `toolId`) but get DISTINCT `webMcpToolCallId`s,
|
|
1005
1005
|
* so this is the key the widget batches a single `/resume` on: preferred
|
|
@@ -3418,7 +3418,7 @@ export type EventStreamConfig = {
|
|
|
3418
3418
|
/**
|
|
3419
3419
|
* Custom badge color mappings by event type prefix or exact type.
|
|
3420
3420
|
* Keys are matched as exact match first, then prefix match (keys ending with "_").
|
|
3421
|
-
* @example { "
|
|
3421
|
+
* @example { "execution_": { bg: "#dcfce7", text: "#166534" }, "error": { bg: "#fecaca", text: "#991b1b" } }
|
|
3422
3422
|
*/
|
|
3423
3423
|
badgeColors?: Record<string, EventStreamBadgeColor>;
|
|
3424
3424
|
/**
|
|
@@ -4983,10 +4983,12 @@ export interface VoiceProvider {
|
|
|
4983
4983
|
* `isFinal=false` is a live interim update (user partials, or assistant deltas
|
|
4984
4984
|
* on providers that stream them); `isFinal=true` finalizes that role's text.
|
|
4985
4985
|
* On the realtime `runtype` path, interim updates fire for the `user` only and
|
|
4986
|
-
* the `assistant` arrives as a single final.
|
|
4986
|
+
* the `assistant` arrives as a single final. Providers with overlapping turns
|
|
4987
|
+
* must include the same turnId on user and assistant finals; without IDs,
|
|
4988
|
+
* providers must discard cancelled output before emitting the next user final.
|
|
4987
4989
|
*/
|
|
4988
4990
|
onTranscript?(
|
|
4989
|
-
callback: (role: 'user' | 'assistant', text: string, isFinal: boolean) => void,
|
|
4991
|
+
callback: (role: 'user' | 'assistant', text: string, isFinal: boolean, metadata?: { turnId?: string }) => void,
|
|
4990
4992
|
): void;
|
|
4991
4993
|
|
|
4992
4994
|
/** Register a callback for per-turn latency metrics (realtime path). */
|
|
@@ -7561,16 +7563,16 @@ export type AgentWidgetConfig = {
|
|
|
7561
7563
|
*
|
|
7562
7564
|
* @example
|
|
7563
7565
|
* ```typescript
|
|
7564
|
-
* // For Runtype
|
|
7566
|
+
* // For a custom backend (Runtype unified events are handled natively)
|
|
7565
7567
|
* config: {
|
|
7566
7568
|
* parseSSEEvent: (data) => {
|
|
7567
|
-
* if (
|
|
7568
|
-
* return { text: data.
|
|
7569
|
+
* if (data.type === 'token' && typeof data.value === 'string') {
|
|
7570
|
+
* return { text: data.value };
|
|
7569
7571
|
* }
|
|
7570
|
-
* if (data.type === '
|
|
7572
|
+
* if (data.type === 'done') {
|
|
7571
7573
|
* return { done: true };
|
|
7572
7574
|
* }
|
|
7573
|
-
* if (data.type === '
|
|
7575
|
+
* if (data.type === 'failure') {
|
|
7574
7576
|
* return { error: data.error };
|
|
7575
7577
|
* }
|
|
7576
7578
|
* return null; // Ignore other events
|
|
@@ -7787,7 +7789,7 @@ export type AgentWidgetReasoning = {
|
|
|
7787
7789
|
/**
|
|
7788
7790
|
* Reasoning channel scope (wire spec). `"turn"` is ordinary per-turn
|
|
7789
7791
|
* thinking; `"loop"` is a cross-iteration agent reflection (the fold that
|
|
7790
|
-
* replaced the legacy `agent_reflection` event).
|
|
7792
|
+
* replaced the legacy `agent_reflection` event). Optional when the stream does not specify a scope.
|
|
7791
7793
|
*/
|
|
7792
7794
|
scope?: "turn" | "loop";
|
|
7793
7795
|
startedAt?: number;
|
|
@@ -7802,6 +7804,9 @@ export type AgentWidgetToolCall = {
|
|
|
7802
7804
|
args?: unknown;
|
|
7803
7805
|
chunks?: string[];
|
|
7804
7806
|
result?: unknown;
|
|
7807
|
+
/** Whether the tool succeeded; failure does not necessarily end the execution. */
|
|
7808
|
+
success?: boolean;
|
|
7809
|
+
error?: string;
|
|
7805
7810
|
duration?: number;
|
|
7806
7811
|
startedAt?: number;
|
|
7807
7812
|
completedAt?: number;
|
|
@@ -7835,7 +7840,7 @@ export type AgentWidgetMessageVariant = "assistant" | "reasoning" | "tool" | "ap
|
|
|
7835
7840
|
|
|
7836
7841
|
/**
|
|
7837
7842
|
* Per-turn / per-step stop reason emitted by the runtime on
|
|
7838
|
-
* `
|
|
7843
|
+
* `turn_complete` and `step_complete` SSE events. The vocabulary is
|
|
7839
7844
|
* owned by the upstream Runtype API: do not extend without coordination.
|
|
7840
7845
|
*
|
|
7841
7846
|
* - `end_turn`: natural completion (no affordance needed)
|
|
@@ -7967,7 +7972,7 @@ export type AgentWidgetMessage = {
|
|
|
7967
7972
|
*/
|
|
7968
7973
|
agentMetadata?: AgentMessageMetadata;
|
|
7969
7974
|
/**
|
|
7970
|
-
* Per-turn stop reason reported by the runtime on `
|
|
7975
|
+
* Per-turn stop reason reported by the runtime on `turn_complete`
|
|
7971
7976
|
* (agent-loop path) or the last `step_complete` for a prompt step
|
|
7972
7977
|
* (dispatch / flow path). Absent when the API did not report a value.
|
|
7973
7978
|
*
|
|
@@ -174,7 +174,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
174
174
|
ok: true,
|
|
175
175
|
body: new ReadableStream({
|
|
176
176
|
start(c) {
|
|
177
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
177
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
178
178
|
c.close();
|
|
179
179
|
},
|
|
180
180
|
}),
|
|
@@ -217,7 +217,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
217
217
|
ok: true,
|
|
218
218
|
body: new ReadableStream({
|
|
219
219
|
start(c) {
|
|
220
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
220
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
221
221
|
c.close();
|
|
222
222
|
},
|
|
223
223
|
}),
|
|
@@ -284,7 +284,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
284
284
|
ok: true,
|
|
285
285
|
body: new ReadableStream({
|
|
286
286
|
start(c) {
|
|
287
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
287
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
288
288
|
c.close();
|
|
289
289
|
},
|
|
290
290
|
}),
|
|
@@ -366,7 +366,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
366
366
|
ok: true,
|
|
367
367
|
body: new ReadableStream({
|
|
368
368
|
start(c) {
|
|
369
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
369
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
370
370
|
c.close();
|
|
371
371
|
},
|
|
372
372
|
}),
|
|
@@ -434,7 +434,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
434
434
|
ok: true,
|
|
435
435
|
body: new ReadableStream({
|
|
436
436
|
start(c) {
|
|
437
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
437
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
438
438
|
c.close();
|
|
439
439
|
},
|
|
440
440
|
}),
|
|
@@ -631,7 +631,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
631
631
|
ok: true,
|
|
632
632
|
body: new ReadableStream({
|
|
633
633
|
start(c) {
|
|
634
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
634
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
635
635
|
c.close();
|
|
636
636
|
},
|
|
637
637
|
}),
|
|
@@ -645,7 +645,7 @@ describe("renderAskUserQuestion plugin hook", () => {
|
|
|
645
645
|
ok: true,
|
|
646
646
|
body: new ReadableStream({
|
|
647
647
|
start(c) {
|
|
648
|
-
c.enqueue(new TextEncoder().encode('data: {"type":"
|
|
648
|
+
c.enqueue(new TextEncoder().encode('data: {"type":"execution_complete","success":true}\n\n'));
|
|
649
649
|
c.close();
|
|
650
650
|
},
|
|
651
651
|
}),
|
|
@@ -3,42 +3,14 @@
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
|
|
5
5
|
import { createAgentExperience } from "./ui";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const legacyEvent = (type: string, data: Record<string, unknown>) =>
|
|
9
|
-
`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
|
|
6
|
+
import { buildAssistantTurnFrames } from "./testing/mock-stream";
|
|
10
7
|
|
|
11
8
|
/** One complete assistant turn, in the same wire shape the widget expects. */
|
|
12
|
-
const unifiedFrames = (): string =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
write(legacyEvent("flow_start", { flowId: "flow_1", flowName: "T", totalSteps: 1 }));
|
|
18
|
-
write(
|
|
19
|
-
legacyEvent("step_start", {
|
|
20
|
-
id: "step_1",
|
|
21
|
-
name: "Prompt",
|
|
22
|
-
stepType: "prompt",
|
|
23
|
-
index: 0,
|
|
24
|
-
totalSteps: 1,
|
|
25
|
-
})
|
|
26
|
-
);
|
|
27
|
-
write(legacyEvent("text_start", { messageId: "message_1" }));
|
|
28
|
-
write(legacyEvent("step_delta", { id: "step_1", text: "answer" }));
|
|
29
|
-
write(legacyEvent("text_end", { messageId: "message_1" }));
|
|
30
|
-
write(
|
|
31
|
-
legacyEvent("step_complete", {
|
|
32
|
-
id: "step_1",
|
|
33
|
-
name: "Prompt",
|
|
34
|
-
stepType: "prompt",
|
|
35
|
-
success: true,
|
|
36
|
-
result: { response: "answer" },
|
|
37
|
-
})
|
|
38
|
-
);
|
|
39
|
-
write(legacyEvent("flow_complete", { flowId: "flow_1", success: true }));
|
|
40
|
-
return out;
|
|
41
|
-
};
|
|
9
|
+
const unifiedFrames = (): string => [
|
|
10
|
+
{ type: "execution_start", executionId: "exec-1", kind: "agent" },
|
|
11
|
+
...buildAssistantTurnFrames({ executionId: "exec-1", text: "answer" }),
|
|
12
|
+
{ type: "execution_complete", executionId: "exec-1", kind: "agent", success: true },
|
|
13
|
+
].map((frame) => `data: ${JSON.stringify(frame)}\n\n`).join("");
|
|
42
14
|
|
|
43
15
|
const mounts: HTMLElement[] = [];
|
|
44
16
|
const controllers: ReturnType<typeof createAgentExperience>[] = [];
|
|
@@ -17,7 +17,7 @@ const streamResponse = () => {
|
|
|
17
17
|
body: new ReadableStream({
|
|
18
18
|
start(controller) {
|
|
19
19
|
controller.enqueue(
|
|
20
|
-
encoder.encode('data: {"type":"
|
|
20
|
+
encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
|
|
21
21
|
);
|
|
22
22
|
controller.close();
|
|
23
23
|
},
|
|
@@ -17,7 +17,7 @@ const streamResponse = () => {
|
|
|
17
17
|
body: new ReadableStream({
|
|
18
18
|
start(controller) {
|
|
19
19
|
controller.enqueue(
|
|
20
|
-
encoder.encode('data: {"type":"
|
|
20
|
+
encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
|
|
21
21
|
);
|
|
22
22
|
controller.close();
|
|
23
23
|
},
|
|
@@ -17,7 +17,7 @@ const streamResponse = () => {
|
|
|
17
17
|
body: new ReadableStream({
|
|
18
18
|
start(controller) {
|
|
19
19
|
controller.enqueue(
|
|
20
|
-
encoder.encode('data: {"type":"
|
|
20
|
+
encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
|
|
21
21
|
);
|
|
22
22
|
controller.close();
|
|
23
23
|
},
|
|
@@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
4
4
|
|
|
5
5
|
import { createAgentExperience } from "./ui";
|
|
6
6
|
import type { AgentWidgetStoredState } from "./types";
|
|
7
|
-
import { createUnifiedEventWrite } from "./utils/__fixtures__/unified-translator.oracle";
|
|
8
7
|
|
|
9
8
|
// The approval UI ships in a lazy chunk; provide it eagerly so this file's
|
|
10
9
|
// synchronous render assertions hold. Per-file module isolation keeps the
|
|
@@ -48,7 +47,7 @@ const flushMicrotasks = async (times = 20) => {
|
|
|
48
47
|
}
|
|
49
48
|
};
|
|
50
49
|
|
|
51
|
-
const
|
|
50
|
+
const sseEvent = (type: string, data: Record<string, unknown>) =>
|
|
52
51
|
`event: ${type}\ndata: ${JSON.stringify({ type, ...data })}\n\n`;
|
|
53
52
|
|
|
54
53
|
const createStreamHarness = () => {
|
|
@@ -59,9 +58,7 @@ const createStreamHarness = () => {
|
|
|
59
58
|
new Response(new ReadableStream<Uint8Array>({
|
|
60
59
|
start(streamController) {
|
|
61
60
|
controller = streamController;
|
|
62
|
-
write =
|
|
63
|
-
streamController.enqueue(encoder.encode(chunk));
|
|
64
|
-
});
|
|
61
|
+
write = (chunk) => streamController.enqueue(encoder.encode(chunk));
|
|
65
62
|
},
|
|
66
63
|
}))
|
|
67
64
|
);
|
|
@@ -69,7 +66,7 @@ const createStreamHarness = () => {
|
|
|
69
66
|
fetchMock,
|
|
70
67
|
send(type: string, data: Record<string, unknown>) {
|
|
71
68
|
if (!write) throw new Error("stream not started");
|
|
72
|
-
write(
|
|
69
|
+
write(sseEvent(type, data));
|
|
73
70
|
},
|
|
74
71
|
close() {
|
|
75
72
|
controller?.close();
|
|
@@ -85,7 +82,7 @@ const startTurn = async (mount: HTMLElement) => {
|
|
|
85
82
|
};
|
|
86
83
|
|
|
87
84
|
const startText = (stream: ReturnType<typeof createStreamHarness>, deltas: string[]) => {
|
|
88
|
-
stream.send("
|
|
85
|
+
stream.send("execution_start", { kind: "flow", executionId: "exec-1", flowId: "flow_1", flowName: "Test", totalSteps: 1 });
|
|
89
86
|
stream.send("step_start", {
|
|
90
87
|
id: "step_1",
|
|
91
88
|
name: "Prompt",
|
|
@@ -93,12 +90,12 @@ const startText = (stream: ReturnType<typeof createStreamHarness>, deltas: strin
|
|
|
93
90
|
index: 0,
|
|
94
91
|
totalSteps: 1,
|
|
95
92
|
});
|
|
96
|
-
stream.send("text_start", {
|
|
97
|
-
deltas.forEach((text) => stream.send("
|
|
93
|
+
stream.send("text_start", { id: "message_1" });
|
|
94
|
+
deltas.forEach((text) => stream.send("text_delta", { id: "message_1", delta: text }));
|
|
98
95
|
};
|
|
99
96
|
|
|
100
97
|
const finishText = (stream: ReturnType<typeof createStreamHarness>) => {
|
|
101
|
-
stream.send("
|
|
98
|
+
stream.send("text_complete", { id: "message_1" });
|
|
102
99
|
stream.send("step_complete", {
|
|
103
100
|
id: "step_1",
|
|
104
101
|
name: "Prompt",
|
|
@@ -106,7 +103,7 @@ const finishText = (stream: ReturnType<typeof createStreamHarness>) => {
|
|
|
106
103
|
success: true,
|
|
107
104
|
result: { response: "Hello world" },
|
|
108
105
|
});
|
|
109
|
-
stream.send("
|
|
106
|
+
stream.send("execution_complete", { kind: "flow", executionId: "exec-1", flowId: "flow_1", success: true });
|
|
110
107
|
stream.close();
|
|
111
108
|
};
|
|
112
109
|
|
|
@@ -145,8 +142,8 @@ describe("pure streaming text update coalescing", () => {
|
|
|
145
142
|
startText(stream, ["Hel"]);
|
|
146
143
|
await flushMicrotasks();
|
|
147
144
|
const afterFirstDelta = saves.length;
|
|
148
|
-
stream.send("
|
|
149
|
-
stream.send("
|
|
145
|
+
stream.send("text_delta", { id: "message_1", delta: "lo" });
|
|
146
|
+
stream.send("text_delta", { id: "message_1", delta: " world" });
|
|
150
147
|
await flushMicrotasks();
|
|
151
148
|
|
|
152
149
|
expect(saves).toHaveLength(afterFirstDelta);
|
package/src/ui.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usesSessionVoice } from "./utils/voice-support";
|
|
1
2
|
import { escapeHtml, createMarkdownProcessorFromConfig } from "./postprocessors";
|
|
2
3
|
import { resolveSanitizer } from "./utils/sanitize";
|
|
3
4
|
import { stabilizeStreamingTables } from "./utils/streaming-table";
|
|
@@ -730,6 +731,12 @@ type Controller = {
|
|
|
730
731
|
* Required host logout wiring.
|
|
731
732
|
*/
|
|
732
733
|
resetHistoryIdentity: () => Promise<{ remoteRevocationConfirmed: boolean }>;
|
|
734
|
+
/**
|
|
735
|
+
* Read the current browser visitor credential for a trusted voice integration.
|
|
736
|
+
* Returns null before minting, outside client-token mode, or after teardown.
|
|
737
|
+
* Never put this bearer credential in logs, URLs, or model context.
|
|
738
|
+
*/
|
|
739
|
+
getVisitorToken: () => Promise<string | null>;
|
|
733
740
|
/** Latest sanitized identity state; carries no token, proof, or identity id. */
|
|
734
741
|
getHistoryIdentityStatus: () => HistoryIdentityStatus;
|
|
735
742
|
/** `focus: false` keeps focus where it is (programmatic/preview opens). */
|
|
@@ -3272,7 +3279,7 @@ export const createAgentExperience = (
|
|
|
3272
3279
|
|
|
3273
3280
|
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
3274
3281
|
|
|
3275
|
-
// Branch: LOCAL-tool pause (
|
|
3282
|
+
// Branch: LOCAL-tool pause (await) resumes via /resume with structured
|
|
3276
3283
|
// toolOutputs; legacy path sends as a plain user message.
|
|
3277
3284
|
const sourceMessage = sessionRef.current
|
|
3278
3285
|
.getMessages()
|
|
@@ -5245,7 +5252,7 @@ export const createAgentExperience = (
|
|
|
5245
5252
|
setTimeout(() => {
|
|
5246
5253
|
if (!voiceState.active) {
|
|
5247
5254
|
voiceState.manuallyDeactivated = false;
|
|
5248
|
-
if (config.voiceRecognition?.provider
|
|
5255
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
5249
5256
|
session.toggleVoice().then(() => {
|
|
5250
5257
|
voiceState.active = session.isVoiceActive();
|
|
5251
5258
|
emitVoiceState("restore");
|
|
@@ -6319,20 +6326,23 @@ export const createAgentExperience = (
|
|
|
6319
6326
|
// reasoning removal, no threshold variant.
|
|
6320
6327
|
const dropCompletedToolCalls =
|
|
6321
6328
|
config.features?.toolCallDisplay?.completedVisibility === "removed";
|
|
6322
|
-
|
|
6323
|
-
|
|
6329
|
+
/** A finished reasoning/tool row that `completedVisibility` removes from the transcript. */
|
|
6330
|
+
const isDroppedCompletedRow = (message: AgentWidgetMessage): boolean => {
|
|
6324
6331
|
if (
|
|
6325
6332
|
message.variant === "reasoning" &&
|
|
6326
|
-
message.reasoning?.status === "complete"
|
|
6327
|
-
dropReasoningRow(message.reasoning)
|
|
6333
|
+
message.reasoning?.status === "complete"
|
|
6328
6334
|
) {
|
|
6329
|
-
return;
|
|
6335
|
+
return dropReasoningRow(message.reasoning);
|
|
6330
6336
|
}
|
|
6331
|
-
|
|
6337
|
+
return (
|
|
6332
6338
|
dropCompletedToolCalls &&
|
|
6333
6339
|
message.variant === "tool" &&
|
|
6334
6340
|
message.toolCall?.status === "complete"
|
|
6335
|
-
)
|
|
6341
|
+
);
|
|
6342
|
+
};
|
|
6343
|
+
|
|
6344
|
+
messages.forEach((message) => {
|
|
6345
|
+
if (isDroppedCompletedRow(message)) {
|
|
6336
6346
|
return;
|
|
6337
6347
|
}
|
|
6338
6348
|
activeMessageIds.add(message.id);
|
|
@@ -6408,11 +6418,11 @@ export const createAgentExperience = (
|
|
|
6408
6418
|
if (cachedWrapper) {
|
|
6409
6419
|
tempContainer.appendChild(cachedWrapper.cloneNode(true));
|
|
6410
6420
|
// Keep the overlay sheet alive only while the server is actively
|
|
6411
|
-
// waiting on the user (awaitingLocalTool === true). Before
|
|
6421
|
+
// waiting on the user (awaitingLocalTool === true). Before await
|
|
6412
6422
|
// fires, or after the answer resumes the flow, omit from
|
|
6413
6423
|
// liveAskToolIds so the prune loop below removes any stale DOM sheet.
|
|
6414
6424
|
// Guards against lingering skeleton sheets from tool_start events
|
|
6415
|
-
// that never get a matching
|
|
6425
|
+
// that never get a matching await (e.g. LLM-hallucinated trailing
|
|
6416
6426
|
// ask_user_question calls at end-of-turn).
|
|
6417
6427
|
if (
|
|
6418
6428
|
isAskUserQuestionMessage(message) &&
|
|
@@ -7002,16 +7012,40 @@ export const createAgentExperience = (
|
|
|
7002
7012
|
// Add standalone typing indicator only if streaming but no assistant message is streaming yet
|
|
7003
7013
|
// (This shows while waiting for the stream to start)
|
|
7004
7014
|
// Check for ANY streaming assistant message, even if empty (to avoid duplicate bubbles)
|
|
7015
|
+
//
|
|
7016
|
+
// Tool and reasoning rows hidden by `features.showToolCalls: false` /
|
|
7017
|
+
// `features.showReasoning: false`, or removed once complete by their
|
|
7018
|
+
// `completedVisibility` setting, never render, so they must not count as
|
|
7019
|
+
// "the assistant is already visibly responding": otherwise the transcript
|
|
7020
|
+
// goes blank for the whole tool phase. When those rows are shown, their own
|
|
7021
|
+
// bubble carries the loading animation and the standalone dots stay hidden.
|
|
7022
|
+
const isHiddenActivityRow = (msg: AgentWidgetMessage): boolean =>
|
|
7023
|
+
(msg.variant === "tool" && !showToolCalls) ||
|
|
7024
|
+
(msg.variant === "reasoning" && !showReasoning) ||
|
|
7025
|
+
isDroppedCompletedRow(msg);
|
|
7005
7026
|
const hasStreamingAssistantMessage = messages.some(
|
|
7006
|
-
(msg) => msg.role === "assistant" && msg.streaming
|
|
7027
|
+
(msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
|
|
7007
7028
|
);
|
|
7008
7029
|
|
|
7009
7030
|
// Also check if there's a recently completed assistant message (streaming just ended)
|
|
7010
7031
|
// This prevents flicker when the message completes but isStreaming hasn't updated yet
|
|
7011
7032
|
// Approval-variant messages are UI controls, not content: exclude them so the typing
|
|
7012
|
-
// indicator still shows while the agent resumes after approval
|
|
7013
|
-
|
|
7014
|
-
|
|
7033
|
+
// indicator still shows while the agent resumes after approval. Hidden tool/reasoning
|
|
7034
|
+
// rows are skipped for the same reason (the gap between a tool finishing and the
|
|
7035
|
+
// model's next text would otherwise show nothing), and a hidden row *after* the
|
|
7036
|
+
// last visible message means the agent has moved on to more work, so that visible
|
|
7037
|
+
// message is not the turn's final response.
|
|
7038
|
+
let lastVisibleIndex = messages.length - 1;
|
|
7039
|
+
while (lastVisibleIndex >= 0 && isHiddenActivityRow(messages[lastVisibleIndex])) {
|
|
7040
|
+
lastVisibleIndex--;
|
|
7041
|
+
}
|
|
7042
|
+
const lastMessage = lastVisibleIndex >= 0 ? messages[lastVisibleIndex] : undefined;
|
|
7043
|
+
const hasHiddenActivityAfterLastVisible = lastVisibleIndex < messages.length - 1;
|
|
7044
|
+
const hasRecentAssistantResponse =
|
|
7045
|
+
lastMessage?.role === "assistant" &&
|
|
7046
|
+
!lastMessage.streaming &&
|
|
7047
|
+
lastMessage.variant !== "approval" &&
|
|
7048
|
+
!hasHiddenActivityAfterLastVisible;
|
|
7015
7049
|
|
|
7016
7050
|
if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
|
|
7017
7051
|
// Get loading indicator using priority chain: plugin -> config -> default
|
|
@@ -7925,6 +7959,10 @@ export const createAgentExperience = (
|
|
|
7925
7959
|
};
|
|
7926
7960
|
|
|
7927
7961
|
const isComposerInputDisabled = (): boolean => composerLock.inputDisabled;
|
|
7962
|
+
const isActiveSessionVoice = (): boolean =>
|
|
7963
|
+
usesSessionVoice(config.voiceRecognition?.provider) &&
|
|
7964
|
+
!!session && (session.isBargeInActive() ||
|
|
7965
|
+
["listening", "processing", "speaking"].includes(session.getVoiceStatus()));
|
|
7928
7966
|
/** Any submission path is blocked; stop is not a submission. */
|
|
7929
7967
|
const isComposerSendBlocked = (): boolean =>
|
|
7930
7968
|
composerLock.inputDisabled || composerLock.sendDisabled;
|
|
@@ -8033,7 +8071,7 @@ export const createAgentExperience = (
|
|
|
8033
8071
|
button.disabled = locked;
|
|
8034
8072
|
});
|
|
8035
8073
|
}
|
|
8036
|
-
if (micButton) micButton.disabled =
|
|
8074
|
+
if (micButton) micButton.disabled = locked || (streaming && !isActiveSessionVoice());
|
|
8037
8075
|
|
|
8038
8076
|
suggestionManagers.forEach((manager) => {
|
|
8039
8077
|
manager.elements.forEach((element) => {
|
|
@@ -8860,7 +8898,7 @@ export const createAgentExperience = (
|
|
|
8860
8898
|
// per-state UI (e.g. a listening/speaking status dock). Fires for every
|
|
8861
8899
|
// provider; the mic-button styling below is runtype-specific.
|
|
8862
8900
|
eventBus.emit("voice:status", { status, timestamp: Date.now() });
|
|
8863
|
-
if (config.voiceRecognition?.provider
|
|
8901
|
+
if (status !== 'disconnected' && !usesSessionVoice(config.voiceRecognition?.provider)) return;
|
|
8864
8902
|
|
|
8865
8903
|
switch (status) {
|
|
8866
8904
|
case 'listening':
|
|
@@ -8950,6 +8988,7 @@ export const createAgentExperience = (
|
|
|
8950
8988
|
destroyCallbacks.push(() => {
|
|
8951
8989
|
session.destroy();
|
|
8952
8990
|
visitorStore?.destroy();
|
|
8991
|
+
visitorStore = null;
|
|
8953
8992
|
});
|
|
8954
8993
|
|
|
8955
8994
|
// Mirror read-aloud playback state into the action buttons, and surface it as
|
|
@@ -8983,7 +9022,7 @@ export const createAgentExperience = (
|
|
|
8983
9022
|
scrollSendSeeded = true;
|
|
8984
9023
|
|
|
8985
9024
|
// Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
|
|
8986
|
-
if (config.voiceRecognition?.
|
|
9025
|
+
if (config.voiceRecognition?.enabled === true && usesSessionVoice(config.voiceRecognition.provider)) {
|
|
8987
9026
|
try {
|
|
8988
9027
|
session.setupVoice();
|
|
8989
9028
|
} catch (err) {
|
|
@@ -12622,7 +12661,7 @@ export const createAgentExperience = (
|
|
|
12622
12661
|
// Dictation is composition: a locked input takes none of it.
|
|
12623
12662
|
if (isComposerInputDisabled()) return;
|
|
12624
12663
|
// Runtype provider: use session.toggleVoice() (WebSocket-based STT)
|
|
12625
|
-
if (config.voiceRecognition?.provider
|
|
12664
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
12626
12665
|
const voiceStatus = session.getVoiceStatus();
|
|
12627
12666
|
const interruptionMode = session.getVoiceInterruptionMode();
|
|
12628
12667
|
|
|
@@ -12693,7 +12732,7 @@ export const createAgentExperience = (
|
|
|
12693
12732
|
// The click listener itself is registered by `wireComposerSurface`, so a
|
|
12694
12733
|
// composer rebuild unwires it with the rest of the surface.
|
|
12695
12734
|
destroyCallbacks.push(() => {
|
|
12696
|
-
if (config.voiceRecognition?.provider
|
|
12735
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
12697
12736
|
if (session.isVoiceActive()) session.toggleVoice();
|
|
12698
12737
|
removeRuntypeMicStateStyles();
|
|
12699
12738
|
} else {
|
|
@@ -12722,7 +12761,7 @@ export const createAgentExperience = (
|
|
|
12722
12761
|
}
|
|
12723
12762
|
setTimeout(() => {
|
|
12724
12763
|
if (!voiceState.active && !voiceState.manuallyDeactivated) {
|
|
12725
|
-
if (config.voiceRecognition?.provider
|
|
12764
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
12726
12765
|
session.toggleVoice().then(() => {
|
|
12727
12766
|
voiceState.active = session.isVoiceActive();
|
|
12728
12767
|
emitVoiceState("auto");
|
|
@@ -14308,9 +14347,9 @@ export const createAgentExperience = (
|
|
|
14308
14347
|
typeof window !== 'undefined' &&
|
|
14309
14348
|
(typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
|
|
14310
14349
|
typeof (window as any).SpeechRecognition !== 'undefined');
|
|
14311
|
-
const
|
|
14312
|
-
config.voiceRecognition?.provider
|
|
14313
|
-
const hasVoiceInput = hasSpeechRecognition ||
|
|
14350
|
+
const hasSessionProvider =
|
|
14351
|
+
usesSessionVoice(config.voiceRecognition?.provider);
|
|
14352
|
+
const hasVoiceInput = hasSpeechRecognition || hasSessionProvider;
|
|
14314
14353
|
|
|
14315
14354
|
if (voiceRecognitionEnabled && hasVoiceInput) {
|
|
14316
14355
|
// Create or update mic button
|
|
@@ -14343,7 +14382,7 @@ export const createAgentExperience = (
|
|
|
14343
14382
|
);
|
|
14344
14383
|
|
|
14345
14384
|
// Set disabled state
|
|
14346
|
-
micButton.disabled = session.isStreaming();
|
|
14385
|
+
micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
|
|
14347
14386
|
}
|
|
14348
14387
|
} else {
|
|
14349
14388
|
// Update existing mic button with new config
|
|
@@ -14439,14 +14478,14 @@ export const createAgentExperience = (
|
|
|
14439
14478
|
|
|
14440
14479
|
// Show and update disabled state
|
|
14441
14480
|
micButtonWrapper.style.display = "";
|
|
14442
|
-
micButton.disabled = session.isStreaming();
|
|
14481
|
+
micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
|
|
14443
14482
|
}
|
|
14444
14483
|
} else {
|
|
14445
14484
|
// Hide mic button
|
|
14446
14485
|
if (micButton && micButtonWrapper) {
|
|
14447
14486
|
micButtonWrapper.style.display = "none";
|
|
14448
14487
|
// Stop any active recording if disabling
|
|
14449
|
-
if (config.voiceRecognition?.provider
|
|
14488
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
14450
14489
|
if (session.isVoiceActive()) session.toggleVoice();
|
|
14451
14490
|
} else if (isRecording) {
|
|
14452
14491
|
stopVoiceRecognition();
|
|
@@ -15015,8 +15054,8 @@ export const createAgentExperience = (
|
|
|
15015
15054
|
},
|
|
15016
15055
|
startVoiceRecognition(): boolean {
|
|
15017
15056
|
if (session.isStreaming()) return false;
|
|
15018
|
-
if (config.voiceRecognition?.provider
|
|
15019
|
-
if (session.isVoiceActive()) return true;
|
|
15057
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
15058
|
+
if (session.isVoiceActive() || session.isBargeInActive()) return true;
|
|
15020
15059
|
if (!open && isPanelToggleable()) setOpenState(true, "system");
|
|
15021
15060
|
voiceState.manuallyDeactivated = false;
|
|
15022
15061
|
persistVoiceMetadata();
|
|
@@ -15037,9 +15076,12 @@ export const createAgentExperience = (
|
|
|
15037
15076
|
return true;
|
|
15038
15077
|
},
|
|
15039
15078
|
stopVoiceRecognition(): boolean {
|
|
15040
|
-
if (config.voiceRecognition?.provider
|
|
15041
|
-
if (!session.isVoiceActive()) return false;
|
|
15042
|
-
session.
|
|
15079
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
15080
|
+
if (!session.isVoiceActive() && !session.isBargeInActive()) return false;
|
|
15081
|
+
const stop = session.isBargeInActive()
|
|
15082
|
+
? session.deactivateBargeIn()
|
|
15083
|
+
: session.toggleVoice();
|
|
15084
|
+
stop.then(() => {
|
|
15043
15085
|
voiceState.active = false;
|
|
15044
15086
|
voiceState.manuallyDeactivated = true;
|
|
15045
15087
|
persistVoiceMetadata();
|
|
@@ -15180,6 +15222,12 @@ export const createAgentExperience = (
|
|
|
15180
15222
|
resetHistoryIdentity(): Promise<{ remoteRevocationConfirmed: boolean }> {
|
|
15181
15223
|
return resetHistoryIdentity();
|
|
15182
15224
|
},
|
|
15225
|
+
async getVisitorToken(): Promise<string | null> {
|
|
15226
|
+
const store = visitorStore;
|
|
15227
|
+
if (!store) return null;
|
|
15228
|
+
const token = await store.get();
|
|
15229
|
+
return store === visitorStore ? token : null;
|
|
15230
|
+
},
|
|
15183
15231
|
getHistoryIdentityStatus(): HistoryIdentityStatus {
|
|
15184
15232
|
if (!historyProvider) {
|
|
15185
15233
|
return historyFeatureEnabled()
|