@runtypelabs/persona 4.32.1 → 4.33.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.
Files changed (165) hide show
  1. package/dist/activity-ui.d.cts +3 -3
  2. package/dist/activity-ui.d.ts +3 -3
  3. package/dist/animations/glyph-cycle.d.cts +1 -1
  4. package/dist/animations/glyph-cycle.d.ts +1 -1
  5. package/dist/animations/wipe.d.cts +1 -1
  6. package/dist/animations/wipe.d.ts +1 -1
  7. package/dist/approval-ui.d.cts +2 -2
  8. package/dist/approval-ui.d.ts +2 -2
  9. package/dist/artifacts-ui.d.cts +3 -3
  10. package/dist/artifacts-ui.d.ts +3 -3
  11. package/dist/background-runtime.d.cts +1 -1
  12. package/dist/background-runtime.d.ts +1 -1
  13. package/dist/{chunk-BbXc58OA.js → chunk-6CUrH_jm.js} +1 -1
  14. package/dist/{chunk-B_H0l3u2.js → chunk-BRM3bHcJ.js} +1 -1
  15. package/dist/chunk-CEPM3FOp.js +1 -0
  16. package/dist/{chunk-CumoXK3u.js → chunk-CXQJG7pb.js} +1 -1
  17. package/dist/{chunk-UWMfWW2a.js → chunk-D1G8tgYc.js} +1 -1
  18. package/dist/{chunk-BpjtKnGI.js → chunk-D6U0E82p.js} +1 -1
  19. package/dist/{chunk-2FnmZ9AA.js → chunk-DOZmcve8.js} +1 -1
  20. package/dist/chunk-DVgzoVCB.js +1 -0
  21. package/dist/{chunk-C9uOhfk5.js → chunk-DW7Vg_l2.js} +1 -1
  22. package/dist/{chunk-B3l7TOOv.js → chunk-DbGLjU2P.js} +1 -1
  23. package/dist/{chunk-DTGvDx8o.js → chunk-Dj2a9f9w.js} +1 -1
  24. package/dist/{chunk-DgZZqy14.js → chunk-d54BJdKd.js} +1 -1
  25. package/dist/chunk-dLJWKfOb.js +1 -0
  26. package/dist/{chunk-DNdeIFJ6.js → chunk-gu34lr-P.js} +1 -1
  27. package/dist/chunk-i4qGOJAL.js +1 -0
  28. package/dist/{chunk-DVZ8ri1z.js → chunk-ql5N2I_t.js} +1 -1
  29. package/dist/client-dispatch.js +1 -1
  30. package/dist/client-history.js +1 -1
  31. package/dist/codegen.cjs +1 -1
  32. package/dist/codegen.js +1 -1
  33. package/dist/composer-actions-runtime.d.cts +2 -2
  34. package/dist/composer-actions-runtime.d.ts +2 -2
  35. package/dist/{composer-input-dwg_o9Z-.d.cts → composer-input-DnqGCXZz.d.cts} +1 -1
  36. package/dist/{composer-input-dwg_o9Z-.d.ts → composer-input-DnqGCXZz.d.ts} +1 -1
  37. package/dist/{composer-store-Dw7Zbqxr.d.cts → composer-store-6Iwy6WLI.d.cts} +1 -1
  38. package/dist/{composer-store-Dw7Zbqxr.d.ts → composer-store-6Iwy6WLI.d.ts} +1 -1
  39. package/dist/{context-mention-manager-D2p0-JH-.d.cts → context-mention-manager-B-rHF98n.d.cts} +2 -2
  40. package/dist/{context-mention-manager-D2p0-JH-.d.ts → context-mention-manager-B-rHF98n.d.ts} +2 -2
  41. package/dist/context-mentions-inline.d.cts +2 -2
  42. package/dist/context-mentions-inline.d.ts +2 -2
  43. package/dist/context-mentions.d.cts +3 -3
  44. package/dist/context-mentions.d.ts +3 -3
  45. package/dist/{defaults-CjX0kKzb.d.ts → defaults-BuOjqUAG.d.cts} +2 -1
  46. package/dist/{defaults-CjX0kKzb.d.cts → defaults-BuOjqUAG.d.ts} +2 -1
  47. package/dist/dictation-runtime.d.cts +2 -2
  48. package/dist/dictation-runtime.d.ts +2 -2
  49. package/dist/event-stream-view.d.cts +1 -1
  50. package/dist/event-stream-view.d.ts +1 -1
  51. package/dist/{fallback-speech-engine-DmF7MczI.d.cts → fallback-speech-engine-SqEfkGda.d.cts} +1 -1
  52. package/dist/{fallback-speech-engine-DmF7MczI.d.ts → fallback-speech-engine-SqEfkGda.d.ts} +1 -1
  53. package/dist/{feature-preferences-CVFDt9Xf.d.cts → feature-preferences-DqAmje-2.d.cts} +1 -1
  54. package/dist/{feature-preferences-CVFDt9Xf.d.ts → feature-preferences-DqAmje-2.d.ts} +1 -1
  55. package/dist/forms-ui.d.cts +2 -2
  56. package/dist/forms-ui.d.ts +2 -2
  57. package/dist/{history-confirm-dialog-DJEXsTyE.d.cts → history-confirm-dialog-B3L7QNcO.d.cts} +1 -1
  58. package/dist/{history-confirm-dialog-DJEXsTyE.d.ts → history-confirm-dialog-B3L7QNcO.d.ts} +1 -1
  59. package/dist/history-shell.cjs +1 -1
  60. package/dist/history-shell.d.cts +6 -6
  61. package/dist/history-shell.d.ts +6 -6
  62. package/dist/history-shell.js +1 -1
  63. package/dist/history-view.d.cts +1 -1
  64. package/dist/history-view.d.ts +1 -1
  65. package/dist/index.cjs +38 -38
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +9 -9
  68. package/dist/index.d.ts +9 -9
  69. package/dist/index.global.js +1 -1
  70. package/dist/index.global.js.map +1 -1
  71. package/dist/index.js +52 -52
  72. package/dist/index.js.map +1 -1
  73. package/dist/install.global.js +1 -1
  74. package/dist/install.global.js.map +1 -1
  75. package/dist/launcher.global.js +2 -2
  76. package/dist/launcher.global.js.map +1 -1
  77. package/dist/{loading-label-runtime-loader-CPxnoaoi.d.cts → loading-label-runtime-loader-JecjOfJS.d.cts} +1 -1
  78. package/dist/{loading-label-runtime-loader-CPxnoaoi.d.ts → loading-label-runtime-loader-JecjOfJS.d.ts} +1 -1
  79. package/dist/loading-label-runtime.d.cts +1 -1
  80. package/dist/loading-label-runtime.d.ts +1 -1
  81. package/dist/{message-bubble-CkZGlCLC.d.ts → message-bubble-CBYtpDsT.d.cts} +2 -2
  82. package/dist/{message-bubble-CkZGlCLC.d.cts → message-bubble-CBYtpDsT.d.ts} +2 -2
  83. package/dist/preferences-runtime.d.cts +1 -1
  84. package/dist/preferences-runtime.d.ts +1 -1
  85. package/dist/{preview-DdUvoLb4.d.cts → preview-_UJwwt2L.d.cts} +2 -2
  86. package/dist/{preview-DdUvoLb4.d.ts → preview-_UJwwt2L.d.ts} +2 -2
  87. package/dist/{registry-Dz4iHNST.d.cts → registry-B5ZV3K-h.d.cts} +1 -1
  88. package/dist/{registry-Dz4iHNST.d.ts → registry-B5ZV3K-h.d.ts} +1 -1
  89. package/dist/{session-Baxp9iRK.d.cts → session-Bys8_XuB.d.cts} +10 -9
  90. package/dist/{session-Baxp9iRK.d.ts → session-Bys8_XuB.d.ts} +10 -9
  91. package/dist/session-reconnect.d.cts +2 -2
  92. package/dist/session-reconnect.d.ts +2 -2
  93. package/dist/smart-dom-reader.d.cts +1 -1
  94. package/dist/smart-dom-reader.d.ts +1 -1
  95. package/dist/theme-editor-preview.cjs +31 -31
  96. package/dist/theme-editor-preview.d.cts +1 -1
  97. package/dist/theme-editor-preview.d.ts +1 -1
  98. package/dist/theme-editor-preview.js +19 -19
  99. package/dist/theme-editor.cjs +1 -1
  100. package/dist/theme-editor.d.cts +2 -2
  101. package/dist/theme-editor.d.ts +2 -2
  102. package/dist/theme-editor.js +1 -1
  103. package/dist/theme-reference.cjs +1 -1
  104. package/dist/theme-reference.d.cts +4 -2
  105. package/dist/theme-reference.d.ts +4 -2
  106. package/dist/theme-reference.js +1 -1
  107. package/dist/{tool-loading-animation-un3kA5Ur.d.cts → tool-loading-animation-d5qAQcwv.d.cts} +1 -1
  108. package/dist/{tool-loading-animation-un3kA5Ur.d.ts → tool-loading-animation-d5qAQcwv.d.ts} +1 -1
  109. package/dist/{turn-activity-ui-BGNhtMcH.d.cts → turn-activity-ui-q6fHVnx-.d.cts} +2 -2
  110. package/dist/{turn-activity-ui-BGNhtMcH.d.ts → turn-activity-ui-q6fHVnx-.d.ts} +2 -2
  111. package/dist/turn-activity-ui.d.cts +1 -1
  112. package/dist/turn-activity-ui.d.ts +1 -1
  113. package/dist/{types-BOZHQIk0.d.cts → types-B_mj21jg.d.cts} +91 -7
  114. package/dist/{types-BOZHQIk0.d.ts → types-B_mj21jg.d.ts} +91 -7
  115. package/dist/voice-client-tools.d.cts +1 -1
  116. package/dist/voice-client-tools.d.ts +1 -1
  117. package/dist/voice-runtime.cjs +1 -1
  118. package/dist/voice-runtime.d.cts +2 -2
  119. package/dist/voice-runtime.d.ts +2 -2
  120. package/dist/voice-runtime.js +1 -1
  121. package/dist/voice-worklet-player.d.cts +2 -2
  122. package/dist/voice-worklet-player.d.ts +2 -2
  123. package/dist/{webmcp-runtime-entry-DuAG1_yF.d.cts → webmcp-runtime-entry-ClEO3qdI.d.cts} +1 -1
  124. package/dist/{webmcp-runtime-entry-DuAG1_yF.d.ts → webmcp-runtime-entry-ClEO3qdI.d.ts} +1 -1
  125. package/dist/webmcp-runtime.d.cts +1 -1
  126. package/dist/webmcp-runtime.d.ts +1 -1
  127. package/dist/widget.css +1 -1
  128. package/package.json +1 -1
  129. package/src/client-dispatch.ts +69 -16
  130. package/src/client.operator-role.test.ts +217 -0
  131. package/src/components/message-bubble.test.ts +169 -0
  132. package/src/components/message-bubble.ts +97 -28
  133. package/src/defaults-parity.test.ts +14 -0
  134. package/src/index-core.ts +7 -0
  135. package/src/index-global.ts +2 -1
  136. package/src/install.test.ts +6 -5
  137. package/src/install.ts +2 -2
  138. package/src/launcher-global.ts +2 -1
  139. package/src/session.resubmit-from.test.ts +27 -0
  140. package/src/session.test.ts +51 -0
  141. package/src/session.ts +22 -12
  142. package/src/styles/widget.css +23 -1
  143. package/src/theme-reference.ts +3 -1
  144. package/src/types/theme.ts +21 -0
  145. package/src/types.ts +71 -6
  146. package/src/ui.ask-user-question-plugin.test.ts +154 -0
  147. package/src/ui.operator-role.test.ts +336 -0
  148. package/src/ui.ts +37 -12
  149. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +236 -0
  150. package/src/utils/brand.test.ts +14 -0
  151. package/src/utils/brand.ts +8 -4
  152. package/src/utils/cdn-release.test.ts +20 -0
  153. package/src/utils/cdn-release.ts +14 -0
  154. package/src/utils/history-messages.test.ts +30 -0
  155. package/src/utils/history-messages.ts +26 -8
  156. package/src/utils/message-fingerprint.ts +4 -0
  157. package/src/utils/resubmit-turn.test.ts +13 -0
  158. package/src/utils/resubmit-turn.ts +12 -1
  159. package/src/utils/tokens.ts +31 -0
  160. package/src/welcome.test.ts +16 -0
  161. package/src/welcome.ts +10 -2
  162. package/dist/chunk-BkOYJ11B.js +0 -1
  163. package/dist/chunk-Bz9jAgba.js +0 -1
  164. package/dist/chunk-CdpVP6LZ.js +0 -1
  165. package/dist/chunk-DM-OJt5q.js +0 -1
