@runtypelabs/persona 4.21.1 → 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.
Files changed (129) hide show
  1. package/dist/animations/glyph-cycle.d.cts +1 -1
  2. package/dist/animations/glyph-cycle.d.ts +1 -1
  3. package/dist/animations/{types-B_JdVK9o.d.cts → types-9Vxm-PPz.d.cts} +136 -6
  4. package/dist/animations/{types-B_JdVK9o.d.ts → types-9Vxm-PPz.d.ts} +136 -6
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/animations-extra.d.cts +136 -6
  8. package/dist/animations-extra.d.ts +136 -6
  9. package/dist/{approval-ui-RNNBR5R3.js → approval-ui-K7R5Q5OC.js} +1 -1
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +221 -16
  12. package/dist/approval-ui.d.ts +221 -16
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/artifacts-ui-F4N6VMKF.js +7 -0
  15. package/dist/artifacts-ui.cjs +1 -1
  16. package/dist/artifacts-ui.d.cts +221 -16
  17. package/dist/artifacts-ui.d.ts +221 -16
  18. package/dist/artifacts-ui.js +1 -1
  19. package/dist/chunk-NQLDCJAP.js +4 -0
  20. package/dist/codegen.cjs +1 -1
  21. package/dist/codegen.js +1 -1
  22. package/dist/context-mentions.d.cts +221 -16
  23. package/dist/context-mentions.d.ts +221 -16
  24. package/dist/event-stream-view-GL3ADRY2.js +1 -0
  25. package/dist/event-stream-view.cjs +1 -1
  26. package/dist/event-stream-view.d.cts +221 -16
  27. package/dist/event-stream-view.d.ts +221 -16
  28. package/dist/event-stream-view.js +1 -1
  29. package/dist/forms-ui.d.cts +234 -25
  30. package/dist/forms-ui.d.ts +234 -25
  31. package/dist/history-view.d.cts +136 -6
  32. package/dist/history-view.d.ts +136 -6
  33. package/dist/index.cjs +70 -70
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.cts +302 -32
  36. package/dist/index.d.ts +302 -32
  37. package/dist/index.global.js +54 -54
  38. package/dist/index.global.js.map +1 -1
  39. package/dist/index.js +69 -69
  40. package/dist/index.js.map +1 -1
  41. package/dist/launcher.global.js +2 -2
  42. package/dist/launcher.global.js.map +1 -1
  43. package/dist/session-reconnect.d.cts +221 -16
  44. package/dist/session-reconnect.d.ts +221 -16
  45. package/dist/smart-dom-reader.d.cts +221 -16
  46. package/dist/smart-dom-reader.d.ts +221 -16
  47. package/dist/testing.cjs +3 -3
  48. package/dist/testing.d.cts +3 -4
  49. package/dist/testing.d.ts +3 -4
  50. package/dist/testing.js +3 -3
  51. package/dist/theme-editor-preview.cjs +73 -73
  52. package/dist/theme-editor-preview.d.cts +221 -16
  53. package/dist/theme-editor-preview.d.ts +221 -16
  54. package/dist/theme-editor-preview.js +51 -51
  55. package/dist/theme-editor.cjs +4 -4
  56. package/dist/theme-editor.d.cts +221 -16
  57. package/dist/theme-editor.d.ts +221 -16
  58. package/dist/theme-editor.js +2 -2
  59. package/dist/theme-reference.cjs +1 -1
  60. package/dist/theme-reference.d.cts +65 -1
  61. package/dist/theme-reference.d.ts +65 -1
  62. package/dist/theme-reference.js +1 -1
  63. package/dist/voice-runtime.d.cts +6 -2
  64. package/dist/voice-runtime.d.ts +6 -2
  65. package/dist/webmcp-polyfill.js +1 -4
  66. package/dist/webmcp-runtime-JI7T4QYF.js +6 -0
  67. package/dist/webmcp-runtime.cjs +2 -2
  68. package/dist/webmcp-runtime.d.cts +34 -2
  69. package/dist/webmcp-runtime.d.ts +34 -2
  70. package/dist/webmcp-runtime.js +2 -2
  71. package/package.json +2 -2
  72. package/src/ask-user-question-tool.ts +1 -1
  73. package/src/client-history-rest.test.ts +2 -1
  74. package/src/client-visitor-history.test.ts +20 -4
  75. package/src/client.composer-options.test.ts +1 -1
  76. package/src/client.interrupt.test.ts +2 -2
  77. package/src/client.test.ts +477 -915
  78. package/src/client.ts +134 -494
  79. package/src/client.unified-tools.test.ts +76 -0
  80. package/src/components/artifact-preview.ts +5 -0
  81. package/src/components/event-stream-badge-theming.test.ts +162 -0
  82. package/src/components/event-stream-view.test.ts +76 -76
  83. package/src/components/event-stream-view.ts +55 -20
  84. package/src/components/tool-bubble.test.ts +12 -0
  85. package/src/components/tool-bubble.ts +13 -2
  86. package/src/event-stream-view-bundle.test.ts +2 -2
  87. package/src/generated/runtype-openapi-contract.ts +137 -1
  88. package/src/index-core.ts +2 -0
  89. package/src/internal/runtype-history-provider.test.ts +2 -2
  90. package/src/runtime/init.registry.test.ts +56 -0
  91. package/src/runtime/init.ts +17 -0
  92. package/src/session-history.test.ts +1 -1
  93. package/src/session.quote.test.ts +1 -1
  94. package/src/session.resubmit-from.test.ts +1 -1
  95. package/src/session.test.ts +5 -5
  96. package/src/session.ts +95 -36
  97. package/src/suggest-replies-tool.ts +1 -1
  98. package/src/testing/mock-stream.test.ts +37 -16
  99. package/src/testing/mock-stream.ts +14 -9
  100. package/src/types/theme.ts +41 -1
  101. package/src/types.ts +57 -15
  102. package/src/ui.ask-user-question-plugin.test.ts +7 -7
  103. package/src/ui.composer-defer-one.test.ts +6 -34
  104. package/src/ui.composer-draft-persistence.test.ts +1 -1
  105. package/src/ui.composer-quote.test.ts +1 -1
  106. package/src/ui.message-history-actions.test.ts +1 -1
  107. package/src/ui.reasoning-display.test.ts +136 -2
  108. package/src/ui.streaming-coalescing.test.ts +10 -13
  109. package/src/ui.ts +101 -35
  110. package/src/ui.typing-indicator.test.ts +216 -0
  111. package/src/ui.voice-custom-provider.test.ts +211 -0
  112. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +1740 -0
  113. package/src/utils/event-stream-buffer.test.ts +13 -13
  114. package/src/utils/event-stream-capture.test.ts +20 -20
  115. package/src/utils/formatting.ts +22 -0
  116. package/src/utils/theme.ts +16 -0
  117. package/src/utils/throughput-tracker.test.ts +100 -142
  118. package/src/utils/throughput-tracker.ts +18 -132
  119. package/src/utils/tokens.ts +59 -0
  120. package/src/utils/voice-support.ts +18 -1
  121. package/src/voice/voice-turn-tracker.ts +18 -0
  122. package/src/webmcp-bridge.test.ts +132 -23
  123. package/src/webmcp-bridge.ts +27 -3
  124. package/src/webmcp-runtime-entry.ts +71 -11
  125. package/dist/artifacts-ui-J6AKUGTS.js +0 -7
  126. package/dist/chunk-HBE7LFNT.js +0 -4
  127. package/dist/event-stream-view-73NWKWVQ.js +0 -1
  128. package/dist/webmcp-runtime-P3CHZRHF.js +0 -6
  129. package/src/utils/__fixtures__/unified-translator.oracle.ts +0 -717
