@runtypelabs/persona 1.48.0 → 2.0.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 (69) hide show
  1. package/README.md +140 -8
  2. package/dist/index.cjs +90 -39
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1055 -24
  5. package/dist/index.d.ts +1055 -24
  6. package/dist/index.global.js +111 -60
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +90 -39
  9. package/dist/index.js.map +1 -1
  10. package/dist/install.global.js +1 -1
  11. package/dist/install.global.js.map +1 -1
  12. package/dist/widget.css +836 -513
  13. package/package.json +1 -1
  14. package/src/artifacts-session.test.ts +80 -0
  15. package/src/client.test.ts +20 -21
  16. package/src/client.ts +153 -4
  17. package/src/components/approval-bubble.ts +45 -42
  18. package/src/components/artifact-card.ts +91 -0
  19. package/src/components/artifact-pane.ts +501 -0
  20. package/src/components/composer-builder.ts +32 -27
  21. package/src/components/event-stream-view.ts +40 -40
  22. package/src/components/feedback.ts +36 -36
  23. package/src/components/forms.ts +11 -11
  24. package/src/components/header-builder.test.ts +32 -0
  25. package/src/components/header-builder.ts +55 -36
  26. package/src/components/header-layouts.ts +58 -125
  27. package/src/components/launcher.ts +36 -21
  28. package/src/components/message-bubble.ts +92 -65
  29. package/src/components/messages.ts +2 -2
  30. package/src/components/panel.ts +42 -11
  31. package/src/components/reasoning-bubble.ts +23 -23
  32. package/src/components/registry.ts +4 -0
  33. package/src/components/suggestions.ts +1 -1
  34. package/src/components/tool-bubble.ts +32 -32
  35. package/src/defaults.ts +30 -4
  36. package/src/index.ts +80 -2
  37. package/src/install.ts +22 -0
  38. package/src/plugins/types.ts +23 -0
  39. package/src/postprocessors.ts +2 -2
  40. package/src/runtime/host-layout.ts +174 -0
  41. package/src/runtime/init.test.ts +236 -0
  42. package/src/runtime/init.ts +114 -55
  43. package/src/session.ts +135 -2
  44. package/src/styles/tailwind.css +1 -1
  45. package/src/styles/widget.css +836 -513
  46. package/src/types/theme.ts +354 -0
  47. package/src/types.ts +314 -15
  48. package/src/ui.docked.test.ts +104 -0
  49. package/src/ui.ts +940 -227
  50. package/src/utils/artifact-gate.test.ts +255 -0
  51. package/src/utils/artifact-gate.ts +142 -0
  52. package/src/utils/artifact-resize.test.ts +64 -0
  53. package/src/utils/artifact-resize.ts +67 -0
  54. package/src/utils/attachment-manager.ts +10 -10
  55. package/src/utils/code-generators.test.ts +52 -0
  56. package/src/utils/code-generators.ts +40 -36
  57. package/src/utils/dock.ts +17 -0
  58. package/src/utils/dom-context.test.ts +504 -0
  59. package/src/utils/dom-context.ts +896 -0
  60. package/src/utils/dom.ts +12 -1
  61. package/src/utils/message-fingerprint.test.ts +187 -0
  62. package/src/utils/message-fingerprint.ts +105 -0
  63. package/src/utils/migration.ts +179 -0
  64. package/src/utils/morph.ts +1 -1
  65. package/src/utils/plugins.ts +175 -0
  66. package/src/utils/positioning.ts +4 -4
  67. package/src/utils/theme.test.ts +125 -0
  68. package/src/utils/theme.ts +216 -60
  69. package/src/utils/tokens.ts +682 -0
