@runtypelabs/persona 4.22.0 → 4.22.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/{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 +166 -27
- package/dist/index.d.ts +166 -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/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 +148 -14
- package/dist/theme-editor-preview.d.ts +148 -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 +20 -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/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 +67 -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";
|
|
@@ -3272,7 +3273,7 @@ export const createAgentExperience = (
|
|
|
3272
3273
|
|
|
3273
3274
|
removeAskUserQuestionSheet(askUserOverlay, toolCallId);
|
|
3274
3275
|
|
|
3275
|
-
// Branch: LOCAL-tool pause (
|
|
3276
|
+
// Branch: LOCAL-tool pause (await) resumes via /resume with structured
|
|
3276
3277
|
// toolOutputs; legacy path sends as a plain user message.
|
|
3277
3278
|
const sourceMessage = sessionRef.current
|
|
3278
3279
|
.getMessages()
|
|
@@ -5245,7 +5246,7 @@ export const createAgentExperience = (
|
|
|
5245
5246
|
setTimeout(() => {
|
|
5246
5247
|
if (!voiceState.active) {
|
|
5247
5248
|
voiceState.manuallyDeactivated = false;
|
|
5248
|
-
if (config.voiceRecognition?.provider
|
|
5249
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
5249
5250
|
session.toggleVoice().then(() => {
|
|
5250
5251
|
voiceState.active = session.isVoiceActive();
|
|
5251
5252
|
emitVoiceState("restore");
|
|
@@ -6319,20 +6320,23 @@ export const createAgentExperience = (
|
|
|
6319
6320
|
// reasoning removal, no threshold variant.
|
|
6320
6321
|
const dropCompletedToolCalls =
|
|
6321
6322
|
config.features?.toolCallDisplay?.completedVisibility === "removed";
|
|
6322
|
-
|
|
6323
|
-
|
|
6323
|
+
/** A finished reasoning/tool row that `completedVisibility` removes from the transcript. */
|
|
6324
|
+
const isDroppedCompletedRow = (message: AgentWidgetMessage): boolean => {
|
|
6324
6325
|
if (
|
|
6325
6326
|
message.variant === "reasoning" &&
|
|
6326
|
-
message.reasoning?.status === "complete"
|
|
6327
|
-
dropReasoningRow(message.reasoning)
|
|
6327
|
+
message.reasoning?.status === "complete"
|
|
6328
6328
|
) {
|
|
6329
|
-
return;
|
|
6329
|
+
return dropReasoningRow(message.reasoning);
|
|
6330
6330
|
}
|
|
6331
|
-
|
|
6331
|
+
return (
|
|
6332
6332
|
dropCompletedToolCalls &&
|
|
6333
6333
|
message.variant === "tool" &&
|
|
6334
6334
|
message.toolCall?.status === "complete"
|
|
6335
|
-
)
|
|
6335
|
+
);
|
|
6336
|
+
};
|
|
6337
|
+
|
|
6338
|
+
messages.forEach((message) => {
|
|
6339
|
+
if (isDroppedCompletedRow(message)) {
|
|
6336
6340
|
return;
|
|
6337
6341
|
}
|
|
6338
6342
|
activeMessageIds.add(message.id);
|
|
@@ -6408,11 +6412,11 @@ export const createAgentExperience = (
|
|
|
6408
6412
|
if (cachedWrapper) {
|
|
6409
6413
|
tempContainer.appendChild(cachedWrapper.cloneNode(true));
|
|
6410
6414
|
// Keep the overlay sheet alive only while the server is actively
|
|
6411
|
-
// waiting on the user (awaitingLocalTool === true). Before
|
|
6415
|
+
// waiting on the user (awaitingLocalTool === true). Before await
|
|
6412
6416
|
// fires, or after the answer resumes the flow, omit from
|
|
6413
6417
|
// liveAskToolIds so the prune loop below removes any stale DOM sheet.
|
|
6414
6418
|
// Guards against lingering skeleton sheets from tool_start events
|
|
6415
|
-
// that never get a matching
|
|
6419
|
+
// that never get a matching await (e.g. LLM-hallucinated trailing
|
|
6416
6420
|
// ask_user_question calls at end-of-turn).
|
|
6417
6421
|
if (
|
|
6418
6422
|
isAskUserQuestionMessage(message) &&
|
|
@@ -7002,16 +7006,40 @@ export const createAgentExperience = (
|
|
|
7002
7006
|
// Add standalone typing indicator only if streaming but no assistant message is streaming yet
|
|
7003
7007
|
// (This shows while waiting for the stream to start)
|
|
7004
7008
|
// Check for ANY streaming assistant message, even if empty (to avoid duplicate bubbles)
|
|
7009
|
+
//
|
|
7010
|
+
// Tool and reasoning rows hidden by `features.showToolCalls: false` /
|
|
7011
|
+
// `features.showReasoning: false`, or removed once complete by their
|
|
7012
|
+
// `completedVisibility` setting, never render, so they must not count as
|
|
7013
|
+
// "the assistant is already visibly responding": otherwise the transcript
|
|
7014
|
+
// goes blank for the whole tool phase. When those rows are shown, their own
|
|
7015
|
+
// bubble carries the loading animation and the standalone dots stay hidden.
|
|
7016
|
+
const isHiddenActivityRow = (msg: AgentWidgetMessage): boolean =>
|
|
7017
|
+
(msg.variant === "tool" && !showToolCalls) ||
|
|
7018
|
+
(msg.variant === "reasoning" && !showReasoning) ||
|
|
7019
|
+
isDroppedCompletedRow(msg);
|
|
7005
7020
|
const hasStreamingAssistantMessage = messages.some(
|
|
7006
|
-
(msg) => msg.role === "assistant" && msg.streaming
|
|
7021
|
+
(msg) => msg.role === "assistant" && msg.streaming && !isHiddenActivityRow(msg)
|
|
7007
7022
|
);
|
|
7008
7023
|
|
|
7009
7024
|
// Also check if there's a recently completed assistant message (streaming just ended)
|
|
7010
7025
|
// This prevents flicker when the message completes but isStreaming hasn't updated yet
|
|
7011
7026
|
// 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
|
-
|
|
7027
|
+
// indicator still shows while the agent resumes after approval. Hidden tool/reasoning
|
|
7028
|
+
// rows are skipped for the same reason (the gap between a tool finishing and the
|
|
7029
|
+
// model's next text would otherwise show nothing), and a hidden row *after* the
|
|
7030
|
+
// last visible message means the agent has moved on to more work, so that visible
|
|
7031
|
+
// message is not the turn's final response.
|
|
7032
|
+
let lastVisibleIndex = messages.length - 1;
|
|
7033
|
+
while (lastVisibleIndex >= 0 && isHiddenActivityRow(messages[lastVisibleIndex])) {
|
|
7034
|
+
lastVisibleIndex--;
|
|
7035
|
+
}
|
|
7036
|
+
const lastMessage = lastVisibleIndex >= 0 ? messages[lastVisibleIndex] : undefined;
|
|
7037
|
+
const hasHiddenActivityAfterLastVisible = lastVisibleIndex < messages.length - 1;
|
|
7038
|
+
const hasRecentAssistantResponse =
|
|
7039
|
+
lastMessage?.role === "assistant" &&
|
|
7040
|
+
!lastMessage.streaming &&
|
|
7041
|
+
lastMessage.variant !== "approval" &&
|
|
7042
|
+
!hasHiddenActivityAfterLastVisible;
|
|
7015
7043
|
|
|
7016
7044
|
if (isStreaming && messages.some((msg) => msg.role === "user") && !hasStreamingAssistantMessage && !hasRecentAssistantResponse) {
|
|
7017
7045
|
// Get loading indicator using priority chain: plugin -> config -> default
|
|
@@ -7925,6 +7953,10 @@ export const createAgentExperience = (
|
|
|
7925
7953
|
};
|
|
7926
7954
|
|
|
7927
7955
|
const isComposerInputDisabled = (): boolean => composerLock.inputDisabled;
|
|
7956
|
+
const isActiveSessionVoice = (): boolean =>
|
|
7957
|
+
usesSessionVoice(config.voiceRecognition?.provider) &&
|
|
7958
|
+
!!session && (session.isBargeInActive() ||
|
|
7959
|
+
["listening", "processing", "speaking"].includes(session.getVoiceStatus()));
|
|
7928
7960
|
/** Any submission path is blocked; stop is not a submission. */
|
|
7929
7961
|
const isComposerSendBlocked = (): boolean =>
|
|
7930
7962
|
composerLock.inputDisabled || composerLock.sendDisabled;
|
|
@@ -8033,7 +8065,7 @@ export const createAgentExperience = (
|
|
|
8033
8065
|
button.disabled = locked;
|
|
8034
8066
|
});
|
|
8035
8067
|
}
|
|
8036
|
-
if (micButton) micButton.disabled =
|
|
8068
|
+
if (micButton) micButton.disabled = locked || (streaming && !isActiveSessionVoice());
|
|
8037
8069
|
|
|
8038
8070
|
suggestionManagers.forEach((manager) => {
|
|
8039
8071
|
manager.elements.forEach((element) => {
|
|
@@ -8860,7 +8892,7 @@ export const createAgentExperience = (
|
|
|
8860
8892
|
// per-state UI (e.g. a listening/speaking status dock). Fires for every
|
|
8861
8893
|
// provider; the mic-button styling below is runtype-specific.
|
|
8862
8894
|
eventBus.emit("voice:status", { status, timestamp: Date.now() });
|
|
8863
|
-
if (config.voiceRecognition?.provider
|
|
8895
|
+
if (status !== 'disconnected' && !usesSessionVoice(config.voiceRecognition?.provider)) return;
|
|
8864
8896
|
|
|
8865
8897
|
switch (status) {
|
|
8866
8898
|
case 'listening':
|
|
@@ -8983,7 +9015,7 @@ export const createAgentExperience = (
|
|
|
8983
9015
|
scrollSendSeeded = true;
|
|
8984
9016
|
|
|
8985
9017
|
// Setup Runtype voice provider when configured (connects WebSocket for server-side STT)
|
|
8986
|
-
if (config.voiceRecognition?.
|
|
9018
|
+
if (config.voiceRecognition?.enabled === true && usesSessionVoice(config.voiceRecognition.provider)) {
|
|
8987
9019
|
try {
|
|
8988
9020
|
session.setupVoice();
|
|
8989
9021
|
} catch (err) {
|
|
@@ -12622,7 +12654,7 @@ export const createAgentExperience = (
|
|
|
12622
12654
|
// Dictation is composition: a locked input takes none of it.
|
|
12623
12655
|
if (isComposerInputDisabled()) return;
|
|
12624
12656
|
// Runtype provider: use session.toggleVoice() (WebSocket-based STT)
|
|
12625
|
-
if (config.voiceRecognition?.provider
|
|
12657
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
12626
12658
|
const voiceStatus = session.getVoiceStatus();
|
|
12627
12659
|
const interruptionMode = session.getVoiceInterruptionMode();
|
|
12628
12660
|
|
|
@@ -12693,7 +12725,7 @@ export const createAgentExperience = (
|
|
|
12693
12725
|
// The click listener itself is registered by `wireComposerSurface`, so a
|
|
12694
12726
|
// composer rebuild unwires it with the rest of the surface.
|
|
12695
12727
|
destroyCallbacks.push(() => {
|
|
12696
|
-
if (config.voiceRecognition?.provider
|
|
12728
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
12697
12729
|
if (session.isVoiceActive()) session.toggleVoice();
|
|
12698
12730
|
removeRuntypeMicStateStyles();
|
|
12699
12731
|
} else {
|
|
@@ -12722,7 +12754,7 @@ export const createAgentExperience = (
|
|
|
12722
12754
|
}
|
|
12723
12755
|
setTimeout(() => {
|
|
12724
12756
|
if (!voiceState.active && !voiceState.manuallyDeactivated) {
|
|
12725
|
-
if (config.voiceRecognition?.provider
|
|
12757
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
12726
12758
|
session.toggleVoice().then(() => {
|
|
12727
12759
|
voiceState.active = session.isVoiceActive();
|
|
12728
12760
|
emitVoiceState("auto");
|
|
@@ -14308,9 +14340,9 @@ export const createAgentExperience = (
|
|
|
14308
14340
|
typeof window !== 'undefined' &&
|
|
14309
14341
|
(typeof (window as any).webkitSpeechRecognition !== 'undefined' ||
|
|
14310
14342
|
typeof (window as any).SpeechRecognition !== 'undefined');
|
|
14311
|
-
const
|
|
14312
|
-
config.voiceRecognition?.provider
|
|
14313
|
-
const hasVoiceInput = hasSpeechRecognition ||
|
|
14343
|
+
const hasSessionProvider =
|
|
14344
|
+
usesSessionVoice(config.voiceRecognition?.provider);
|
|
14345
|
+
const hasVoiceInput = hasSpeechRecognition || hasSessionProvider;
|
|
14314
14346
|
|
|
14315
14347
|
if (voiceRecognitionEnabled && hasVoiceInput) {
|
|
14316
14348
|
// Create or update mic button
|
|
@@ -14343,7 +14375,7 @@ export const createAgentExperience = (
|
|
|
14343
14375
|
);
|
|
14344
14376
|
|
|
14345
14377
|
// Set disabled state
|
|
14346
|
-
micButton.disabled = session.isStreaming();
|
|
14378
|
+
micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
|
|
14347
14379
|
}
|
|
14348
14380
|
} else {
|
|
14349
14381
|
// Update existing mic button with new config
|
|
@@ -14439,14 +14471,14 @@ export const createAgentExperience = (
|
|
|
14439
14471
|
|
|
14440
14472
|
// Show and update disabled state
|
|
14441
14473
|
micButtonWrapper.style.display = "";
|
|
14442
|
-
micButton.disabled = session.isStreaming();
|
|
14474
|
+
micButton.disabled = isComposerInputDisabled() || (session.isStreaming() && !isActiveSessionVoice());
|
|
14443
14475
|
}
|
|
14444
14476
|
} else {
|
|
14445
14477
|
// Hide mic button
|
|
14446
14478
|
if (micButton && micButtonWrapper) {
|
|
14447
14479
|
micButtonWrapper.style.display = "none";
|
|
14448
14480
|
// Stop any active recording if disabling
|
|
14449
|
-
if (config.voiceRecognition?.provider
|
|
14481
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
14450
14482
|
if (session.isVoiceActive()) session.toggleVoice();
|
|
14451
14483
|
} else if (isRecording) {
|
|
14452
14484
|
stopVoiceRecognition();
|
|
@@ -15015,8 +15047,8 @@ export const createAgentExperience = (
|
|
|
15015
15047
|
},
|
|
15016
15048
|
startVoiceRecognition(): boolean {
|
|
15017
15049
|
if (session.isStreaming()) return false;
|
|
15018
|
-
if (config.voiceRecognition?.provider
|
|
15019
|
-
if (session.isVoiceActive()) return true;
|
|
15050
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
15051
|
+
if (session.isVoiceActive() || session.isBargeInActive()) return true;
|
|
15020
15052
|
if (!open && isPanelToggleable()) setOpenState(true, "system");
|
|
15021
15053
|
voiceState.manuallyDeactivated = false;
|
|
15022
15054
|
persistVoiceMetadata();
|
|
@@ -15037,9 +15069,12 @@ export const createAgentExperience = (
|
|
|
15037
15069
|
return true;
|
|
15038
15070
|
},
|
|
15039
15071
|
stopVoiceRecognition(): boolean {
|
|
15040
|
-
if (config.voiceRecognition?.provider
|
|
15041
|
-
if (!session.isVoiceActive()) return false;
|
|
15042
|
-
session.
|
|
15072
|
+
if (usesSessionVoice(config.voiceRecognition?.provider)) {
|
|
15073
|
+
if (!session.isVoiceActive() && !session.isBargeInActive()) return false;
|
|
15074
|
+
const stop = session.isBargeInActive()
|
|
15075
|
+
? session.deactivateBargeIn()
|
|
15076
|
+
: session.toggleVoice();
|
|
15077
|
+
stop.then(() => {
|
|
15043
15078
|
voiceState.active = false;
|
|
15044
15079
|
voiceState.manuallyDeactivated = true;
|
|
15045
15080
|
persistVoiceMetadata();
|