@@ -58,10 +58,24 @@ describe('brand derivation', () => {
58
58
  expect(getColorScheme({ brand: { neutral: '#111111' }, colorScheme: 'light' })).toBe('light');
59
59
  expect(getColorScheme({ brand: { neutral: '#ffffff' }, colorScheme: 'dark' })).toBe('dark');
60
60
  });
61
+ it.each(['light', 'dark'] as const)('outlines the V5 operator bubble instead of filling it in %s mode', (colorScheme) => {
62
+ const stock = getCssVariables(getActiveTheme({ colorScheme, future: { v5Defaults: true } }));
63
+ expect(stock['--persona-components-message-operator-background']).toBe('transparent');
64
+ expect(stock['--persona-components-message-operator-border']).toBe(stock['--persona-border']);
65
+ expect(stock['--persona-components-message-operator-borderWidth']).toBe('1px');
66
+ // A brand accent tints the outline; the bubble stays unfilled.
67
+ const branded = getCssVariables(getActiveTheme({ colorScheme, future: { v5Defaults: true }, brand: { accent: '#4c00ff', density: 'compact' } }));
68
+ expect(branded['--persona-components-message-operator-background']).toBe('transparent');
69
+ expect(branded['--persona-components-message-operator-border']).toBe('color-mix(in srgb, var(--persona-primary) 35%, var(--persona-border))');
70
+ // V4 keeps its primary-tinted fill.
71
+ const v4 = getCssVariables(getActiveTheme({ colorScheme }));
72
+ expect(v4['--persona-components-message-operator-background']).toBe('color-mix(in srgb, var(--persona-primary) 6%, var(--persona-container))');
73
+ });
61
74
  it.each([['compact', '13px', '44px'], ['comfortable', '14px', '48px'], ['spacious', '15px', '52px']] as const)('maps %s density', (density, size, height) => {
62
75
  const theme = deriveBrandTheme({ density });
63
76
  expect(theme.semantic?.typography?.fontSize).toBe(size);
64
77
  expect(theme.components?.header?.minHeight).toBe(height);
78
+ expect(theme.components?.message?.operator?.fontSize).toBe(size);
65
79
  });
66
80
  it.each([['none', '0px'], ['sm', '4px'], ['md', '8px'], ['lg', '16px']] as const)('maps %s radius while preserving circles', (radius, value) => {
67
81
  const theme = deriveBrandTheme({ radius });
@@ -61,9 +61,13 @@ export const deriveBrandTheme = (brand?: BrandConfig, scheme: 'light' | 'dark' =
61
61
  ? { background: 'semantic.colors.container', foreground: 'semantic.colors.text', iconBackground: 'transparent', iconForeground: 'semantic.colors.text' }
62
62
  : { background: ramp[500], foreground: ink, iconBackground: ramp[500], iconForeground: ink },
63
63
  approval: { approve: { foreground: ink } },
64
- message: { user: v5Defaults
65
- ? { background: 'color-mix(in srgb, var(--persona-primary) 12%, var(--persona-container))', text: 'semantic.colors.text' }
66
- : { background: ramp[500], text: ink } },
64
+ message: v5Defaults
65
+ ? {
66
+ user: { background: 'color-mix(in srgb, var(--persona-primary) 12%, var(--persona-container))', text: 'semantic.colors.text' },
67
+ // The V5 operator stays outlined; a brand accent tints the outline.
68
+ operator: { border: 'color-mix(in srgb, var(--persona-primary) 35%, var(--persona-border))' },
69
+ }
70
+ : { user: { background: ramp[500], text: ink } },
67
71
  };
68
72
  }
69
73
  const neutral = parseColor(brand.neutral);
@@ -104,7 +108,7 @@ export const deriveBrandTheme = (brand?: BrandConfig, scheme: 'light' | 'dark' =
104
108
  if (brand.density && density) {
105
109
  const [size, height, gap] = density;
106
110
  theme.semantic = { ...theme.semantic, typography: { ...theme.semantic?.typography, fontSize: `${size}px` } };
107
- theme.components = { ...theme.components, header: { ...theme.components?.header, minHeight: `${height}px`, padding: `${gap}px 16px`, minimalPadding: `${gap}px 16px` }, history: { railHeader: { minHeight: `${height}px` } }, composer: { ...theme.components?.composer, fontSize: `${size}px`, padding: `${gap}px`, gap: `${gap / 2}px` }, message: { ...theme.components?.message, gap: `${gap}px`, user: { ...theme.components?.message?.user, fontSize: `${size}px` }, assistant: { fontSize: `${size}px` } } };
111
+ theme.components = { ...theme.components, header: { ...theme.components?.header, minHeight: `${height}px`, padding: `${gap}px 16px`, minimalPadding: `${gap}px 16px` }, history: { railHeader: { minHeight: `${height}px` } }, composer: { ...theme.components?.composer, fontSize: `${size}px`, padding: `${gap}px`, gap: `${gap / 2}px` }, message: { ...theme.components?.message, gap: `${gap}px`, user: { ...theme.components?.message?.user, fontSize: `${size}px` }, assistant: { fontSize: `${size}px` }, operator: { ...theme.components?.message?.operator, fontSize: `${size}px` } } };
108
112
  }
109
113
  if (brand.elevation && ['flat', 'subtle', 'raised'].includes(brand.elevation)) {
110
114
  const flat = brand.elevation === 'flat';
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { version } from "../../package.json";
3
+ import { pinCdnRelease } from "./cdn-release";
4
+
5
+ describe("pinCdnRelease", () => {
6
+ it.each(["install", "index", "launcher"])("pins the %s entry's dependencies to its embedded release", (entry) => {
7
+ expect(pinCdnRelease(`https://cdn.runtype.com/persona/latest/${entry}.global.js?embed=1`))
8
+ .toBe(`https://cdn.runtype.com/persona/${version}/${entry}.global.js?embed=1`);
9
+ });
10
+
11
+ it.each([
12
+ "https://cdn.runtype.com/persona/4.0.0/index.global.js",
13
+ "https://cdn.example.com/persona/latest/index.global.js",
14
+ "https://cdn.jsdelivr.net/npm/@runtypelabs/persona@4/dist/index.global.js",
15
+ "https://cdn.runtype.com/other/latest/index.global.js",
16
+ "",
17
+ ])("preserves explicit versions and other hosting layouts: %s", (src) => {
18
+ expect(pinCdnRelease(src)).toBe(src);
19
+ });
20
+ });
@@ -0,0 +1,14 @@
1
+ import { version } from "../../package.json";
2
+
3
+ /**
4
+ * A cached /latest/ entry must keep using its own release, even after another
5
+ * release replaces latest. Only rewrite our published CDN layout; custom hosts
6
+ * and explicitly versioned URLs retain their existing sibling resolution.
7
+ * The bundler embeds this package version in each script-tag entry.
8
+ */
9
+ export function pinCdnRelease(src: string): string {
10
+ return src.replace(
11
+ /^https:\/\/cdn\.runtype\.com\/persona\/latest\//,
12
+ `https://cdn.runtype.com/persona/${version}/`,
13
+ );
14
+ }
@@ -182,6 +182,17 @@ describe("mapWireMessages: role filtering", () => {
182
182
  ]);
183
183
  expect(mapped.map((message) => message.id)).toEqual(["a", "d", "e"]);
184
184
  });
185
+
186
+ it("keeps operator messages from a custom history provider", () => {
187
+ const mapped = mapWireMessages([
188
+ { id: "a", role: "user", content: "hi", displayAvailable: true },
189
+ { id: "b", role: "operator", content: "Sam from support", displayAvailable: true },
190
+ ]);
191
+ expect(mapped.map((message) => [message.id, message.role])).toEqual([
192
+ ["a", "user"],
193
+ ["b", "operator"],
194
+ ]);
195
+ });
185
196
  });