package/src/types.ts CHANGED
@@ -826,6 +826,14 @@ export type ClientToolDefinition = {
826
826
  readOnlyHint?: boolean;
827
827
  untrustedContentHint?: boolean;
828
828
  };
829
+ /**
830
+ * Hoisted copy of `annotations.untrustedContentHint`. This is the field the
831
+ * server actually acts on (`InlineClientToolSchema`): it spotlights the
832
+ * tool's output before the model reads it, so instructions embedded in
833
+ * page-provided content (reviews, comments, emails) are not treated as
834
+ * instructions. `annotations` itself is trace/dashboard metadata only.
835
+ */
836
+ untrustedContentHint?: boolean;
829
837
  };
830
838
 
831
839
  /**
@@ -983,15 +991,15 @@ export type AgentMessageMetadata = {
983
991
  */
984
992
  parentStepId?: string;
985
993
  /**
986
- * Set to `true` on a tool-variant message produced from a `step_await`
994
+ * Set to `true` on a tool-variant message produced from an `await`
987
995
  * event (`awaitReason: "local_tool_required"`). Signals to UI code that
988
996
  * the tool call is a LOCAL tool and the server is paused waiting for a
989
997
  * `POST /v1/dispatch/resume` with the user's answer keyed by tool name.
990
998
  */