@@ -11,15 +11,15 @@ export const updateToolBubbleUI = (messageId: string, bubble: HTMLElement, confi
11
11
  const expanded = toolExpansionState.has(messageId);
12
12
  const toolCallConfig = config?.toolCall ?? {};
13
13
  const header = bubble.querySelector('button[data-expand-header="true"]') as HTMLElement;
14
- const content = bubble.querySelector('.tvw-border-t') as HTMLElement;
14
+ const content = bubble.querySelector('.persona-border-t') as HTMLElement;
15
15
 
16
16
  if (!header || !content) return;
17
17
 
18
18
  header.setAttribute("aria-expanded", expanded ? "true" : "false");
19
19
 
20
- // Find toggle icon container - it's the direct child div of headerMeta (which has tvw-ml-auto)
21
- const headerMeta = header.querySelector('.tvw-ml-auto') as HTMLElement;
22
- const toggleIcon = headerMeta?.querySelector(':scope > .tvw-flex.tvw-items-center') as HTMLElement;
20
+ // Find toggle icon container - it's the direct child div of headerMeta (which has persona-ml-auto)
21
+ const headerMeta = header.querySelector('.persona-ml-auto') as HTMLElement;
22
+ const toggleIcon = headerMeta?.querySelector(':scope > .persona-flex.persona-items-center') as HTMLElement;
23
23
  if (toggleIcon) {
24
24
  toggleIcon.innerHTML = "";
25
25
  const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
@@ -43,17 +43,17 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
43
43
  [
44
44
  "vanilla-message-bubble",
45
45
  "vanilla-tool-bubble",
46
- "tvw-w-full",
47
- "tvw-max-w-[85%]",
48
- "tvw-rounded-2xl",
49
- "tvw-bg-cw-surface",
50
- "tvw-border",
51
- "tvw-border-cw-message-border",
52
- "tvw-text-cw-primary",
53
- "tvw-shadow-sm",
54
- "tvw-overflow-hidden",
55
- "tvw-px-0",
56
- "tvw-py-0"
46
+ "persona-w-full",
47
+ "persona-max-w-[85%]",
48
+ "persona-rounded-2xl",
49
+ "persona-bg-persona-surface",
50
+ "persona-border",
51
+ "persona-border-persona-message-border",
52
+ "persona-text-persona-primary",
53
+ "persona-shadow-sm",
54
+ "persona-overflow-hidden",
55
+ "persona-px-0",
56
+ "persona-py-0"
57
57
  ].join(" ")
58
58
  );
59
59
  // Set id for idiomorph matching
@@ -81,7 +81,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
81
81
  let expanded = toolExpansionState.has(message.id);
82
82
  const header = createElement(
83
83
  "button",
84
- "tvw-flex tvw-w-full tvw-items-center tvw-justify-between tvw-gap-3 tvw-bg-transparent tvw-px-4 tvw-py-3 tvw-text-left tvw-cursor-pointer tvw-border-none"
84
+ "persona-flex persona-w-full persona-items-center persona-justify-between persona-gap-3 persona-bg-transparent persona-px-4 persona-py-3 persona-text-left persona-cursor-pointer persona-border-none"
85
85
  ) as HTMLButtonElement;
86
86
  header.type = "button";
87
87
  header.setAttribute("aria-expanded", expanded ? "true" : "false");
@@ -101,15 +101,15 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
101
101
  header.style.paddingBottom = toolCallConfig.headerPaddingY;
102
102
  }
103
103
 
104
- const headerContent = createElement("div", "tvw-flex tvw-flex-col tvw-text-left");
105
- const title = createElement("span", "tvw-text-xs tvw-text-cw-primary");
104
+ const headerContent = createElement("div", "persona-flex persona-flex-col persona-text-left");
105
+ const title = createElement("span", "persona-text-xs persona-text-persona-primary");
106
106
  if (toolCallConfig.headerTextColor) {
107
107
  title.style.color = toolCallConfig.headerTextColor;
108
108
  }
109
109
  title.textContent = describeToolTitle(tool);
110
110
  headerContent.appendChild(title);
111
111
 
112
- const toggleIcon = createElement("div", "tvw-flex tvw-items-center");
112
+ const toggleIcon = createElement("div", "persona-flex persona-items-center");
113
113
  const iconColor = toolCallConfig.toggleTextColor || toolCallConfig.headerTextColor || "currentColor";
114
114
  const chevronIcon = renderLucideIcon(expanded ? "chevron-up" : "chevron-down", 16, iconColor, 2);
115
115
  if (chevronIcon) {
@@ -119,14 +119,14 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
119
119
  toggleIcon.textContent = expanded ? "Hide" : "Show";
120
120
  }
121
121
 
122
- const headerMeta = createElement("div", "tvw-flex tvw-items-center tvw-gap-2 tvw-ml-auto");
122
+ const headerMeta = createElement("div", "persona-flex persona-items-center persona-gap-2 persona-ml-auto");
123
123
  headerMeta.append(toggleIcon);