186
197
 
187
198
  describe("mapWireMessages: timestamp synthesis", () => {
@@ -282,3 +293,22 @@ describe("mergeWireMessagesById", () => {
282
293
  expect(merged.map((message) => message.id)).toEqual(["a", "c", "b"]);
283
294
  });
284
295
  });
296
+
297
+ describe("custom history human provenance", () => {
298
+ it("discards malformed author fields from a JavaScript history provider", () => {
299
+ const [message] = mapWireMessages([{ id: "human", role: "operator", content: "Hello", displayAvailable: true,
300
+ author: { id: 42, displayName: null, kind: "admin" } as unknown as HistoryWireMessage["author"] }]);
301
+ expect(message.author).toEqual({});
302
+ });
303
+ it("preserves human identity with text and attachment histories", () => {
304
+ const author = { id: "sam", displayName: "Sam", kind: "human" as const };
305
+ const mapped = mapWireMessages([
306
+ { id: "human", role: "operator", content: "Refund confirmed", displayAvailable: true, author },
307
+ { id: "photo", role: "operator", content: [{ type: "image", image: "https://example.com/photo.png" }], displayAvailable: true, author },
308
+ ]);
309
+ expect(mapped.map(message => message.author)).toEqual([author, author]);
310
+ expect(mapped.map(message => message.role)).toEqual(["operator", "operator"]);
311
+ expect(mapped[1].contentParts).toEqual([{ type: "image", image: "https://example.com/photo.png" }]);
312
+ expect(mapped[0].author).not.toBe(author);
313
+ });
314
+ });
@@ -11,6 +11,7 @@ import type {
11
11
  AgentMessageMetadata,
12
12
  AgentWidgetMessage,
13
13
  AgentWidgetMessageRole,
14
+ AgentWidgetMessageAuthor,
14
15
  ContentPart,
15
16
  } from "../types";