991
999
  awaitingLocalTool?: boolean;
992
1000
  /**
993
- * The provider per-call id (`toolu_…`) carried on the `step_await` /
994
- * `flow_await` events for a LOCAL tool (core#3878). Present only when the
1001
+ * The provider per-call id (`toolu_…`) carried on the `await`
1002
+ * event for a LOCAL tool (core#3878). Present only when the
995
1003
  * server emits it. Two PARALLEL calls to the same tool in one turn share a
996
1004
  * `toolName` (and a collapsed `toolId`) but get DISTINCT `webMcpToolCallId`s,
997
1005
  * so this is the key the widget batches a single `/resume` on: preferred
@@ -2237,6 +2245,16 @@ export type AgentWidgetToolCallDisplayFeature = {
2237
2245
  * @default "stack"
2238
2246
  */
2239
2247
  groupedMode?: "stack" | "summary";
2248
+ /**
2249
+ * What happens to a tool call row once it completes.
2250
+ *
2251
+ * - `"kept"` (default): the finished row stays in the transcript.
2252
+ * - `"removed"`: no row is rendered at all, so the transcript gap closes with
2253
+ * it. The tool call stays on the message and in `getMessages()`.
2254
+ *
2255
+ * @default "kept"
2256
+ */
2257
+ completedVisibility?: AgentWidgetToolCallCompletedVisibility;
2240
2258
  /**
2241
2259
  * When false, tool call bubbles show only the collapsed summary with no
2242
2260
  * expand/collapse toggle. Users see tool awareness without full details.
@@ -2255,6 +2273,9 @@ export type AgentWidgetToolCallDisplayFeature = {
2255
2273
  loadingAnimation?: AgentWidgetToolCallLoadingAnimation;
2256
2274
  };
2257
2275
 
2276
+ /** See `features.toolCallDisplay.completedVisibility`. */
2277
+ export type AgentWidgetToolCallCompletedVisibility = "kept" | "removed";
2278
+
2258
2279
  export type AgentWidgetReasoningDisplayFeature = {
2259
2280
  /**
2260
2281
  * When true, active collapsed reasoning rows can render a lightweight preview block.
@@ -2300,14 +2321,30 @@ export type AgentWidgetReasoningDisplayFeature = {
2300
2321
  * - `"kept"` (default): the finished row stays in the transcript.
2301
2322
  * - `"removed"`: no row is rendered at all, so the transcript gap closes with
2302
2323
  * it. The reasoning text stays on the message and in `getMessages()`.
2324
+ * - `"removed-when-short"`: behaves as `"removed"` when the completed trace
2325
+ * is short (see `shortThinkThreshold`), and as `"kept"` otherwise.
2303
2326
  *
2304
2327
  * @default "kept"
2305
2328
  */
2306
2329
  completedVisibility?: AgentWidgetReasoningCompletedVisibility;
2330
+ /**
2331
+ * Thresholds used by `completedVisibility: "removed-when-short"` to decide
2332
+ * whether a completed trace counts as short. Duration is checked first when
2333
+ * the reasoning reports one; the character count is a fallback for traces
2334
+ * with no reported duration.
2335
+ * @default { durationMs: 15000, chars: 1200 }
2336
+ */
2337
+ shortThinkThreshold?: {
2338
+ durationMs?: number;
2339
+ chars?: number;
2340
+ };
2307
2341
  };
2308
2342
 
2309
2343
  /** See `features.reasoningDisplay.completedVisibility`. */
2310
- export type AgentWidgetReasoningCompletedVisibility = "kept" | "removed";
2344
+ export type AgentWidgetReasoningCompletedVisibility =
2345
+ | "kept"
2346
+ | "removed"
2347
+ | "removed-when-short";
2311
2348
 