124
124
 
125
125
  header.append(headerContent, headerMeta);
126
126
 
127
127
  const content = createElement(
128
128
  "div",
129
- "tvw-border-t tvw-space-y-3 tvw-px-4 tvw-py-3"
129
+ "persona-border-t persona-border-gray-200 persona-bg-gray-50 persona-space-y-3 persona-px-4 persona-py-3"
130
130
  );
131
131
  content.style.display = expanded ? "" : "none";
132
132
 
@@ -148,7 +148,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
148
148
 
149
149
  // Add tool name at the top of content
150
150
  if (tool.name) {
151
- const toolName = createElement("div", "tvw-text-xs tvw-text-cw-muted tvw-italic");
151
+ const toolName = createElement("div", "persona-text-xs persona-text-persona-muted persona-italic");
152
152
  if (toolCallConfig.contentTextColor) {
153
153
  toolName.style.color = toolCallConfig.contentTextColor;
154
154
  } else if (toolCallConfig.headerTextColor) {
@@ -159,10 +159,10 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
159
159
  }
160
160
 
161
161
  if (tool.args !== undefined) {
162
- const argsBlock = createElement("div", "tvw-space-y-1");
162
+ const argsBlock = createElement("div", "persona-space-y-1");
163
163
  const argsLabel = createElement(
164
164
  "div",
165
- "tvw-text-xs tvw-text-cw-muted"
165
+ "persona-text-xs persona-text-persona-muted"
166
166
  );
167
167
  if (toolCallConfig.labelTextColor) {
168
168
  argsLabel.style.color = toolCallConfig.labelTextColor;
@@ -170,7 +170,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
170
170
  argsLabel.textContent = "Arguments";
171
171
  const argsPre = createElement(
172
172
  "pre",
173
- "tvw-max-h-48 tvw-overflow-auto tvw-whitespace-pre-wrap tvw-rounded-lg tvw-border tvw-px-3 tvw-py-2 tvw-text-xs tvw-text-cw-primary"
173
+ "persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-border-gray-100 persona-bg-white persona-px-3 persona-py-2 persona-text-xs persona-text-persona-primary"
174
174
  );
175
175
  // Ensure font size matches header text (0.75rem / 12px)
176
176
  argsPre.style.fontSize = "0.75rem";
@@ -190,10 +190,10 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
190
190
  }
191
191
 
192
192
  if (tool.chunks && tool.chunks.length) {
193
- const logsBlock = createElement("div", "tvw-space-y-1");
193
+ const logsBlock = createElement("div", "persona-space-y-1");
194
194
  const logsLabel = createElement(
195
195
  "div",
196
- "tvw-text-xs tvw-text-cw-muted"
196
+ "persona-text-xs persona-text-persona-muted"
197
197
  );
198
198
  if (toolCallConfig.labelTextColor) {
199
199
  logsLabel.style.color = toolCallConfig.labelTextColor;
@@ -201,7 +201,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
201
201
  logsLabel.textContent = "Activity";
202
202
  const logsPre = createElement(
203
203
  "pre",
204
- "tvw-max-h-48 tvw-overflow-auto tvw-whitespace-pre-wrap tvw-rounded-lg tvw-border tvw-px-3 tvw-py-2 tvw-text-xs tvw-text-cw-primary"
204
+ "persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-border-gray-100 persona-bg-white persona-px-3 persona-py-2 persona-text-xs persona-text-persona-primary"
205
205
  );
206
206
  // Ensure font size matches header text (0.75rem / 12px)
207
207
  logsPre.style.fontSize = "0.75rem";
@@ -221,10 +221,10 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
221
221
  }
222
222
 
223
223
  if (tool.status === "complete" && tool.result !== undefined) {
224
- const resultBlock = createElement("div", "tvw-space-y-1");
224
+ const resultBlock = createElement("div", "persona-space-y-1");
225
225
  const resultLabel = createElement(
226
226
  "div",
227
- "tvw-text-xs tvw-text-cw-muted"
227
+ "persona-text-xs persona-text-persona-muted"
228
228
  );
229
229
  if (toolCallConfig.labelTextColor) {
230
230
  resultLabel.style.color = toolCallConfig.labelTextColor;
@@ -232,7 +232,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
232
232
  resultLabel.textContent = "Result";
233
233
  const resultPre = createElement(
234
234
  "pre",
235
- "tvw-max-h-48 tvw-overflow-auto tvw-whitespace-pre-wrap tvw-rounded-lg tvw-border tvw-px-3 tvw-py-2 tvw-text-xs tvw-text-cw-primary"
235
+ "persona-max-h-48 persona-overflow-auto persona-whitespace-pre-wrap persona-rounded-lg persona-border persona-border-gray-100 persona-bg-white persona-px-3 persona-py-2 persona-text-xs persona-text-persona-primary"
236
236
  );
237
237
  // Ensure font size matches header text (0.75rem / 12px)
238
238
  resultPre.style.fontSize = "0.75rem";
@@ -254,7 +254,7 @@ export const createToolBubble = (message: AgentWidgetMessage, config?: AgentWidg
254
254
  if (tool.status === "complete" && typeof tool.duration === "number") {
255
255
  const duration = createElement(
256
256
  "div",
257
- "tvw-text-xs tvw-text-cw-muted"
257
+ "persona-text-xs persona-text-persona-muted"
258
258
  );
259
259
  if (toolCallConfig.contentTextColor) {
260
260
  duration.style.color = toolCallConfig.contentTextColor;
package/src/defaults.ts CHANGED
@@ -91,6 +91,12 @@ export const DEFAULT_WIDGET_CONFIG: Partial<AgentWidgetConfig> = {
91
91
  colorScheme: "light",
92
92
  launcher: {
93
93
  enabled: true,
94
+ mountMode: "floating",
95
+ dock: {
96
+ side: "right",
97
+ width: "420px",
98
+ collapsedWidth: "72px",
99
+ },
94
100
  title: "Chat Assistant",
95
101
  subtitle: "Here to help you get answers fast",
96
102
  agentIconText: "💬",
@@ -251,6 +257,10 @@ export function mergeWithDefaults(
251
257
  launcher: {
252
258
  ...DEFAULT_WIDGET_CONFIG.launcher,
253
259
  ...config.launcher,
260
+ dock: {
261
+ ...DEFAULT_WIDGET_CONFIG.launcher?.dock,
262
+ ...config.launcher?.dock,
263
+ },
254
264
  clearChat: {
255
265
  ...DEFAULT_WIDGET_CONFIG.launcher?.clearChat,
256
266
  ...config.launcher?.clearChat,
@@ -272,10 +282,26 @@ export function mergeWithDefaults(
272
282
  ...DEFAULT_WIDGET_CONFIG.voiceRecognition,
273
283
  ...config.voiceRecognition,
274
284
  },
275
- features: {
276
- ...DEFAULT_WIDGET_CONFIG.features,
277
- ...config.features,
278
- },
285
+ features: (() => {
286
+ const da = DEFAULT_WIDGET_CONFIG.features?.artifacts;
287
+ const ca = config.features?.artifacts;
288
+ const mergedArtifacts =
289
+ da === undefined && ca === undefined
290
+ ? undefined
291
+ : {
292
+ ...da,
293
+ ...ca,
294
+ layout: {
295
+ ...da?.layout,
296
+ ...ca?.layout,
297
+ },
298
+ };
299
+ return {
300
+ ...DEFAULT_WIDGET_CONFIG.features,
301
+ ...config.features,
302
+ ...(mergedArtifacts !== undefined ? { artifacts: mergedArtifacts } : {}),
303
+ };
304
+ })(),
279
305
  suggestionChips: config.suggestionChips ?? DEFAULT_WIDGET_CONFIG.suggestionChips,
280
306
  suggestionChipsConfig: {
281
307
  ...DEFAULT_WIDGET_CONFIG.suggestionChipsConfig,
package/src/index.ts CHANGED
@@ -7,9 +7,16 @@ export type {
7
7
  AgentWidgetConfig,
8
8
  AgentWidgetTheme,
9
9
  AgentWidgetFeatureFlags,
10
+ AgentWidgetArtifactsFeature,
11
+ AgentWidgetArtifactsLayoutConfig,
12
+ PersonaArtifactKind,
13
+ PersonaArtifactRecord,
14
+ PersonaArtifactManualUpsert,
15
+ ArtifactConfigPayload,
10
16
  AgentWidgetInitOptions,
11
17
  AgentWidgetMessage,
12
18
  AgentWidgetLauncherConfig,
19
+ AgentWidgetDockConfig,
13
20
  AgentWidgetEvent,
14
21
  AgentWidgetStreamParser,
15
22
  AgentWidgetStreamParserResult,
@@ -25,6 +32,7 @@ export type {
25
32
  MessageContent,
26
33
  // Attachment config type
27
34
  AgentWidgetAttachmentsConfig,
35
+ AgentWidgetComposerConfig,
28
36
  // Layout types
29
37
  AgentWidgetLayoutConfig,
30
38
  AgentWidgetHeaderLayoutConfig,
@@ -62,6 +70,7 @@ export type {
62
70
  // Agent execution types
63
71
  AgentConfig,
64
72
  AgentLoopConfig,
73
+ AgentToolsConfig,
65
74
  AgentRequestOptions,
66
75
  AgentExecutionState,
67
76
  AgentMessageMetadata,
@@ -82,6 +91,11 @@ export type {
82
91
  } from "./types";
83
92
 
84
93
  export { initAgentWidgetFn as initAgentWidget };
94
+ export {
95
+ createWidgetHostLayout,
96
+ type WidgetHostLayout,
97
+ type WidgetHostLayoutMode
98
+ } from "./runtime/host-layout";
85
99
  export {
86
100
  createAgentExperience,
87
101
  type AgentWidgetController
@@ -125,6 +139,21 @@ export {
125
139
  fileToImagePart,
126
140
  validateImageFile
127
141
  } from "./utils/content";
142
+ export {
143
+ collectEnrichedPageContext,
144
+ formatEnrichedContext,
145
+ generateStableSelector,
146
+ defaultParseRules
147
+ } from "./utils/dom-context";
148
+ export type {
149
+ EnrichedPageElement,
150
+ DomContextOptions,
151
+ DomContextMode,
152
+ ParseOptionsConfig,
153
+ ParseRule,
154
+ RuleScoringContext,
155
+ FormatEnrichedContextOptions
156
+ } from "./utils/dom-context";
128
157
  export {
129
158
  AttachmentManager,
130
159
  type PendingAttachment,
@@ -135,6 +164,7 @@ export {
135
164
  generateUserMessageId,
136
165
  generateAssistantMessageId
137
166
  } from "./utils/message-id";
167
+ export { isDockedMountMode, resolveDockConfig } from "./utils/dock";
138
168
  export { generateCodeSnippet } from "./utils/code-generators";
139
169
  export type { CodeFormat, CodeGeneratorHooks, CodeGeneratorOptions } from "./utils/code-generators";
140
170
  export { VERSION } from "./version";
@@ -144,6 +174,55 @@ export type { AgentWidgetInitHandle };
144
174
  export type { AgentWidgetPlugin } from "./plugins/types";
145
175
  export { pluginRegistry } from "./plugins/registry";
146
176
 
177
+ // Theme system exports
178
+ export {
179
+ createTheme,
180
+ resolveTokens,
181
+ themeToCssVariables,
182
+ applyThemeVariables,
183
+ getActiveTheme,
184
+ getColorScheme,
185
+ detectColorScheme,
186
+ createThemeObserver
187
+ } from "./utils/theme";
188
+ export {
189
+ DEFAULT_PALETTE,
190
+ DEFAULT_SEMANTIC,
191
+ DEFAULT_COMPONENTS,
192
+ validateTheme
193
+ } from "./utils/tokens";
194
+ export {
195
+ accessibilityPlugin,
196
+ animationsPlugin,
197
+ brandPlugin,
198
+ reducedMotionPlugin,
199
+ highContrastPlugin,
200
+ createPlugin
201
+ } from "./utils/plugins";
202
+ export {
203
+ migrateV1Theme,
204
+ validateV1Theme
205
+ } from "./utils/migration";
206
+ export type {
207
+ PersonaTheme,
208
+ PersonaThemePlugin,
209
+ CreateThemeOptions,
210
+ TokenReference,
211
+ ColorShade,
212
+ ColorPalette,
213
+ SpacingScale,
214
+ TypographyScale,
215
+ ShadowScale,
216
+ BorderScale,
217
+ RadiusScale,
218
+ SemanticColors,
219
+ SemanticSpacing,
220
+ SemanticTypography,
221
+ ComponentTokens,
222
+ ThemeValidationResult,
223
+ ThemeValidationError
224
+ } from "./types/theme";
225
+
147
226
  // Component system exports
148
227
  export { componentRegistry } from "./components/registry";
149
228
  export type { ComponentRenderer, ComponentContext } from "./components/registry";
@@ -184,8 +263,7 @@ export {
184
263
  getHeaderLayout,
185
264
  buildHeaderWithLayout,
186
265
  buildDefaultHeader,
187
- buildMinimalHeader,
188
- buildExpandedHeader
266
+ buildMinimalHeader
189
267
  } from "./components/header-layouts";
190
268
  export type {
191
269
  HeaderLayoutContext,
package/src/install.ts CHANGED
@@ -18,6 +18,8 @@ interface SiteAgentInstallConfig {
18
18
  clientToken?: string;
19
19
  flowId?: string;
20
20
  apiUrl?: string;
21
+ // Optional query param key that gates widget installation in preview mode
22
+ previewQueryParam?: string;
21
23
  // Shadow DOM option (defaults to false for better CSS compatibility)
22
24
  useShadowDom?: boolean;
23
25
  }
@@ -84,6 +86,12 @@ declare global {
84
86
  scriptConfig.apiUrl = apiUrl;
85
87
  }
86
88
 
89
+ // Optional preview query param gate
90
+ const previewQueryParam = script.getAttribute('data-preview-param');
91
+ if (previewQueryParam) {
92
+ scriptConfig.previewQueryParam = previewQueryParam;
93
+ }
94
+
87
95
  return scriptConfig;
88
96
  };
89
97
 
@@ -93,6 +101,20 @@ declare global {
93
101
  // Merge script attributes with window config (script attributes take precedence)
94
102
  const windowConfig: SiteAgentInstallConfig = window.siteAgentConfig || {};
95
103
  const config: SiteAgentInstallConfig = { ...windowConfig, ...scriptConfig };
104
+
105
+ const isPreviewModeEnabled = (): boolean => {
106
+ if (!config.previewQueryParam) {
107
+ return true;
108
+ }
109
+
110
+ const params = new URLSearchParams(window.location.search);
111
+ const value = params.get(config.previewQueryParam);
112
+ return value !== null && value !== "" && value.toLowerCase() !== "false" && value !== "0";
113
+ };
114
+
115
+ if (!isPreviewModeEnabled()) {
116
+ return;
117
+ }
96
118
 
97
119
  const version = config.version || "latest";
98
120
  const cdn = config.cdn || "jsdelivr";
@@ -61,7 +61,30 @@ export interface AgentWidgetPlugin {
61
61
  config: AgentWidgetConfig;
62
62
  defaultRenderer: () => HTMLElement;
63
63
  onSubmit: (text: string) => void;
64
+ /**
65
+ * When true, the assistant stream is active — same moment `session.isStreaming()` becomes true.
66
+ * Prefer wiring controls to `data-persona-composer-disable-when-streaming` plus `setComposerDisabled`
67
+ * in the host, or react to `footer.dataset.personaComposerStreaming === "true"`.
68
+ */
69
+ streaming: boolean;
70
+ /**
71
+ * Legacy alias: host disables the primary submit control while `streaming` is true.
72
+ * @deprecated Use `streaming` for new plugins.
73
+ */
64
74
  disabled: boolean;
75
+ /** Opens the hidden file input when `config.attachments.enabled` is true (no-op otherwise). */
76
+ openAttachmentPicker: () => void;
77
+ /** From `config.composer.models` */
78
+ models?: Array<{ id: string; label: string }>;
79
+ /** From `config.composer.selectedModelId` */
80
+ selectedModelId?: string;
81
+ /** Updates `config.composer.selectedModelId` for the running widget instance. */
82
+ onModelChange?: (modelId: string) => void;
83
+ /**
84
+ * Same behavior as the built-in mic when voice is enabled.
85
+ * Omitted when `config.voiceRecognition.enabled` is not true.
86
+ */
87
+ onVoiceToggle?: () => void;
65
88
  }) => HTMLElement | null;
66
89
 
67
90
  /**
@@ -162,7 +162,7 @@ export const createDirectivePostprocessor = (markdownConfig?: AgentWidgetMarkdow
162
162
  placeholders.forEach(({ token, type }) => {
163
163
  const tokenRegex = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
164
164
  const safeType = escapeAttribute(type);
165
- const replacement = `<div class="tvw-form-directive" data-tv-form="${safeType}"></div>`;
165
+ const replacement = `<div class="persona-form-directive" data-tv-form="${safeType}"></div>`;
166
166
  html = html.replace(tokenRegex, replacement);
167
167
  });
168
168
 
@@ -186,7 +186,7 @@ export const directivePostprocessor = (text: string): string => {
186
186
  placeholders.forEach(({ token, type }) => {
187
187
  const tokenRegex = new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
188
188
  const safeType = escapeAttribute(type);
189
- const replacement = `<div class="tvw-form-directive" data-tv-form="${safeType}"></div>`;
189
+ const replacement = `<div class="persona-form-directive" data-tv-form="${safeType}"></div>`;
190
190
  html = html.replace(tokenRegex, replacement);
191
191
  });
192
192
 
@@ -0,0 +1,174 @@
1
+ import type {
2
+ AgentWidgetConfig,
3
+ AgentWidgetStateSnapshot,
4
+ } from "../types";
5
+ import { isDockedMountMode, resolveDockConfig } from "../utils/dock";
6
+
7
+ export type WidgetHostLayoutMode = "direct" | "docked";
8
+
9
+ export type WidgetHostLayout = {
10
+ mode: WidgetHostLayoutMode;
11
+ host: HTMLElement;
12
+ shell: HTMLElement | null;
13
+ syncWidgetState: (state: Pick<AgentWidgetStateSnapshot, "open" | "launcherEnabled">) => void;
14
+ updateConfig: (config?: AgentWidgetConfig) => void;
15
+ destroy: () => void;
16
+ };
17
+
18
+ const setDirectHostStyles = (host: HTMLElement, config?: AgentWidgetConfig): void => {
19
+ const launcherEnabled = config?.launcher?.enabled ?? true;
20
+ host.className = "persona-host";
21
+ host.style.height = launcherEnabled ? "" : "100%";
22
+ host.style.display = launcherEnabled ? "" : "flex";
23
+ host.style.flexDirection = launcherEnabled ? "" : "column";
24
+ host.style.flex = launcherEnabled ? "" : "1 1 auto";
25
+ host.style.minHeight = launcherEnabled ? "" : "0";
26
+ };
27
+
28
+ const applyDockStyles = (
29
+ shell: HTMLElement,
30
+ contentSlot: HTMLElement,
31
+ dockSlot: HTMLElement,
32
+ host: HTMLElement,
33
+ config: AgentWidgetConfig | undefined,
34
+ expanded: boolean
35
+ ): void => {
36
+ const dock = resolveDockConfig(config);
37
+ const width = expanded ? dock.width : dock.collapsedWidth;
38
+
39
+ shell.dataset.personaHostLayout = "docked";
40
+ shell.dataset.personaDockSide = dock.side;
41
+ shell.dataset.personaDockOpen = expanded ? "true" : "false";
42
+ shell.style.display = "flex";
43
+ shell.style.flexDirection = "row";
44
+ shell.style.alignItems = "stretch";
45
+ shell.style.width = "100%";
46
+ shell.style.maxWidth = "100%";
47
+ shell.style.minWidth = "0";
48
+ shell.style.height = "100%";
49
+ shell.style.minHeight = "0";
50
+ shell.style.position = "relative";
51
+
52
+ contentSlot.style.display = "flex";
53
+ contentSlot.style.flexDirection = "column";
54
+ contentSlot.style.flex = "1 1 auto";
55
+ contentSlot.style.minWidth = "0";
56
+ contentSlot.style.minHeight = "0";
57
+ contentSlot.style.position = "relative";
58
+
59
+ dockSlot.style.display = "flex";
60
+ dockSlot.style.flexDirection = "column";
61
+ dockSlot.style.flex = `0 0 ${width}`;
62
+ dockSlot.style.width = width;
63
+ dockSlot.style.maxWidth = width;
64
+ dockSlot.style.minWidth = width;
65
+ dockSlot.style.minHeight = "0";
66
+ dockSlot.style.position = "relative";
67
+ dockSlot.style.overflow = "visible";
68
+ dockSlot.style.transition = "width 180ms ease, min-width 180ms ease, max-width 180ms ease, flex-basis 180ms ease";
69
+
70
+ host.className = "persona-host";
71
+ host.style.height = "100%";
72
+ host.style.minHeight = "0";
73
+ host.style.display = "flex";
74
+ host.style.flexDirection = "column";
75
+ host.style.flex = "1 1 auto";
76
+
77
+ if (dock.side === "left") {
78
+ if (shell.firstElementChild !== dockSlot) {
79
+ shell.replaceChildren(dockSlot, contentSlot);
80
+ }
81
+ } else if (shell.lastElementChild !== dockSlot) {
82
+ shell.replaceChildren(contentSlot, dockSlot);
83
+ }
84
+ };
85
+
86
+ const createDirectLayout = (target: HTMLElement, config?: AgentWidgetConfig): WidgetHostLayout => {
87
+ const host = target.ownerDocument.createElement("div");
88
+ setDirectHostStyles(host, config);
89
+ target.appendChild(host);
90
+
91
+ return {
92
+ mode: "direct",
93
+ host,
94
+ shell: null,
95
+ syncWidgetState: () => {},
96
+ updateConfig(nextConfig?: AgentWidgetConfig) {
97
+ setDirectHostStyles(host, nextConfig);
98
+ },
99
+ destroy() {
100
+ host.remove();
101
+ },
102
+ };
103
+ };
104
+
105
+ const createDockedLayout = (target: HTMLElement, config?: AgentWidgetConfig): WidgetHostLayout => {
106
+ const { ownerDocument } = target;
107
+ const originalParent = target.parentElement;
108
+
109
+ if (!originalParent) {
110
+ throw new Error("Docked widget target must be attached to the DOM");
111
+ }
112
+
113
+ const tagName = target.tagName.toUpperCase();
114
+ if (tagName === "BODY" || tagName === "HTML") {
115
+ throw new Error('Docked widget target must be a concrete container element, not "body" or "html"');
116
+ }
117
+
118
+ const originalNextSibling = target.nextSibling;
119
+ const shell = ownerDocument.createElement("div");
120
+ const contentSlot = ownerDocument.createElement("div");
121
+ const dockSlot = ownerDocument.createElement("aside");
122
+ const host = ownerDocument.createElement("div");
123
+ let expanded = (config?.launcher?.enabled ?? true) ? (config?.launcher?.autoExpand ?? false) : true;
124
+
125
+ contentSlot.dataset.personaDockRole = "content";
126
+ dockSlot.dataset.personaDockRole = "panel";
127
+ host.dataset.personaDockRole = "host";
128
+
129
+ dockSlot.appendChild(host);
130
+ originalParent.insertBefore(shell, target);
131
+ contentSlot.appendChild(target);
132
+ shell.appendChild(contentSlot);
133
+ shell.appendChild(dockSlot);
134
+ applyDockStyles(shell, contentSlot, dockSlot, host, config, expanded);
135
+
136
+ return {
137
+ mode: "docked",
138
+ host,
139
+ shell,
140
+ syncWidgetState(state) {
141
+ const nextExpanded = state.launcherEnabled ? state.open : true;
142
+ if (expanded === nextExpanded) return;
143
+ expanded = nextExpanded;
144
+ applyDockStyles(shell, contentSlot, dockSlot, host, config, expanded);
145
+ },
146
+ updateConfig(nextConfig?: AgentWidgetConfig) {
147
+ config = nextConfig;
148
+ if ((config?.launcher?.enabled ?? true) === false) {
149
+ expanded = true;
150
+ }
151
+ applyDockStyles(shell, contentSlot, dockSlot, host, config, expanded);
152
+ },
153
+ destroy() {
154
+ if (originalParent.isConnected) {
155
+ if (originalNextSibling && originalNextSibling.parentNode === originalParent) {
156
+ originalParent.insertBefore(target, originalNextSibling);
157
+ } else {
158
+ originalParent.appendChild(target);
159
+ }
160
+ }
161
+ shell.remove();
162
+ },
163
+ };
164
+ };
165
+
166
+ export const createWidgetHostLayout = (
167
+ target: HTMLElement,
168
+ config?: AgentWidgetConfig
169
+ ): WidgetHostLayout => {
170
+ if (isDockedMountMode(config)) {
171
+ return createDockedLayout(target, config);
172
+ }
173
+ return createDirectLayout(target, config);
174
+ };