16
17
  import {
@@ -24,6 +25,8 @@ import {
24
25
 
25
26
  /** One message as served by the client conversation history route. */
26
27
  export type HistoryWireMessage = {
28
+ /** Optional display provenance from a custom history adapter. */
29
+ author?: AgentWidgetMessageAuthor;
27
30
  id: string;
28
31
  role: string;
29
32
  /** Model content: string (legacy render-safe fallback) or multi-modal parts. */
@@ -61,7 +64,9 @@ type HistoryAgentMetadata = AgentMessageMetadata & {
61
64
  historyDisplayUnavailable?: true;
62
65
  };
63
66
 
64
- const WIDGET_ROLES: readonly string[] = ["user", "assistant", "system"];
67
+ // `operator` never comes from the Runtype API, but a custom history provider
68
+ // may return it for a human-takeover transcript.
69
+ const WIDGET_ROLES: readonly string[] = ["user", "assistant", "system", "operator"];
65
70
 
66
71
  /** Anchor for a page with no real timestamps and no older bound. */
67
72
  const SYNTHETIC_EPOCH_MS = 0;
@@ -187,13 +192,19 @@ export function mapWireMessages(
187
192
  if (!WIDGET_ROLES.includes(message.role)) return;
188
193
  const role = message.role as AgentWidgetMessageRole;
189
194
  const createdAt = createdAtByIndex[index];
195
+ const provenance = message.author;
196
+ const author = provenance && typeof provenance === "object" ? {
197
+ ...(typeof provenance.id === "string" && { id: provenance.id }),
198
+ ...(typeof provenance.displayName === "string" && { displayName: provenance.displayName }),
199
+ ...(["human", "ai", "system"].includes(provenance.kind ?? "") && { kind: provenance.kind }),
200
+ } : undefined;
190
201
 
191
202
  // No reconstructable visible form: render nothing, mark the gap.
192
203
  if (message.displayAvailable === false) {
193
204
  const agentMetadata: HistoryAgentMetadata = {
194
205
  historyDisplayUnavailable: true,
195
206
  };
196
- mapped.push({ id: message.id, role, content: "", createdAt, agentMetadata });
207
+ mapped.push({ id: message.id, role, ...(author && { author }), content: "", createdAt, agentMetadata });
197
208
  return;
198
209
  }
199
210
 
@@ -201,6 +212,7 @@ export function mapWireMessages(
201
212
  mapped.push({
202
213
  id: message.id,
203
214
  role,
215
+ ...(author && { author }),
204
216
  content: message.displayContent,
205
217
  createdAt,
206
218
  });
@@ -208,7 +220,7 @@ export function mapWireMessages(
208
220
  }
209
221
 
210
222
  if (typeof message.content === "string") {
211
- mapped.push({ id: message.id, role, content: message.content, createdAt });
223
+ mapped.push({ id: message.id, role, ...(author && { author }), content: message.content, createdAt });
212
224
  return;
213
225
  }
214
226
 
@@ -220,6 +232,7 @@ export function mapWireMessages(
220
232
  mapped.push({
221
233
  id: message.id,
222
234
  role,
235
+ ...(author && { author }),
223
236
  content:
224
237
  text || (hasImages(parts) ? IMAGE_ONLY_MESSAGE_FALLBACK_TEXT : ""),
225
238
  createdAt,
@@ -228,7 +241,7 @@ export function mapWireMessages(
228
241
  return;
229
242
  }
230
243
 
231
- mapped.push({ id: message.id, role, content: "", createdAt });
244
+ mapped.push({ id: message.id, role, ...(author && { author }), content: "", createdAt });
232
245
  });
233
246
 
234
247
  return mapped;
@@ -240,12 +253,17 @@ export function mapWireMessages(
240
253
  * content already IS the projection, so nothing extra needs sending.
241
254
  */
242
255
  export function divergentDisplayProjection(
243
- message: AgentWidgetMessage
256
+ message: AgentWidgetMessage,
257
+ modelContent = message.contentParts ?? message.llmContent ?? message.rawContent ?? message.content
244
258
  ): string | undefined {
245
259
  const display = message.content;
246
- if (typeof display !== "string" || display === "") return undefined;
247
- const model =
248
- message.contentParts ?? message.llmContent ?? message.rawContent ?? display;
260
+ if (typeof display !== "string") return undefined;
261
+ const originalModel = message.contentParts ?? message.llmContent ?? message.rawContent ?? display;
262
+ // Preserve legacy empty-display behavior unless a host conversion introduced
263
+ // new model-only content (for example attribution on a media-only human reply).
264
+ if (display === "" && (modelContent === originalModel ||
265
+ JSON.stringify(modelContent) === JSON.stringify(originalModel))) return undefined;
266
+ const model = modelContent;
249
267
  if (typeof model === "string" && model === display) return undefined;
250
268
  return display;
251
269
  }
@@ -10,6 +10,7 @@
10
10
  export type FingerprintableMessage = {
11
11
  id: string;
12
12
  role: string;
13
+ author?: { id?: string; displayName?: string; kind?: string };
13
14
  content: string;
14
15
  streaming?: boolean;
15
16
  dispatchError?: boolean;
@@ -65,6 +66,9 @@ export function computeMessageFingerprint(
65
66
  return [
66
67
  message.id,
67
68
  message.role,
69
+ message.author?.id ?? "",
70
+ message.author?.displayName ?? "",
71
+ message.author?.kind ?? "",
68
72
  message.content?.length ?? 0,
69
73
  message.content?.slice(-32) ?? "",
70
74
  message.streaming ? "1" : "0",
@@ -128,3 +128,16 @@ describe("isEditableUserMessage", () => {
128
128
  expect(isEditableUserMessage({ role: "user", content: "" })).toBe(false);
129
129
  });
130
130
  });
131
+
132
+ describe("operator retry boundaries", () => {
133
+ it("does not retry an assistant answer followed by a human reply", () => {
134
+ expect(findRetryableAssistantMessageId([
135
+ { id: "u", role: "user" }, { id: "a", role: "assistant" }, { id: "h", role: "operator" },
136
+ ])).toBeNull();
137
+ });
138
+ it("does not cross a human reply to locate the initiating user", () => {
139
+ const messages = [{ id: "u", role: "user" }, { id: "h", role: "operator" }, { id: "a", role: "assistant" }];
140
+ expect(findPrecedingUserMessageId(messages, "a")).toBeNull();
141
+ expect(findRetryableAssistantMessageId(messages)).toBeNull();
142
+ });
143
+ });
@@ -33,6 +33,16 @@ export function findUserTurnBoundary(
33
33
  return { start, end };
34
34
  }
35
35
 
36
+ /** Destructive resubmission must never remove a human operator contribution. */
37
+ export function canResubmitUserMessage(
38
+ messages: readonly TurnBoundaryMessage[],
39
+ messageId: string
40
+ ): boolean {
41
+ const start = messages.findIndex(message => message.id === messageId);
42
+ return start >= 0 && messages[start].role === "user" &&
43
+ !messages.slice(start + 1).some(message => message.role === "operator");
44
+ }
45
+
36
46
  /**
37
47
  * The user message that a retry of `messageId` (an assistant message) should
38
48
  * replay: the nearest preceding user message. Returns null when the assistant
@@ -45,6 +55,7 @@ export function findPrecedingUserMessageId(
45
55
  const index = messages.findIndex((message) => message.id === messageId);
46
56
  if (index <= 0) return null;
47
57
  for (let i = index - 1; i >= 0; i -= 1) {
58
+ if (messages[i].role === "operator") return null;
48
59
  if (messages[i].role === "user") return messages[i].id;
49
60
  }
50
61
  return null;
@@ -64,7 +75,7 @@ export function findRetryableAssistantMessageId(
64
75
  ): string | null {
65
76
  for (let i = messages.length - 1; i >= 0; i -= 1) {
66
77
  const message = messages[i];
67
- if (message.role === "user") return null;
78
+ if (message.role === "user" || message.role === "operator") return null;
68
79
  if (message.streaming) return null;
69
80
  if (message.role !== "assistant" || message.variant) continue;
70
81
  return findPrecedingUserMessageId(messages, message.id) ? message.id : null;
@@ -258,6 +258,11 @@ const DEFAULT_SEMANTIC_BASE: SemanticTokens = {
258
258
  },
259
259
  };
260
260
 
261
+ /** Operator bubble fill: the container tinted with the theme primary. */
262
+ const OPERATOR_BACKGROUND = 'color-mix(in srgb, var(--persona-primary) 6%, var(--persona-container))';
263
+ /** Operator bubble border: a stronger primary tint than the fill. */
264
+ const OPERATOR_BORDER = 'color-mix(in srgb, var(--persona-primary) 20%, var(--persona-container))';
265
+
261
266
  const DEFAULT_COMPONENTS_BASE: ComponentTokens = {
262
267
  button: {
263
268
  primary: {
@@ -360,6 +365,16 @@ const DEFAULT_COMPONENTS_BASE: ComponentTokens = {
360
365
  border: 'semantic.colors.border',
361
366
  shadow: 'palette.shadows.sm',
362
367
  },
368
+ operator: {
369
+ // Operator role (human takeover): a primary-tinted container, so it
370
+ // reads apart from the assistant on the same side and follows `brand`.
371
+ background: OPERATOR_BACKGROUND,
372
+ text: 'semantic.colors.text',
373
+ borderRadius: 'palette.radius.lg',
374
+ border: OPERATOR_BORDER,
375
+ borderWidth: '1px',
376
+ shadow: 'palette.shadows.sm',
377
+ },
363
378
  border: 'semantic.colors.border',
364
379
  },
365
380
  introCard: {
@@ -653,6 +668,16 @@ export const DEFAULT_COMPONENTS_V5: ComponentTokens = {
653
668
  borderRadius: '0px', shadow: 'none', padding: '0px',
654
669
  fontSize: '14px', lineHeight: '1.55',
655
670
  },
671
+ // The V5 assistant is flat prose, so the operator keeps a bubble with the
672
+ // user bubble's V5 geometry to stay visibly distinct on the same side. It
673
+ // is outlined, not filled: the stock near-black primary tints a fill to
674
+ // the user bubble's gray, so identity never depends on the primary hue.
675
+ operator: {
676
+ ...DEFAULT_COMPONENTS_BASE.message.operator,
677
+ background: 'transparent', border: 'semantic.colors.border',
678
+ borderRadius: 'palette.radius.xl', shadow: 'none', padding: '8px 14px',
679
+ fontSize: '14px', lineHeight: '1.5',
680
+ },
656
681
  },
657
682
  artifact: {
658
683
  ...DEFAULT_COMPONENTS_BASE.artifact,
@@ -1513,6 +1538,11 @@ export function themeToCssVariables(theme: PersonaTheme): Record<string, string>
1513
1538
  ['message-assistant-font-size', 'components-message-assistant-fontSize'],
1514
1539
  ['message-assistant-font-family', 'components-message-assistant-fontFamily'],
1515
1540
  ['message-assistant-line-height', 'components-message-assistant-lineHeight'],
1541
+ ['message-operator-padding', 'components-message-operator-padding'],
1542
+ ['message-operator-max-width', 'components-message-operator-maxWidth'],
1543
+ ['message-operator-font-size', 'components-message-operator-fontSize'],
1544
+ ['message-operator-font-family', 'components-message-operator-fontFamily'],
1545
+ ['message-operator-line-height', 'components-message-operator-lineHeight'],
1516
1546
  ]);
1517
1547
 
1518
1548
  // Icon button tokens
@@ -1712,6 +1742,7 @@ export const THEME_ZONES = {
1712
1742
  messages: 'Message list area',
1713
1743
  'user-message': 'User message bubble',
1714
1744
  'assistant-message': 'Assistant message bubble',
1745
+ 'operator-message': 'Operator message bubble',
1715
1746
  composer: 'Footer / composer area',
1716
1747
  container: 'Main widget container',
1717
1748
  'artifact-pane': 'Artifact sidebar',
@@ -279,6 +279,22 @@ describe("resolveConversationState", () => {
279
279
  expect(isWelcomeVisible(resolveWelcomeConfig(config({ welcome: { dismiss: "on-first-message" } })), [message])).toBe(false);
280
280
  });
281
281
 
282
+ it("treats a visible operator message as activity", () => {
283
+ expect(resolveConversationState([{ ...assistantMessage, role: "operator", content: "Sam here" }])).toBe("active");
284
+ expect(resolveConversationState([{ ...assistantMessage, role: "operator", content: " " }])).toBe("empty");
285
+ });
286
+
287
+ it("treats a media-only operator message as activity", () => {
288
+ const operator = { ...assistantMessage, role: "operator" as const, content: "" };
289
+ for (const part of [
290
+ { type: "image" as const, image: "https://example.com/receipt.png" },
291
+ { type: "file" as const, data: "https://example.com/receipt.pdf", mimeType: "application/pdf", filename: "receipt.pdf" },
292
+ ]) {
293
+ expect(resolveConversationState([{ ...operator, contentParts: [part] }])).toBe("active");
294
+ }
295
+ expect(resolveConversationState([{ ...operator, contentParts: [{ type: "text", text: " " }] }])).toBe("empty");
296
+ });
297
+
282
298
  it("flips to active on the first user message", () => {
283
299
  expect(resolveConversationState([userMessage])).toBe("active");
284
300
  expect(resolveConversationState([assistantMessage, userMessage])).toBe(
package/src/welcome.ts CHANGED
@@ -181,14 +181,22 @@ export const resolveWelcomeConfig = (
181
181
  * system context do not. Configured `welcome.message` is separate UI chrome,
182
182
  * so an intentional greeting never activates the conversation by itself.
183
183
  */
184
+ const hasVisibleContent = (message: AgentWidgetMessage): boolean =>
185
+ Boolean(
186
+ message.content.trim() ||
187
+ message.contentParts?.some((part) => part.type !== "text" || part.text.trim())
188
+ );
189
+
184
190
  const hasConversationActivity = (
185
191
  messages: readonly AgentWidgetMessage[] = []
186
192
  ): boolean =>
187
193
  messages.some((message) =>
188
194
  message.role === "user" ||
195
+ // A human operator speaking is conversation, even before any agent turn,
196
+ // including a media-only message (an image, file, audio or video part).
197
+ (message.role === "operator" && hasVisibleContent(message)) ||
189
198
  (message.role === "assistant" && (
190
- message.content.trim() ||
191
- message.contentParts?.some((part) => part.type !== "text" || part.text.trim()) ||
199
+ hasVisibleContent(message) ||
192
200
  message.toolCall || message.tools?.length || message.reasoning || message.approval
193
201
  ))
194
202
  );
@@ -1 +0,0 @@
1
- const e=`4.32.1`;export{e as t};
@@ -1 +0,0 @@
1
- import{a as e,i as t,l as n,r,s as i}from"./chunk-BlTD-Wgn.js";const a=[`user`,`assistant`,`system`];function o(e){if(typeof e!=`string`||e===``)return null;let t=Date.parse(e);return Number.isNaN(t)?null:t}function s(e){return typeof e==`string`?e:void 0}function c(n){if(typeof n!=`object`||!n)return null;let i=n;switch(i.type){case`text`:return typeof i.text==`string`?e(i.text):null;case`image`:return typeof i.image==`string`?t(i.image,{mimeType:s(i.mimeType),alt:s(i.alt)}):null;case`file`:return typeof i.data==`string`&&typeof i.mimeType==`string`&&typeof i.filename==`string`?r(i.data,i.mimeType,i.filename):null;case`audio`:return typeof i.audio==`string`?{type:`audio`,audio:i.audio,...s(i.mimeType)!==void 0&&{mimeType:i.mimeType}}:null;case`video`:return typeof i.video==`string`?{type:`video`,video:i.video,...s(i.mimeType)!==void 0&&{mimeType:i.mimeType}}:null;default:return null}}function l(e,t){let n=o(t),r=e.map(e=>o(e.timestamp)),i=Array(e.length),a=0;for(;a<r.length;){let e=r[a];if(e!==null){i[a]=e,a+=1;continue}let t=a;for(;t+1<r.length&&r[t+1]===null;)t+=1;let o=t-a+1,s=a>0?i[a-1]:null,c=t+1<r.length?r[t+1]:null,l=c!==null&&n!==null?Math.min(c,n):c??n;for(let e=0;e<o;e+=1)i[a+e]=l===null?s===null?0+e:s+e+1:l-(o-e);a=t+1}return i.map(e=>new Date(e).toISOString())}function u(e,t){let r=l(e,t?.beforeCreatedAt),o=[];return e.forEach((e,t)=>{if(typeof e?.id!=`string`||e.id===``||!a.includes(e.role))return;let s=e.role,l=r[t];if(e.displayAvailable===!1)o.push({id:e.id,role:s,content:``,createdAt:l,agentMetadata:{historyDisplayUnavailable:!0}});else if(typeof e.displayContent==`string`)o.push({id:e.id,role:s,content:e.displayContent,createdAt:l});else if(typeof e.content==`string`)o.push({id:e.id,role:s,content:e.content,createdAt:l});else if(Array.isArray(e.content)){let t=e.content.map(c).filter(e=>e!==null),r=i(t);o.push({id:e.id,role:s,content:r||(n(t)?`[Image]`:``),createdAt:l,...t.length>0&&{contentParts:t}})}else o.push({id:e.id,role:s,content:``,createdAt:l})}),o}function d(e){let t=e.content;if(typeof t!=`string`||t===``)return;let n=e.contentParts??e.llmContent??e.rawContent??t;if(typeof n!=`string`||n!==t)return t}function f(e,t){let n=/* @__PURE__ */ new Map;for(let t of e)n.set(t.id,t);for(let e of t)n.set(e.id,e);return[...n.values()].sort((e,t)=>{let n=new Date(e.createdAt).getTime(),r=new Date(t.createdAt).getTime();if(!Number.isNaN(n)&&!Number.isNaN(r)&&n!==r)return n-r;let i=e.sequence??0,a=t.sequence??0;return i===a?e.id.localeCompare(t.id):i-a})}export{u as n,f as r,d as t};
@@ -1 +0,0 @@
1
- const e=Symbol.for(`persona.defaultsProvenance`),t=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function n(e,r=``){return Object.entries(e).flatMap(([e,i])=>{let a=r?`${r}.${e}`:e;return t(i)?n(i,a):[a]})}function r(e,n){let r=n.split(`.`);for(let n=0;n<r.length;n++){let i=r[n];if(!t(e)||!Object.prototype.hasOwnProperty.call(e,i))return`absent`;if(e=e[i],e===void 0||n<r.length-1&&!t(e))return`clear`}return`explicit`}function i(t,n){return t?.[e]??n.filter(e=>r(t,e)===`explicit`)}function a(t,n){return Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0}),t}function o(e,t,r){return a(e,i(t,n(r)))}function s(t,n,a){let o=n[e];if(o)return o;let s=new Set(i(t,a));for(let e of a){let t=r(n,e);t===`clear`&&s.delete(e),t===`explicit`&&s.add(e)}return[...s]}function c(e,n,r){let i={...e};for(let e of n){if(r.includes(e))continue;let n=e.split(`.`),a=i;for(let e of n.slice(0,-1)){let n=a[e];if(!t(n)){a=void 0;break}a[e]={...n},a=a[e]}a&&delete a[n[n.length-1]]}return i}const l=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function u(e,t){if(!e)return t;if(!t)return e;let n={...e};for(let[e,r]of Object.entries(t)){let t=n[e];n[e]=l(t)&&l(r)?u(t,r):r}return n}const d=e=>{if(typeof e!=`string`)return;let t=e.trim(),n=/^#([\da-f]{3}|[\da-f]{6})$/i.exec(t)?.[1];if(n){let e=n.length===3?[...n].map(e=>e+e).join(``):n;return[0,2,4].map(t=>parseInt(e.slice(t,t+2),16))}let r=/^rgba?\(\s*([^)]*)\)$/i.exec(t);if(!r)return;let i=r[1].trim().split(/\s*[,/]\s*|\s+/);if(i.length===4&&/^(?:1(?:\.0+)?|100%)$/.test(i[3])&&i.pop(),i.length!==3)return;let a=i.map(e=>/^\d+(?:\.\d+)?%?$/.test(e)?Number.parseFloat(e)*(e.endsWith(`%`)?2.55:1):NaN);if(!a.some(e=>!Number.isFinite(e)||e<0||e>255))return a},f=e=>e.reduce((e,t,n)=>{let r=t/255;return e+(r<=.04045?r/12.92:((r+.055)/1.055)**2.4)*[.2126,.7152,.0722][n]},0),p=e=>`#`+e.map(e=>Math.round(e).toString(16).padStart(2,`0`)).join(``),m=(e,t,n)=>p(e.map(e=>e+(t-e)*n)),h=e=>f(e)>.179?`#000000`:`#ffffff`,g=e=>{let t=d(e?.neutral);return t?f(t)<.18?`dark`:`light`:void 0},_=(e,t=`light`,n,r=!1)=>{let i={};if(!e)return i;let a=d(e.accent);if(a){let e={};for(let[t,n]of[[50,.96],[100,.9],[200,.75],[300,.55],[400,.25]])e[t]=m(a,255,n);e[500]=p(a);for(let[t,n]of[[600,.15],[700,.3],[800,.45],[900,.6],[950,.75]])e[t]=m(a,0,n);let t=h(a);i.palette={colors:{primary:e,accent:e}},i.semantic={colors:{primary:e[500],accent:e[500],interactive:{default:e[500],hover:e[600],focus:e[500],active:e[700]}}},i.components={button:{primary:{background:e[500],foreground:t}},launcher:{background:e[500],foreground:t},header:r?{background:`semantic.colors.container`,foreground:`semantic.colors.text`,iconBackground:`transparent`,iconForeground:`semantic.colors.text`}:{background:e[500],foreground:t,iconBackground:e[500],iconForeground:t},approval:{approve:{foreground:t}},message:{user:r?{background:`color-mix(in srgb, var(--persona-primary) 12%, var(--persona-container))`,text:`semantic.colors.text`}:{background:e[500],text:t}}}}let o=d(e.neutral);if(o){let e=t===`dark`,n=e===f(o)<.18?o:d(m(o,e?0:255,.9)),r=p(n),a=m(n,e?255:0,e?.06:.025),s=m(n,e?255:0,e?.2:.16),c=h(n),l={};for(let[e,t]of[[50,0],[100,.06],[200,.16],[300,.28],[400,.45],[500,.65],[600,.72],[700,.8],[800,.9],[900,.97],[950,1]])l[e]=m(n,c===`#ffffff`?255:0,t);i.palette={...i.palette,colors:{...i.palette?.colors,gray:l}},i.semantic={...i.semantic,colors:{...i.semantic?.colors,surface:r,background:r,container:a,border:s,divider:s,text:c,textMuted:m(n,c===`#ffffff`?255:0,.65)}},i.components={...i.components,panel:{background:r},header:{...i.components?.header,background:r,foreground:c},composer:{footerBackground:r}}}if(a){let e=d(i.semantic?.colors?.surface)??(t===`dark`?[15,15,16]:[255,255,255]),n=p(a),r=h(e)===`#ffffff`?255:0;for(let t=0;t<=1;t+=.1){n=m(a,r,t);let i=f(d(n)),o=f(e);if((Math.max(i,o)+.05)/(Math.min(i,o)+.05)>=4.5)break}i.components={...i.components,markdown:{link:{foreground:n}},input:{focus:{border:n,ring:n}}}}e.font?.trim()&&(i.semantic={...i.semantic,typography:{fontFamily:e.font}});let s={none:`0px`,sm:`4px`,md:`8px`,lg:`16px`}[e.radius??`none`];e.radius&&s&&(i.palette={...i.palette,radius:{none:`0px`,sm:s,md:s,lg:s,xl:s,"2xl":s}},i.components={...i.components,input:{...i.components?.input,borderRadius:s},header:{...i.components?.header,controlBorderRadius:s}});let c={compact:[13,44,8],comfortable:[14,48,12],spacious:[15,52,16]}[e.density??`comfortable`];if(e.density&&c){let[e,t,n]=c;i.semantic={...i.semantic,typography:{...i.semantic?.typography,fontSize:`${e}px`}},i.components={...i.components,header:{...i.components?.header,minHeight:`${t}px`,padding:`${n}px 16px`,minimalPadding:`${n}px 16px`},history:{railHeader:{minHeight:`${t}px`}},composer:{...i.components?.composer,fontSize:`${e}px`,padding:`${n}px`,gap:`${n/2}px`},message:{...i.components?.message,gap:`${n}px`,user:{...i.components?.message?.user,fontSize:`${e}px`},assistant:{fontSize:`${e}px`}}}}if(e.elevation&&[`flat`,`subtle`,`raised`].includes(e.elevation)){let t=e.elevation===`flat`,a=t?`none`:e.elevation===`subtle`?`0 2px 8px rgba(0, 0, 0, 0.08)`:`0 12px 32px rgba(0, 0, 0, 0.18)`,o=t?`1px solid var(--persona-border)`:`1px solid color-mix(in srgb, var(--persona-border) 65%, transparent)`;i.palette={...i.palette,shadows:{sm:a,md:a,lg:a,xl:a,"2xl":a}};let s=(e,n=``)=>{let i={};if(r&&n===`message.assistant`)return i;for(let[r,c]of Object.entries(e))if(r.toLowerCase().endsWith(`shadow`)){i[r]=a;let n=r===`shadow`?`border`:r.replace(/Shadow$/,`Border`);n in e&&(i[n]=typeof e[n]==`string`&&e[n].includes(`solid`)?o:t?`semantic.colors.border`:`color-mix(in srgb, var(--persona-border) 65%, transparent)`)}else if(c&&typeof c==`object`){let e=s(c,n?`${n}.${r}`:r);Object.keys(e).length&&(i[r]=e)}return i},c=n?s(n):{};i.components=u(i.components,c),i.components={...i.components,panel:{...i.components?.panel,shadow:a,modes:{floating:{shadow:a,border:o},inline:{shadow:a,border:o},docked:{shadow:a,border:o},sidebar:{shadow:a,border:o},mobile:{shadow:a,border:o}}},launcher:{...i.components?.launcher,shadow:a,border:`semantic.colors.border`},composer:{...i.components?.composer,shadow:a,borderColor:`semantic.colors.border`},toolBubble:{shadow:a},reasoningBubble:{shadow:a}}}return i},v=(e,t)=>t.split(` `).map(t=>`${e}.${t}`),y=[...v(`launcher`,`closeButtonSize closeButtonColor closeButtonBackgroundColor closeButtonBorderWidth closeButtonBorderColor closeButtonBorderRadius closeButtonPaddingX closeButtonPaddingY`),...v(`launcher.clearChat`,`size iconColor backgroundColor borderWidth borderColor borderRadius paddingX paddingY`),...v(`sendButton`,`size iconSize iconStrokeWidth backgroundColor textColor borderWidth borderColor paddingX paddingY`),...v(`voiceRecognition`,`iconSize iconColor backgroundColor borderColor borderWidth paddingX paddingY recordingIconColor recordingBackgroundColor recordingBorderColor processingIconColor processingBackgroundColor processingBorderColor speakingIconColor speakingBackgroundColor speakingBorderColor`),...v(`toolCall`,`shadow backgroundColor borderColor borderWidth borderRadius headerBackgroundColor headerTextColor headerPaddingX headerPaddingY contentBackgroundColor contentTextColor contentPaddingX contentPaddingY codeBlockBackgroundColor codeBlockBorderColor codeBlockTextColor toggleTextColor labelTextColor loadingAnimationColor loadingAnimationSecondaryColor loadingAnimationDuration`),...v(`approval`,`backgroundColor borderColor shadow titleColor descriptionColor reasonColor approveButtonColor approveButtonTextColor denyButtonColor denyButtonTextColor parameterBackgroundColor parameterTextColor`),...v(`features.askUserQuestion.styles`,`sheetBackground sheetBorder sheetShadow pillBackground pillBackgroundSelected pillTextColor pillTextColorSelected pillBorderRadius customInputBackground`),...v(`features.artifacts.layout`,`paneBackground panePadding paneBorderRadius paneShadow chatShadow paneBorder paneBorderLeft unifiedSplitOuterRadius documentToolbarIconColor documentToolbarToggleActiveBackground documentToolbarToggleActiveBorderColor documentToolbarToggleActiveColor tabFadeSize`),...v(`suggestionChipsConfig`,`fontWeight paddingX paddingY`)],b=[`voice.recording.border`,`voice.processing.icon`,`voice.processing.background`,`voice.speaking.icon`,`approval.requested.text`,`approval.approve.foreground`,`approval.deny.foreground`,`approval.deny.background`,`reasoningBubble.shadow`,`toolBubble.shadow`],x=[...y,`suggestionChips`,`copy.welcomeTitle`,`copy.welcomeSubtitle`,`copy.showWelcomeCard`,`features.suggestReplies.enabled`,`features.suggestReplies.expose`,...[`theme`,`darkTheme`].flatMap(e=>b.map(t=>`${e}.components.${t}`))],S=Symbol.for(`persona.styleProvenance`),C=e=>e[S]!==void 0,w=(e,t)=>{for(let n of t.split(`.`)){if(typeof e!=`object`||!e)return;e=e[n]}return e},T=e=>{let t=e?.[S];if(!t)return x.filter(t=>r(e,t)===`explicit`);let n=new Set(t.explicit);return x.forEach(i=>{w(e,i)!==t.values[i]&&(r(e,i)===`explicit`?n.add(i):n.delete(i))}),[...n]},E=(e,t)=>(Object.defineProperty(e,S,{value:{explicit:t,values:Object.fromEntries(x.map(t=>[t,w(e,t)]))},enumerable:!0,configurable:!0}),e),D=(e,t)=>E(e,T(t)),O=(e,t)=>{if(t[S])return T(t);let n=new Set(T(e));for(let e of x){let i=r(t,e);i===`clear`&&n.delete(e),i===`explicit`&&n.add(e)}return[...n]},k=(e,t)=>{let n=e?.[S];return!n||!Object.prototype.hasOwnProperty.call(n.values,t)||w(e,t)!==n.values[t]?r(e,t)===`explicit`:n.explicit.includes(t)},A=(e,t)=>k(e,t)?w(e,t):void 0,j=(e,t,n)=>{let r=e,i=n??r?.colorScheme??g(r?.brand)??`light`,a=i===`dark`||i===`auto`&&(typeof document<`u`&&document.documentElement.classList.contains(`dark`)||typeof window<`u`&&window.matchMedia?.(`(prefers-color-scheme: dark)`).matches);return k(e,`theme.components.${t}`)||a&&k(e,`darkTheme.components.${t}`)};export{r as _,j as a,E as c,g as d,u as f,a as g,c as h,k as i,O as l,o as m,b as n,C as o,i as p,A as r,D as s,y as t,_ as u,s as v,n as y};
@@ -1 +0,0 @@
1
- import{n as e}from"./chunk-BAdIgC5d.js";import{t}from"./chunk-BkOYJ11B.js";import{t as n}from"./chunk-Bz9jAgba.js";import{n as r}from"./chunk-DTDHeUsG.js";const i=e=>e.replace(/[^a-zA-Z0-9_]/g,`_`).toLowerCase().replace(/_+/g,`_`).replace(/^_|_$/g,``),a=e=>typeof e==`object`&&!!e&&!Array.isArray(e),o=e=>{if(e.variant!==`tool`)return null;let t=e.agentMetadata?.clientToolAnswer;if(!t||!t.toolCallId||t.result===void 0)return null;let n=i(t.toolName);return n?{call:{toolCallId:t.toolCallId,toolName:n,args:a(t.args)?t.args:{}},result:{toolCallId:t.toolCallId,toolName:n,result:t.result}}:null},s=(e,t)=>{let n=[],r=[],i=[],a=``,s,c=()=>{r.length!==0&&(n.push({role:`assistant`,content:``,createdAt:a,toolCalls:r},{role:`tool`,content:``,createdAt:a,toolResults:i}),r=[],i=[])};for(let l of e){let e=o(l);if(e){let t=l.agentMetadata?.clientToolAnswer?.batch;r.length>0&&(t===void 0||t!==s)&&c(),r.length===0&&(a=l.createdAt,s=t),r.some(t=>t.toolCallId===e.call.toolCallId)||(r.push(e.call),i.push(e.result));continue}let u=t(l);u&&(c(),n.push(u))}return c(),n},c=e=>e.voiceCaption||r(e)?!1:!!(e.contentParts&&e.contentParts.length>0||e.llmContent&&e.llmContent.trim().length>0||e.rawContent&&e.rawContent.trim().length>0||e.content&&e.content.trim().length>0),l=e=>e.slice().sort((e,t)=>new Date(e.createdAt).getTime()-new Date(t.createdAt).getTime()),u=e=>{let t=e.agentMetadata?.providerPhase??e.agentMetadata?.phase;return e.role===`assistant`&&(t===`commentary`||t===`partial_answer`||t===`final_answer`)?{phase:t}:{}},d=e=>({role:e.role,content:e.contentParts??e.llmContent??e.rawContent??e.content,createdAt:e.createdAt,...u(e)});var f=class{constructor(e){this.host=e}async initializeSession(){await this.host.historyInternals.historyBootstrapReady;let e=this.host.config.getStoredConversationId?.()||null,t=this.host.historyInternals.shouldResumeDurableConversation?.()===!0;if(!this.host.isHistoryCapable()&&!t)return this.#e(e,!1);let n=await this.host.readVisitorToken();if(e&&n)try{let n=await this.createClientSession({conversationId:e,durableResume:t});return t&&n.durableRecovery?.enabled!==!0&&this.host.historyInternals.setStoredResumableHandle?.(null),this.host.finishInit(n,e,!1)}catch(n){if(!(n instanceof this.host.HistoryClientError&&n.code===`not_found`)&&!(n instanceof this.host.HistoryClientError&&n.code===`visitor_required`)&&!(n instanceof this.host.HistoryClientError&&n.code===`history_disabled`))throw n;return t&&this.host.historyInternals.setStoredResumableHandle?.(null),this.#e(e,!0)}return this.#e(e,!1)}async#e(e,t){let n=this.host.historyInternals.visitorStore;return n&&!await this.host.readVisitorToken()?n.withFirstInitLock(()=>this.#t(e,t)):this.#t(e,t)}async#t(e,t){let n=this.host.config.getStoredSessionId?.()||null,r=await this.createClientSession({storedSessionId:n}),i=await this.#n(r);return this.host.finishInit(i,e,t)}async#n(e){if(!e.visitor?.token||this.host.claimInFlight||!this.host.isHistoryCapable()&&e.durableRecovery?.enabled!==!0)return e;this.host.claimInFlight=!0;try{return await this.createClientSession({storedSessionId:e.sessionId})}catch{return e}finally{this.host.claimInFlight=!1}}async sendWithClientToolsDiff(t,n,r,i){let a=!!(n&&n.length>0),o=a?e(n):void 0,s=this.host.clientToolsFingerprintSessionId===t,c=a&&s&&this.host.lastSentClientToolsFingerprint===o,l=!a&&i?.emptyMeansReplace===!0&&this.host.sentNonEmptyClientToolsSessionId===t,u=!1,d;for(let e=0;d=await r({...a&&(u||!c)&&n?{clientTools:n}:{},...l?{clientTools:[]}:{},...o?{clientToolsFingerprint:o}:{}}),d.status===409&&e===0&&a&&(await d.clone().json().catch(()=>null))?.error===`client_tools_resend_required`;e++)u=!0,this.host.lastSentClientToolsFingerprint=null;return{response:d,commit:()=>{this.host.lastSentClientToolsFingerprint=o??null,this.host.clientToolsFingerprintSessionId=t,a?this.host.sentNonEmptyClientToolsSessionId=t:l&&(this.host.sentNonEmptyClientToolsSessionId=null)}}}async createClientSession(e){let n=this.host.isHistoryCapable()&&!e.omitVisitorFields,r=!e.omitVisitorFields,i=null;(n||r)&&(await this.host.historyInternals.historyBootstrapReady,i=await this.host.readVisitorToken());let a=this.host.routing(),o=a.agentId??a.flowId,s=n||e.durableResume?e.conversationId:void 0,c={token:this.host.config.clientToken,...o&&{flowId:o},...n&&{visitorHistory:!0},...r&&{durableRecovery:!0},...(n||r)&&i?{visitorToken:i}:{},...n&&e.identityProof?{identityProof:e.identityProof}:{},...s?{conversationId:s}:e.storedSessionId?{sessionId:e.storedSessionId}:{}};e.signal?.throwIfAborted();let l=await fetch(this.host.getClientApiUrl(`init`),{method:`POST`,headers:{"Content-Type":`application/json`,"X-Persona-Version":t},body:JSON.stringify(c),signal:e.signal});if(!l.ok){let t=await l.json().catch(()=>({error:`Session initialization failed`}));if(n&&l.status===403&&t.error===`visitor_history_disabled`){if(this.host.markHistoryUnavailable(),s)throw new this.host.HistoryClientError(`history_disabled`,`Visitor history is disabled for this surface`);return this.createClientSession({...e,omitVisitorFields:!0})}throw l.status===403&&t.error===`durable_recovery_disabled`?new this.host.HistoryClientError(`history_disabled`,`Durable recovery is disabled for this surface`):(n||e.durableResume)&&l.status===401&&t.error===`visitor_required`?new this.host.HistoryClientError(`visitor_required`,`Visitor credential no longer resolves`):s&&l.status===404?new this.host.HistoryClientError(`not_found`,`Conversation not found`):l.status===401?Error(`Invalid client token: ${t.hint||t.error}`):l.status===403?Error(`Origin not allowed: ${t.hint||t.error}`):Error(t.error||`Failed to initialize session`)}let u=await l.json();return u.visitor?.token&&await this.host.historyInternals.visitorStore?.set(u.visitor.token),{sessionId:u.sessionId,expiresAt:new Date(u.expiresAt),flow:u.flow,...u.conversationId?{conversationId:u.conversationId}:{},...u.targetId?{targetId:u.targetId}:{},...u.conversationRevision?{conversationRevision:u.conversationRevision}:{},...u.durableRecovery?{durableRecovery:u.durableRecovery}:{},...u.visitor?{visitor:u.visitor}:{},config:{welcomeMessage:u.config.welcomeMessage,placeholder:u.config.placeholder,theme:u.config.theme}}}async sendFeedback(e){if([`upvote`,`downvote`,`copy`].includes(e.type)&&!e.messageId)throw Error(`messageId is required for ${e.type} feedback type`);if(e.type===`csat`&&(e.rating===void 0||e.rating<1||e.rating>5))throw Error(`CSAT rating must be between 1 and 5`);if(e.type===`nps`&&(e.rating===void 0||e.rating<0||e.rating>10))throw Error(`NPS rating must be between 0 and 10`);this.host.debug&&console.debug(`[AgentWidgetClient] sending feedback`,e);let n={...e,...this.host.config.clientToken&&{token:this.host.config.clientToken}},r=await fetch(`${this.host.clientApiBase()}/v1/client/feedback`,{method:`POST`,headers:{"Content-Type":`application/json`,"X-Persona-Version":t},body:JSON.stringify(n)});if(!r.ok){let e=await r.json().catch(()=>({error:`Feedback submission failed`}));throw r.status===401?(this.host.clientSession=null,this.host.resetSessionWarmLatch(),this.host.config.onSessionExpired?.(),Error(`Session expired. Please refresh to continue.`)):Error(e.error||`Failed to submit feedback`)}}async dispatchClientToken(e,r,i){let{HistoryClientError:a}=this.host,o=()=>this.host.isCurrentTurn(i),s=e=>{o()&&r(e)};try{let r=()=>{if(e.signal?.throwIfAborted(),!o())throw new DOMException(`Turn superseded`,`AbortError`)},l=this.host.clientSession??await this.host.initSession();r();let u=async()=>{r();let t=l,n=t.durableRecovery?.enabled===!0;if(n&&(!t.conversationId||!await this.host.readVisitorToken()))throw Error(`Renewing this conversation requires its visitor credential.`);r();let i=await this.host.createClientSession({...n||this.host.isHistoryCapable()?{conversationId:t.conversationId,durableResume:n}:{storedSessionId:t.sessionId},signal:e.signal});if(r(),t.conversationId&&i.conversationId!==t.conversationId||n&&i.durableRecovery?.enabled!==!0)throw Error(`Session renewal did not preserve this conversation.`);l=this.host.finishInit(i,t.conversationId??null,!1),this.host.clientSession=l,this.host.resetSessionWarmLatch(),this.host.resetClientToolsFingerprint(),this.host.config.onSessionInit?.(l)};Date.now()>=l.expiresAt.getTime()-6e4&&await u();let d=await this.#c(e.messages),f=d.metadata?Object.fromEntries(Object.entries(d.metadata).filter(([e])=>e!==`sessionId`&&e!==`session_id`)):void 0,p=this.host.isHistoryCapable(),m={sessionId:l.sessionId,messages:e.messages.filter(c).map(e=>{let t=p?n(e):void 0;return{id:e.id,role:e.role,content:e.contentParts??e.llmContent??e.rawContent??e.content,...t!==void 0&&{displayContent:t}}}),...e.assistantMessageId&&{assistantMessageId:e.assistantMessageId},...f&&Object.keys(f).length>0&&{metadata:f},...d.inputs&&Object.keys(d.inputs).length>0&&{inputs:d.inputs},...d.context&&{context:d.context},turnId:i,...e.interrupt&&{submitMode:`interrupt`},...e.voiceSpoken&&{voice:{spoken:!0,...e.voicePriorToolResults?.length&&{priorToolResults:e.voicePriorToolResults}}}},h=async()=>{r();let n=l.durableRecovery?.enabled===!0?await this.host.readVisitorToken():null;return r(),this.host.sendWithClientToolsDiff(l.sessionId,d.clientTools,async i=>{r();let a=await this.host.resolveChatIdentityProof(e.signal);r();let o={...m,sessionId:l.sessionId,...i,...a&&{identityProof:a}};return this.host.debug&&console.debug(`[AgentWidgetClient] client token dispatch`,{...o,...a&&{identityProof:{provider:a.provider,token:`[REDACTED]`}}}),fetch(this.host.getClientApiUrl(`chat`),{method:`POST`,headers:{"Content-Type":`application/json`,"X-Persona-Version":t,...n?{"X-Visitor-Token":n}:{}},body:JSON.stringify(o),signal:e.signal})})},g=await h();if(r(),g.response.status===401){let e=await g.response.clone().json().catch(()=>null);r(),e?.error===`Session not found or expired`&&(await u(),g=await h(),r())}let{response:_,commit:v}=g;if(!_.ok){let e=await _.json().catch(()=>({error:`Chat request failed`}));if(_.status===401&&e.error===`invalid_identity_proof`){let e=new a(`invalid_identity_proof`,`The identity proof was rejected`);throw s({type:`error`,error:e}),e}if(_.status===401){this.host.clearClientSession(),this.host.config.onSessionExpired?.();let e=/* @__PURE__ */ Error(`Session expired. Please refresh to continue.`);throw s({type:`error`,error:e}),e}if(_.status===429){let t=Error(e.hint||`Message limit reached for this session.`);throw s({type:`error`,error:t}),t}if(_.status===410&&e.error===`conversation_deleted`){let e=new a(`conversation_deleted`,`This conversation was deleted`);throw s({type:`error`,error:e}),e}let t=Error(e.error||`Failed to send message`);throw s({type:`error`,error:t}),t}if(!_.body){let e=/* @__PURE__ */ Error(`No response body received`);throw s({type:`error`,error:e}),e}v(),s({type:`status`,status:`connected`});try{await this.host.streamResponse(_.body,s,e.assistantMessageId,void 0,e.signal)}finally{s({type:`status`,status:`idle`})}}catch(e){let t=e instanceof Error?e:Error(String(e));throw!(t instanceof a)&&!t.message.includes(`Session expired`)&&!t.message.includes(`Message limit`)&&s({type:`error`,error:t}),t}}async dispatchProxy(e,t){let n=await this.#c(e.messages,e.composerOptions);this.host.debug&&console.debug(`[AgentWidgetClient] dispatch payload`,n);let r={...this.host.headers};if(this.host.getHeaders)try{let e=await this.host.getHeaders();r={...r,...e}}catch(e){typeof console<`u`&&console.error(`[AgentWidget] getHeaders error:`,e)}let i;if(this.host.customFetch)try{i=await this.host.customFetch(this.host.apiUrl,{method:`POST`,headers:r,body:JSON.stringify(n),signal:e.signal},n)}catch(e){let n=e instanceof Error?e:Error(String(e));throw t({type:`error`,error:n}),n}else i=await fetch(this.host.apiUrl,{method:`POST`,headers:r,body:JSON.stringify(n),signal:e.signal});if(!i.ok||!i.body){let e=/* @__PURE__ */ Error(`Chat backend request failed: ${i.status} ${i.statusText}`);throw t({type:`error`,error:e}),e}t({type:`status`,status:`connected`});try{await this.host.streamResponse(i.body,t,void 0,void 0,e.signal)}finally{t({type:`status`,status:`idle`})}}async dispatchAgent(e,t){let n=await this.#s(e.messages,e.composerOptions);this.host.debug&&console.debug(`[AgentWidgetClient] agent dispatch payload`,n);let r={...this.host.headers};if(this.host.getHeaders)try{let e=await this.host.getHeaders();r={...r,...e}}catch(e){typeof console<`u`&&console.error(`[AgentWidget] getHeaders error:`,e)}let i;if(this.host.customFetch)try{i=await this.host.customFetch(this.host.apiUrl,{method:`POST`,headers:r,body:JSON.stringify(n),signal:e.signal},n)}catch(e){let n=e instanceof Error?e:Error(String(e));throw t({type:`error`,error:n}),n}else i=await fetch(this.host.apiUrl,{method:`POST`,headers:r,body:JSON.stringify(n),signal:e.signal});if(!i.ok||!i.body){let e=/* @__PURE__ */ Error(`Agent execution request failed: ${i.status} ${i.statusText}`);throw t({type:`error`,error:e}),e}t({type:`status`,status:`connected`});try{await this.host.streamResponse(i.body,t,e.assistantMessageId,void 0,e.signal)}finally{t({type:`status`,status:`idle`})}}#r(e){for(let t=e.length-1;t>=0;t--){let n=e[t];if(n.role===`user`)return n.mentionContext&&Object.keys(n.mentionContext).length>0?{mentions:n.mentionContext}:null}return null}async#i(e){let t={};this.host.contextProviders.length&&await Promise.all(this.host.contextProviders.map(async n=>{try{let r=await n({messages:e,config:this.host.config});r&&typeof r==`object`&&Object.assign(t,r)}catch(e){typeof console<`u`&&console.warn(`[AgentWidget] Context provider failed:`,e)}}));let n=this.#r(e);return n&&Object.assign(t,n),Object.keys(t).length?t:null}#a(e){if(!e)return;let t={};return e.selectedModelId&&(t.selectedModelId=e.selectedModelId),e.activeModeIds?.length&&(t.activeModeIds=[...e.activeModeIds]),Object.keys(t).length>0?t:void 0}#o(e,t){let n=t?.selectedModelId;return!n||!(`model`in e)?e:this.host.config.composer?.models?.some(e=>e.id===n)?{...e,model:n}:e}async#s(e,t){let n=this.host.routing().agentId;if(!this.host.config.agent&&!n)throw Error(`Agent configuration required for agent mode`);let r=s(l(e),e=>c(e)&&(e.role===`user`||e.role===`assistant`||e.role===`system`)&&(!e.variant||e.variant===`assistant`)?d(e):null),i=this.#a(t),a={agent:this.#o(this.host.config.agent??{agentId:n},i),messages:r,options:{streamResponse:!0,recordMode:`virtual`,...this.host.config.agentOptions}};i&&(a.composerOptions=i);let o=[...this.host.builtInClientTools(this.host.config),...await(await this.host.getWebMcpBridge())?.snapshotForDispatch()??[]];o.length>0&&(a.clientTools=o);let u=await this.#i(e);return u&&(a.context=u),a}async#c(e,t){let n=s(l(e),e=>c(e)?d(e):null),r=this.host.routing(),i={messages:n,...r.agentId?{agent:{agentId:r.agentId}}:r.flowId?{flowId:r.flowId}:{}};if(r.targetPayload)for(let[e,t]of Object.entries(r.targetPayload))e!==`messages`&&(i[e]=t);let a=[...this.host.builtInClientTools(this.host.config),...await(await this.host.getWebMcpBridge())?.snapshotForDispatch()??[]];a.length>0&&(i.clientTools=a);let o=await this.#i(e);o&&(i.context=o);let u=this.#a(t);if(u&&(i.composerOptions=u),this.host.requestMiddleware)try{let e=await this.host.requestMiddleware({payload:{...i},config:this.host.config});if(e&&typeof e==`object`){let t=e;return i.clientTools!==void 0&&!(`clientTools`in t)&&(t.clientTools=i.clientTools),t}}catch(e){typeof console<`u`&&console.error(`[AgentWidget] Request middleware error:`,e)}return i}};export{f as ClientDispatch};