2312
2349
  /**
2313
2350
  * Reveal animation applied to assistant message text while it is streaming.
@@ -3381,7 +3418,7 @@ export type EventStreamConfig = {
3381
3418
  /**
3382
3419
  * Custom badge color mappings by event type prefix or exact type.
3383
3420
  * Keys are matched as exact match first, then prefix match (keys ending with "_").
3384
- * @example { "flow_": { bg: "#dcfce7", text: "#166534" }, "error": { bg: "#fecaca", text: "#991b1b" } }
3421
+ * @example { "execution_": { bg: "#dcfce7", text: "#166534" }, "error": { bg: "#fecaca", text: "#991b1b" } }
3385
3422
  */
3386
3423
  badgeColors?: Record<string, EventStreamBadgeColor>;
3387
3424
  /**
@@ -4946,10 +4983,12 @@ export interface VoiceProvider {
4946
4983
  * `isFinal=false` is a live interim update (user partials, or assistant deltas
4947
4984
  * on providers that stream them); `isFinal=true` finalizes that role's text.
4948
4985
  * On the realtime `runtype` path, interim updates fire for the `user` only and
4949
- * 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.
4950
4989
  */
4951
4990
  onTranscript?(
4952
- callback: (role: 'user' | 'assistant', text: string, isFinal: boolean) => void,
4991
+ callback: (role: 'user' | 'assistant', text: string, isFinal: boolean, metadata?: { turnId?: string }) => void,
4953
4992
  ): void;
4954
4993
 
4955
4994
  /** Register a callback for per-turn latency metrics (realtime path). */
@@ -7524,16 +7563,16 @@ export type AgentWidgetConfig = {
7524
7563
  *
7525
7564
  * @example
7526
7565
  * ```typescript
7527
- * // For Runtype API format
7566
+ * // For a custom backend (Runtype unified events are handled natively)
7528
7567
  * config: {
7529
7568
  * parseSSEEvent: (data) => {
7530
- * if ((data.type === 'step_delta' || data.type === 'step_chunk') && (data.delta || data.chunk)) {
7531
- * return { text: data.delta ?? data.chunk };
7569
+ * if (data.type === 'token' && typeof data.value === 'string') {
7570
+ * return { text: data.value };
7532
7571
  * }
7533
- * if (data.type === 'flow_complete') {
7572
+ * if (data.type === 'done') {
7534
7573
  * return { done: true };
7535
7574
  * }
7536
- * if (data.type === 'step_error') {
7575
+ * if (data.type === 'failure') {
7537
7576
  * return { error: data.error };
7538
7577
  * }
7539
7578
  * return null; // Ignore other events
@@ -7750,7 +7789,7 @@ export type AgentWidgetReasoning = {
7750
7789
  /**
7751
7790
  * Reasoning channel scope (wire spec). `"turn"` is ordinary per-turn
7752
7791
  * thinking; `"loop"` is a cross-iteration agent reflection (the fold that
7753
- * replaced the legacy `agent_reflection` event). Absent for legacy streams.
7792
+ * replaced the legacy `agent_reflection` event). Optional when the stream does not specify a scope.
7754
7793
  */
7755
7794
  scope?: "turn" | "loop";
7756
7795
  startedAt?: number;
@@ -7765,6 +7804,9 @@ export type AgentWidgetToolCall = {
7765
7804
  args?: unknown;
7766
7805
  chunks?: string[];
7767
7806
  result?: unknown;
7807
+ /** Whether the tool succeeded; failure does not necessarily end the execution. */
7808
+ success?: boolean;
7809
+ error?: string;
7768
7810
  duration?: number;
7769
7811
  startedAt?: number;
7770
7812
  completedAt?: number;
@@ -7798,7 +7840,7 @@ export type AgentWidgetMessageVariant = "assistant" | "reasoning" | "tool" | "ap
7798
7840
 
7799
7841
  /**
7800
7842
  * Per-turn / per-step stop reason emitted by the runtime on
7801
- * `agent_turn_complete` and `step_complete` SSE events. The vocabulary is
7843
+ * `turn_complete` and `step_complete` SSE events. The vocabulary is
7802
7844
  * owned by the upstream Runtype API: do not extend without coordination.
7803
7845
  *
7804
7846
  * - `end_turn`: natural completion (no affordance needed)
@@ -7930,7 +7972,7 @@ export type AgentWidgetMessage = {
7930
7972
  */
7931
7973
  agentMetadata?: AgentMessageMetadata;
7932
7974
  /**
7933
- * Per-turn stop reason reported by the runtime on `agent_turn_complete`
7975
+ * Per-turn stop reason reported by the runtime on `turn_complete`
7934
7976
  * (agent-loop path) or the last `step_complete` for a prompt step
7935
7977
  * (dispatch / flow path). Absent when the API did not report a value.
7936
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":"flow_complete","success":true}\n\n'));
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":"flow_complete","success":true}\n\n'));
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":"flow_complete","success":true}\n\n'));
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":"flow_complete","success":true}\n\n'));
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":"flow_complete","success":true}\n\n'));
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":"flow_complete","success":true}\n\n'));
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":"flow_complete","success":true}\n\n'));
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 { createUnifiedEventWrite } from "./utils/__fixtures__/unified-translator.oracle";
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
- let out = "";
14
- const write = createUnifiedEventWrite((chunk) => {
15
- out += chunk;
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":"flow_complete","success":true}\n\n')
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":"flow_complete","success":true}\n\n')
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":"flow_complete","success":true}\n\n')
20
+ encoder.encode('data: {"type":"execution_complete","success":true}\n\n')
21
21
  );
22
22
  controller.close();
23
23
  },
@@ -25,7 +25,8 @@ const injectReasoning = (
25
25
  controller: ReturnType<typeof createAgentExperience>,
26
26
  id: string,
27
27
  status: "streaming" | "complete",
28
- chunks: string[] = ["Weighing the options"]
28
+ chunks: string[] = ["Weighing the options"],
29
+ durationMs?: number
29
30
  ) => {
30
31
  controller.injectTestMessage({
31
32
  type: "message",
@@ -36,7 +37,26 @@ const injectReasoning = (
36
37
  createdAt: new Date().toISOString(),
37
38
  streaming: status !== "complete",
38
39
  variant: "reasoning",
39
- reasoning: { id, status, chunks },
40
+ reasoning: { id, status, chunks, durationMs },
41
+ },
42
+ });
43
+ };
44
+
45
+ const injectToolCall = (
46
+ controller: ReturnType<typeof createAgentExperience>,
47
+ id: string,
48
+ status: "pending" | "running" | "complete"
49
+ ) => {
50
+ controller.injectTestMessage({
51
+ type: "message",
52
+ message: {
53
+ id,
54
+ role: "assistant",
55
+ content: "",
56
+ createdAt: new Date().toISOString(),
57
+ streaming: status !== "complete",
58
+ variant: "tool",
59
+ toolCall: { id, name: "search", status, chunks: [] },
40
60
  },
41
61
  });
42
62
  };
@@ -161,4 +181,118 @@ describe("features.reasoningDisplay.completedVisibility", () => {
161
181
  ?.chunks
162
182
  ).toEqual(["Weighing the options"]);
163
183
  });
184
+
185
+ it('"removed-when-short" drops a completed trace with a short reported duration', () => {
186
+ const { mount, controller } = makeController({
187
+ features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
188
+ });
189
+ injectReasoning(controller, "r1", "complete", ["Weighing the options"], 5_000);
190
+ expect(rowOf(mount, "r1")).toBeNull();
191
+ });
192
+
193
+ it('"removed-when-short" keeps a completed trace with a long reported duration', () => {
194
+ const { mount, controller } = makeController({
195
+ features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
196
+ });
197
+ injectReasoning(controller, "r1", "complete", ["Weighing the options"], 20_000);
198
+ expect(rowOf(mount, "r1")).not.toBeNull();
199
+ });
200
+
201
+ it('"removed-when-short" drops a completed trace with no duration and few chars', () => {
202
+ const { mount, controller } = makeController({
203
+ features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
204
+ });
205
+ injectReasoning(controller, "r1", "complete", ["Short thought"]);
206
+ expect(rowOf(mount, "r1")).toBeNull();
207
+ });
208
+
209
+ it('"removed-when-short" keeps a completed trace with no duration and many chars', () => {
210
+ const { mount, controller } = makeController({
211
+ features: { reasoningDisplay: { completedVisibility: "removed-when-short" } },
212
+ });
213
+ injectReasoning(controller, "r1", "complete", ["x".repeat(1_500)]);
214
+ expect(rowOf(mount, "r1")).not.toBeNull();
215
+ });
216
+
217
+ it('"removed-when-short" honors custom duration and char thresholds', () => {
218
+ const { mount, controller } = makeController({
219
+ features: {
220
+ reasoningDisplay: {
221
+ completedVisibility: "removed-when-short",
222
+ shortThinkThreshold: { durationMs: 1_000, chars: 50 },
223
+ },
224
+ },
225
+ });
226
+ // 5s exceeds the custom 1s threshold, so this long-by-default trace is kept.
227
+ injectReasoning(controller, "r1", "complete", ["Weighing the options"], 5_000);
228
+ expect(rowOf(mount, "r1")).not.toBeNull();
229
+
230
+ // 60 chars exceeds the custom 50-char threshold, so this trace is kept too.
231
+ injectReasoning(controller, "r2", "complete", ["y".repeat(60)]);
232
+ expect(rowOf(mount, "r2")).not.toBeNull();
233
+
234
+ // Under both custom thresholds, so this trace is dropped.
235
+ injectReasoning(controller, "r3", "complete", ["z".repeat(10)], 500);
236
+ expect(rowOf(mount, "r3")).toBeNull();
237
+ });
238
+ });
239
+
240
+ describe("features.toolCallDisplay.completedVisibility", () => {
241
+ beforeEach(() => {
242
+ window.localStorage.clear();
243
+ vi.stubGlobal("requestAnimationFrame", (cb: (time: number) => void) => {
244
+ cb(0);
245
+ return 1;
246
+ });
247
+ vi.stubGlobal("cancelAnimationFrame", () => {});
248
+ window.scrollTo = vi.fn();
249
+ });
250
+
251
+ afterEach(() => {
252
+ controllers.splice(0).forEach((controller) => controller.destroy());
253
+ mounts.splice(0).forEach((mount) => mount.remove());
254
+ document.body.innerHTML = "";
255
+ vi.restoreAllMocks();
256
+ });
257
+
258
+ it("keeps a finished tool call row by default", () => {
259
+ const { mount, controller } = makeController();
260
+ injectToolCall(controller, "t1", "complete");
261
+ expect(rowOf(mount, "t1")).not.toBeNull();
262
+ });
263
+
264
+ it('removes the whole row under "removed", leaving no transcript gap', () => {
265
+ const { mount, controller } = makeController({
266
+ features: { toolCallDisplay: { completedVisibility: "removed" } },
267
+ });
268
+ injectToolCall(controller, "t1", "complete");
269
+ injectText(controller, "a1", "Here is the answer");
270
+
271
+ expect(rowOf(mount, "t1")).toBeNull();
272
+ const rows = mount.querySelectorAll("[data-wrapper-id]");
273
+ expect(Array.from(rows).map((row) => row.getAttribute("data-wrapper-id")))
274
+ .toEqual(["a1"]);
275
+ });
276
+
277
+ it('shows a running tool call under "removed" and drops it once it completes', () => {
278
+ const { mount, controller } = makeController({
279
+ features: { toolCallDisplay: { completedVisibility: "removed" } },
280
+ });
281
+ injectToolCall(controller, "t1", "running");
282
+ expect(rowOf(mount, "t1")).not.toBeNull();
283
+
284
+ injectToolCall(controller, "t1", "complete");
285
+ expect(rowOf(mount, "t1")).toBeNull();
286
+ });
287
+
288
+ it('keeps the tool call message in the session under "removed"', () => {
289
+ const { controller } = makeController({
290
+ features: { toolCallDisplay: { completedVisibility: "removed" } },
291
+ });
292
+ injectToolCall(controller, "t1", "complete");
293
+ expect(
294
+ controller.getMessages().find((message) => message.id === "t1")?.toolCall
295
+ ?.status
296
+ ).toEqual("complete");
297
+ });
164
298
  });
@@ -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 legacyEvent = (type: string, data: Record<string, unknown>) =>
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 = createUnifiedEventWrite((chunk) => {
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(legacyEvent(type, data));
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("flow_start", { flowId: "flow_1", flowName: "Test", totalSteps: 1 });
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", { messageId: "message_1" });
97
- deltas.forEach((text) => stream.send("step_delta", { id: "step_1", text }));
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("text_end", { messageId: "message_1" });
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("flow_complete", { flowId: "flow_1", success: true });
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("step_delta", { id: "step_1", text: "lo" });
149
- stream.send("step_delta", { id: "step_1", text: " world" });
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);