@runtypelabs/persona 4.23.2 → 4.25.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 (166) 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-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
  4. package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
  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 +2 -0
  8. package/dist/animations-extra.d.ts +2 -0
  9. package/dist/approval-ui-3NMYNXBD.js +1 -0
  10. package/dist/approval-ui.cjs +2 -2
  11. package/dist/approval-ui.d.cts +278 -37
  12. package/dist/approval-ui.d.ts +278 -37
  13. package/dist/approval-ui.js +2 -2
  14. package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
  15. package/dist/artifacts-ui.d.cts +273 -25
  16. package/dist/artifacts-ui.d.ts +273 -25
  17. package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
  18. package/dist/codegen.cjs +11 -11
  19. package/dist/codegen.js +13 -13
  20. package/dist/context-mentions.d.cts +273 -25
  21. package/dist/context-mentions.d.ts +273 -25
  22. package/dist/event-stream-view.d.cts +273 -25
  23. package/dist/event-stream-view.d.ts +273 -25
  24. package/dist/forms-ui.d.cts +319 -27
  25. package/dist/forms-ui.d.ts +319 -27
  26. package/dist/history-view.d.cts +2 -0
  27. package/dist/history-view.d.ts +2 -0
  28. package/dist/index.cjs +70 -69
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.cts +344 -39
  31. package/dist/index.d.ts +344 -39
  32. package/dist/index.global.js +57 -54
  33. package/dist/index.global.js.map +1 -1
  34. package/dist/index.js +70 -69
  35. package/dist/index.js.map +1 -1
  36. package/dist/install.global.js +1 -1
  37. package/dist/install.global.js.map +1 -1
  38. package/dist/launcher.global.js +2 -2
  39. package/dist/launcher.global.js.map +1 -1
  40. package/dist/session-reconnect.d.cts +273 -25
  41. package/dist/session-reconnect.d.ts +273 -25
  42. package/dist/smart-dom-reader.d.cts +273 -25
  43. package/dist/smart-dom-reader.d.ts +273 -25
  44. package/dist/theme-editor-preview.cjs +74 -73
  45. package/dist/theme-editor-preview.d.cts +280 -25
  46. package/dist/theme-editor-preview.d.ts +280 -25
  47. package/dist/theme-editor-preview.js +51 -50
  48. package/dist/theme-editor.cjs +10 -10
  49. package/dist/theme-editor.d.cts +293 -27
  50. package/dist/theme-editor.d.ts +293 -27
  51. package/dist/theme-editor.js +10 -10
  52. package/dist/theme-reference.cjs +1 -1
  53. package/dist/theme-reference.d.cts +1522 -11
  54. package/dist/theme-reference.d.ts +1522 -11
  55. package/dist/theme-reference.js +1 -1
  56. package/dist/voice-runtime-7DA7MJH4.js +1 -0
  57. package/dist/voice-runtime.cjs +1 -1
  58. package/dist/voice-runtime.d.cts +33 -0
  59. package/dist/voice-runtime.d.ts +33 -0
  60. package/dist/voice-runtime.js +1 -1
  61. package/dist/widget.css +1 -1
  62. package/package.json +3 -2
  63. package/src/__fixtures__/defaults-parity.v4.json +776 -0
  64. package/src/approval-ui-entry.ts +0 -1
  65. package/src/approval-ui-loader.ts +5 -4
  66. package/src/approval-ui.ts +0 -1
  67. package/src/client-identity-proof.test.ts +193 -0
  68. package/src/client-session-warm.test.ts +216 -0
  69. package/src/client-visitor-history.test.ts +6 -0
  70. package/src/client.ts +122 -30
  71. package/src/codegen.test.ts +24 -0
  72. package/src/components/activity-row.test.ts +100 -0
  73. package/src/components/activity-row.ts +136 -0
  74. package/src/components/approval-actions.test.ts +8 -5
  75. package/src/components/approval-actions.ts +19 -6
  76. package/src/components/approval-bubble.test.ts +56 -9
  77. package/src/components/approval-bubble.ts +36 -41
  78. package/src/components/composer-builder.ts +5 -2
  79. package/src/components/composer-parts.ts +10 -2
  80. package/src/components/header-builder.test.ts +32 -0
  81. package/src/components/header-builder.ts +7 -4
  82. package/src/components/header-layouts.test.ts +12 -0
  83. package/src/components/header-layouts.ts +3 -2
  84. package/src/components/launcher.ts +11 -2
  85. package/src/components/message-bubble.test.ts +22 -0
  86. package/src/components/message-bubble.ts +7 -3
  87. package/src/components/panel.test.ts +52 -1
  88. package/src/components/panel.ts +47 -8
  89. package/src/components/pill-composer-builder.test.ts +2 -0
  90. package/src/components/pill-composer-builder.ts +0 -1
  91. package/src/components/reasoning-bubble.ts +29 -17
  92. package/src/components/suggestions.test.ts +1 -0
  93. package/src/components/suggestions.ts +0 -16
  94. package/src/components/tool-bubble.ts +131 -114
  95. package/src/components/tool-details.test.ts +87 -0
  96. package/src/components/tool-details.ts +102 -0
  97. package/src/components/welcome.test.ts +8 -0
  98. package/src/components/welcome.ts +4 -2
  99. package/src/defaults-parity.test.ts +158 -0
  100. package/src/defaults.test.ts +69 -0
  101. package/src/defaults.ts +128 -49
  102. package/src/generated/runtype-openapi-contract.ts +2 -2
  103. package/src/index-core.ts +4 -0
  104. package/src/install.test.ts +24 -0
  105. package/src/install.ts +3 -1
  106. package/src/launcher-global.ts +2 -1
  107. package/src/launcher-variants.test.ts +52 -0
  108. package/src/runtime/init-update-reset.test.ts +21 -0
  109. package/src/session.ts +59 -7
  110. package/src/session.voice.test.ts +72 -1
  111. package/src/styles/widget-styles.test.ts +19 -0
  112. package/src/styles/widget.css +295 -23
  113. package/src/theme-editor/index.ts +2 -0
  114. package/src/theme-editor/preview-utils.test.ts +8 -0
  115. package/src/theme-editor/preview-utils.ts +7 -3
  116. package/src/theme-editor/sections.test.ts +31 -0
  117. package/src/theme-editor/sections.ts +55 -1
  118. package/src/theme-editor/state-preferences.test.ts +49 -0
  119. package/src/theme-editor/state.ts +73 -15
  120. package/src/theme-reference.test.ts +20 -0
  121. package/src/theme-reference.ts +11 -5
  122. package/src/tool-call-display-defaults.test.ts +33 -1
  123. package/src/types/theme.ts +80 -3
  124. package/src/types.ts +207 -22
  125. package/src/ui.activity-row.test.ts +91 -0
  126. package/src/ui.approval-plugin.test.ts +63 -0
  127. package/src/ui.composer-compact.test.ts +24 -2
  128. package/src/ui.composer-placement.test.ts +23 -0
  129. package/src/ui.detached-panel.test.ts +33 -6
  130. package/src/ui.docked.test.ts +19 -0
  131. package/src/ui.header-update-stability.test.ts +25 -0
  132. package/src/ui.identity-proof.test.ts +58 -0
  133. package/src/ui.launcher-teaser.test.ts +25 -0
  134. package/src/ui.message-width.test.ts +17 -2
  135. package/src/ui.reasoning-display.test.ts +92 -0
  136. package/src/ui.rebuild-composer.test.ts +20 -3
  137. package/src/ui.scroll.test.ts +32 -0
  138. package/src/ui.session-init.test.ts +312 -0
  139. package/src/ui.status-indicator.test.ts +82 -0
  140. package/src/ui.suggest-replies.test.ts +11 -0
  141. package/src/ui.ts +422 -74
  142. package/src/ui.v5-defaults.test.ts +144 -0
  143. package/src/ui.voice-prewarm.test.ts +222 -0
  144. package/src/ui.welcome.test.ts +92 -3
  145. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
  146. package/src/utils/code-generators.ts +14 -0
  147. package/src/utils/composer-compact.test.ts +0 -2
  148. package/src/utils/composer-compact.ts +2 -4
  149. package/src/utils/composer-placement.ts +2 -3
  150. package/src/utils/config-merge.test.ts +81 -5
  151. package/src/utils/config-merge.ts +13 -3
  152. package/src/utils/defaults-provenance.ts +67 -0
  153. package/src/utils/dock.ts +7 -1
  154. package/src/utils/launcher-variant.ts +16 -0
  155. package/src/utils/panel-config.ts +97 -0
  156. package/src/utils/status-indicator.ts +39 -0
  157. package/src/utils/theme.test.ts +182 -0
  158. package/src/utils/theme.ts +131 -18
  159. package/src/utils/tokens.test.ts +122 -1
  160. package/src/utils/tokens.ts +177 -17
  161. package/src/voice/runtype-voice-provider.ts +238 -27
  162. package/src/voice/voice.test.ts +272 -2
  163. package/src/welcome.test.ts +23 -0
  164. package/src/welcome.ts +16 -9
  165. package/dist/approval-ui-K7R5Q5OC.js +0 -1
  166. package/dist/voice-runtime-EFR3R5ES.js +0 -1
@@ -588,6 +588,13 @@ function appendSerializableObjectBlock(
588
588
  lines.push(`${indent}},`);
589
589
  }
590
590
 
591
+ /** Preserve an explicitly authored defaults-version opt-in in every snippet. */
592
+ function appendFutureDefaultsConfig(lines: string[], config: any, indent: string): void {
593
+ if (config.future?.v5Defaults !== undefined) {
594
+ lines.push(`${indent}future: { v5Defaults: ${config.future.v5Defaults === true} },`);
595
+ }
596
+ }
597
+
591
598
  /**
592
599
  * Resolve the mount-target selector for a single-quoted JS string context.
593
600
  * Defaults to `body` and escapes backslashes / single quotes so an arbitrary
@@ -647,6 +654,7 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
647
654
  if (config.target) lines.push(` target: "${config.target}",`);
648
655
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
649
656
  if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
657
+ appendFutureDefaultsConfig(lines, config, " ");
650
658
 
651
659
  if (config.theme && typeof config.theme === "object" && Object.keys(config.theme).length > 0) {
652
660
  appendSerializableObjectBlock(lines, "theme", config.theme as Record<string, unknown>, " ");
@@ -797,6 +805,7 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
797
805
  if (config.target) lines.push(` target: "${config.target}",`);
798
806
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
799
807
  if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
808
+ appendFutureDefaultsConfig(lines, config, " ");
800
809
 
801
810
  if (config.theme && typeof config.theme === "object" && Object.keys(config.theme).length > 0) {
802
811
  appendSerializableObjectBlock(lines, "theme", config.theme as Record<string, unknown>, " ");
@@ -1064,6 +1073,7 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
1064
1073
  if (config.agentId) lines.push(` agentId: "${config.agentId}",`);
1065
1074
  if (config.target) lines.push(` target: "${config.target}",`);
1066
1075
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
1076
+ appendFutureDefaultsConfig(lines, config, " ");
1067
1077
 
1068
1078
  if (config.theme && typeof config.theme === "object" && Object.keys(config.theme).length > 0) {
1069
1079
  appendSerializableObjectBlock(lines, "theme", config.theme as Record<string, unknown>, " ");
@@ -1324,6 +1334,9 @@ function buildSerializableConfig(config: any): Record<string, any> {
1324
1334
  if (config.target) serializableConfig.target = config.target;
1325
1335
  if (config.flowId) serializableConfig.flowId = config.flowId;
1326
1336
  if (shouldEmitParserType) serializableConfig.parserType = parserType;
1337
+ if (config.future?.v5Defaults !== undefined) {
1338
+ serializableConfig.future = { v5Defaults: config.future.v5Defaults === true };
1339
+ }
1327
1340
  if (config.theme) serializableConfig.theme = config.theme;
1328
1341
  if (config.launcher) serializableConfig.launcher = config.launcher;
1329
1342
  if (config.copy) serializableConfig.copy = config.copy;
@@ -1477,6 +1490,7 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
1477
1490
  if (config.target) lines.push(` target: "${config.target}",`);
1478
1491
  if (config.flowId) lines.push(` flowId: "${config.flowId}",`);
1479
1492
  if (shouldEmitParserType) lines.push(` parserType: "${parserType}",`);
1493
+ appendFutureDefaultsConfig(lines, config, " ");
1480
1494
 
1481
1495
  if (config.theme && typeof config.theme === "object" && Object.keys(config.theme).length > 0) {
1482
1496
  appendSerializableObjectBlock(lines, "theme", config.theme as Record<string, unknown>, " ");
@@ -13,7 +13,6 @@ const base: ComposerCompactInput = {
13
13
  hasChips: false,
14
14
  hasQuote: false,
15
15
  hasPendingSubmission: false,
16
- dictationActive: false,
17
16
  };
18
17
 
19
18
  describe("isComposerCompact", () => {
@@ -38,7 +37,6 @@ describe("isComposerCompact", () => {
38
37
  ["hasChips"],
39
38
  ["hasQuote"],
40
39
  ["hasPendingSubmission"],
41
- ["dictationActive"],
42
40
  ] as const)("%s expands an otherwise empty composer", (key) => {
43
41
  expect(isComposerCompact({ ...base, [key]: true })).toBe(false);
44
42
  });
@@ -18,13 +18,12 @@ export type ComposerCompactInput = {
18
18
  hasChips: boolean;
19
19
  hasQuote: boolean;
20
20
  hasPendingSubmission: boolean;
21
- dictationActive: boolean;
22
21
  };
23
22
 
24
23
  /**
25
24
  * Compact when the draft is empty or a single line and nothing else occupies
26
25
  * the composer. Text alone is not enough: any chip, attachment, quote, pending
27
- * submission, or live dictation expands it.
26
+ * submission expands it. Recording state alone does not affect layout.
28
27
  */
29
28
  export function isComposerCompact(input: ComposerCompactInput): boolean {
30
29
  if (input.wrapped) return false;
@@ -33,8 +32,7 @@ export function isComposerCompact(input: ComposerCompactInput): boolean {
33
32
  input.hasAttachments ||
34
33
  input.hasChips ||
35
34
  input.hasQuote ||
36
- input.hasPendingSubmission ||
37
- input.dictationActive
35
+ input.hasPendingSubmission
38
36
  );
39
37
  }
40
38
 
@@ -4,9 +4,8 @@ export const DEFAULT_ANCHOR_COMPOSER_TOP = "44%";
4
4
  export const DEFAULT_COMPOSER_GAP = "24px";
5
5
 
6
6
  /**
7
- * Resolver-owned default, not `defaults.ts`: `mergeWithDefaults` materializes
8
- * anything declared there (see `welcome.ts` header). Composer-bar mode owns
9
- * its own geometry and always resolves `"block"`.
7
+ * V4 falls back to block; mergeWithDefaults supplies V5 overlay placement.
8
+ * Composer-bar mode owns its own geometry and always resolves `"block"`.
10
9
  */
11
10
  export const resolveComposerPlacement = (
12
11
  config: AgentWidgetConfig | undefined,
@@ -1,14 +1,49 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import { mergeConfigUpdate } from "./config-merge";
4
- import { mergeWithDefaults } from "../defaults";
4
+ import { DEFAULT_FLOATING_LAUNCHER_WIDTH, mergeWithDefaults } from "../defaults";
5
+ import { getPanelAliasProvenance } from "./panel-config";
5
6
  import type { AgentWidgetConfig, StreamAnimationPlugin } from "../types";
6
7
 
7
- // Stored controller config is post-mergeWithDefaults; simulate that here.
8
- const base = (overrides: Partial<AgentWidgetConfig> = {}): AgentWidgetConfig =>
9
- mergeWithDefaults({ apiUrl: "https://api.example.com/chat", ...overrides }) as AgentWidgetConfig;
8
+ describe.each([false, true])("mergeConfigUpdate (v5=%s)", (v5Defaults) => {
9
+ // Stored controller config is post-mergeWithDefaults.
10
+ const base = (overrides: Partial<AgentWidgetConfig> = {}): AgentWidgetConfig =>
11
+ mergeWithDefaults({ apiUrl: "https://api.example.com/chat", future: { v5Defaults }, ...overrides }) as AgentWidgetConfig;
12
+
13
+ it("retains and clears legacy panel-alias provenance across live patches", () => {
14
+ const initial = base({ launcher: { width: "600px", dock: { width: "520px" } } });
15
+ expect(getPanelAliasProvenance(initial)).toMatchObject({ launcherWidth: true, dockWidth: true });
16
+
17
+ const retained = mergeConfigUpdate(initial, { launcher: { title: "Updated" } });
18
+ expect(getPanelAliasProvenance(retained)).toMatchObject({ launcherWidth: true, dockWidth: true });
19
+
20
+ const cleared = mergeConfigUpdate(retained, { launcher: { width: undefined, dock: { width: undefined } } });
21
+ expect(getPanelAliasProvenance(cleared)).toMatchObject({ launcherWidth: false, dockWidth: false });
22
+
23
+ const parentCleared = mergeConfigUpdate(initial, { launcher: undefined });
24
+ expect(getPanelAliasProvenance(parentCleared)).toMatchObject({ launcherWidth: false, dockWidth: false });
25
+
26
+ const dockParentCleared = mergeConfigUpdate(initial, { launcher: { dock: undefined } });
27
+ expect(getPanelAliasProvenance(dockParentCleared)).toMatchObject({ launcherWidth: true, dockWidth: false });
28
+ });
29
+
30
+ it("does not treat an explicit undefined width as a legacy alias", () => {
31
+ const config = mergeWithDefaults({
32
+ future: { v5Defaults },
33
+ launcherWidth: "600px",
34
+ launcher: { width: undefined },
35
+ }) as AgentWidgetConfig;
36
+ expect(getPanelAliasProvenance(config)).toMatchObject({
37
+ launcherWidth: false,
38
+ legacyLauncherWidth: true,
39
+ });
40
+ });
41
+
42
+ it("keeps an explicit legacy width even when it equals the materialized default", () => {
43
+ const config = base({ launcher: { width: DEFAULT_FLOATING_LAUNCHER_WIDTH } });
44
+ expect(getPanelAliasProvenance(config).launcherWidth).toBe(true);
45
+ });
10
46
 
11
- describe("mergeConfigUpdate", () => {
12
47
  it("recursively merges nested plain objects, preserving sibling overrides", () => {
13
48
  const prev = base({ launcher: { enabled: false, clearChat: { backgroundColor: "#123456" } } });
14
49
  const next = mergeConfigUpdate(prev, { launcher: { title: "After" } });
@@ -304,3 +339,44 @@ describe("mergeConfigUpdate", () => {
304
339
  expect(again).toEqual(merged);
305
340
  });
306
341
  });
342
+
343
+
344
+ describe("live defaults version changes", () => {
345
+ const base = (config: Partial<AgentWidgetConfig> = {}) =>
346
+ mergeWithDefaults({ apiUrl: "/api", ...config }) as AgentWidgetConfig;
347
+
348
+ it("moves inherited defaults in both directions and resets a cleared future flag", () => {
349
+ const v5 = mergeConfigUpdate(base(), { future: { v5Defaults: true } });
350
+ expect(v5.launcher?.headerIconSize).toBe("20px");
351
+ expect(v5.layout?.header?.showSubtitle).toBe(false);
352
+ expect(v5.composer?.layout).toBe("single-row");
353
+ expect(v5.statusIndicator?.mode).toBe("transient");
354
+ const v4 = mergeConfigUpdate(v5, { future: undefined });
355
+ expect(v4.launcher?.headerIconSize).toBe("40px");
356
+ expect(v4.layout?.header?.showSubtitle).toBe(true);
357
+ expect(v4.composer?.layout).toBeUndefined();
358
+ expect(v4.statusIndicator?.mode).toBeUndefined();
359
+ const v5Again = mergeConfigUpdate(v4, { future: { v5Defaults: true } });
360
+ expect(v5Again.layout?.messages?.assistant?.width).toBe("full");
361
+ expect(v5Again.composer?.layout).toBe("single-row");
362
+ });
363
+
364
+ it("retains host choices even when they equal a previous version default", () => {
365
+ const initial = base({ launcher: { headerIconSize: "40px" }, layout: { header: { showSubtitle: true } } });
366
+ const v5 = mergeConfigUpdate(initial, { future: { v5Defaults: true } });
367
+ expect(v5.launcher?.headerIconSize).toBe("40px");
368
+ expect(v5.layout?.header?.showSubtitle).toBe(true);
369
+ const reset = mergeConfigUpdate(v5, { launcher: { headerIconSize: undefined }, layout: { header: undefined } });
370
+ expect(reset.launcher?.headerIconSize).toBe("20px");
371
+ expect(reset.layout?.header?.showSubtitle).toBe(false);
372
+ expect(initial.launcher?.headerIconSize).toBe("40px");
373
+ });
374
+
375
+ it("preserves live overrides and normal config spreads across version changes", () => {
376
+ const edited = mergeConfigUpdate(base(), { composer: { layout: "stacked" }, statusIndicator: { mode: "always" } });
377
+ const v5 = mergeConfigUpdate({ ...edited }, { future: { v5Defaults: true } });
378
+ expect(v5.composer?.layout).toBe("stacked");
379
+ expect(v5.statusIndicator?.mode).toBe("always");
380
+ expect(v5.launcher?.headerIconSize).toBe("20px");
381
+ });
382
+ });
@@ -1,5 +1,8 @@
1
- import { mergeWithDefaults } from "../defaults";
1
+ import { DEFAULTS_V5, mergeWithDefaults } from "../defaults";
2
2
  import type { AgentWidgetConfig, AgentWidgetConfigPatch } from "../types";
3
+ import { setPanelAliasProvenance, updatePanelAliasProvenance } from "./panel-config";
4
+
5
+ import { omitInheritedDefaults, setDefaultProvenance, updateDefaultProvenance, versionedDefaultPaths } from "./defaults-provenance";
3
6
 
4
7
  // Same predicate as utils/deep-merge.ts: plain object, non-null, non-array.
5
8
  // Class instances and DOM nodes also pass here, so replace-leaf paths guard the
@@ -63,6 +66,13 @@ export function mergeConfigUpdate(
63
66
  previousConfig: AgentWidgetConfig,
64
67
  patch: AgentWidgetConfigPatch
65
68
  ): AgentWidgetConfig {
66
- const merged = mergePatch(previousConfig, patch, "") as Partial<AgentWidgetConfig>;
67
- return mergeWithDefaults(merged) as AgentWidgetConfig;
69
+ const paths = versionedDefaultPaths(DEFAULTS_V5);
70
+ const explicit = updateDefaultProvenance(previousConfig, patch, paths);
71
+ const merged = setDefaultProvenance(omitInheritedDefaults(
72
+ mergePatch(previousConfig, patch, "") as Partial<AgentWidgetConfig>, paths, explicit
73
+ ), explicit);
74
+ return setPanelAliasProvenance(
75
+ mergeWithDefaults(merged) as AgentWidgetConfig,
76
+ updatePanelAliasProvenance(previousConfig, patch)
77
+ );
68
78
  }
@@ -0,0 +1,67 @@
1
+ /** Tracks host intent for versioned defaults without comparing values to defaults. */
2
+ const DEFAULTS_PROVENANCE = Symbol("persona.defaultsProvenance");
3
+ type Provenance = { [DEFAULTS_PROVENANCE]?: readonly string[] };
4
+ const record = (value: unknown): value is Record<string, unknown> =>
5
+ typeof value === "object" && value !== null && !Array.isArray(value);
6
+
7
+ export function versionedDefaultPaths(overlay: object, prefix = ""): string[] {
8
+ return Object.entries(overlay).flatMap(([key, value]) => {
9
+ const path = prefix ? `${prefix}.${key}` : key;
10
+ return record(value) ? versionedDefaultPaths(value, path) : [path];
11
+ });
12
+ }
13
+
14
+ function suppliedPath(value: unknown, path: string): "absent" | "clear" | "explicit" {
15
+ for (const key of path.split(".")) {
16
+ if (!record(value) || !Object.prototype.hasOwnProperty.call(value, key)) return "absent";
17
+ value = value[key];
18
+ if (value === undefined) return "clear";
19
+ }
20
+ return "explicit";
21
+ }
22
+
23
+ export function defaultProvenance(config: object | undefined, paths: readonly string[]): readonly string[] {
24
+ return (config as Provenance | undefined)?.[DEFAULTS_PROVENANCE]
25
+ ?? paths.filter((path) => suppliedPath(config, path) === "explicit");
26
+ }
27
+
28
+ export function setDefaultProvenance<T extends object>(config: T, explicit: readonly string[]): T {
29
+ Object.defineProperty(config, DEFAULTS_PROVENANCE, {
30
+ value: explicit, enumerable: true, configurable: true,
31
+ });
32
+ return config;
33
+ }
34
+
35
+ export function inheritDefaultProvenance<T extends object>(result: T, raw: object | undefined, overlay: object): T {
36
+ return setDefaultProvenance(result, defaultProvenance(raw, versionedDefaultPaths(overlay)));
37
+ }
38
+
39
+ export function updateDefaultProvenance(previous: object, patch: object, paths: readonly string[]): readonly string[] {
40
+ const marked = (patch as Provenance)[DEFAULTS_PROVENANCE];
41
+ if (marked) return marked;
42
+ const explicit = new Set(defaultProvenance(previous, paths));
43
+ for (const path of paths) {
44
+ const supplied = suppliedPath(patch, path);
45
+ if (supplied === "clear") explicit.delete(path);
46
+ if (supplied === "explicit") explicit.add(path);
47
+ }
48
+ return [...explicit];
49
+ }
50
+
51
+ /** Copy only traversed objects so deleting inherited defaults never mutates callers. */
52
+ export function omitInheritedDefaults<T extends object>(config: T, paths: readonly string[], explicit: readonly string[]): T {
53
+ const result = { ...config } as Record<string, unknown>;
54
+ for (const path of paths) {
55
+ if (explicit.includes(path)) continue;
56
+ const keys = path.split(".");
57
+ let target: Record<string, unknown> | undefined = result;
58
+ for (const key of keys.slice(0, -1)) {
59
+ const child: unknown = target[key];
60
+ if (!record(child)) { target = undefined; break; }
61
+ target[key] = { ...child };
62
+ target = target[key] as Record<string, unknown>;
63
+ }
64
+ if (target) delete target[keys[keys.length - 1]];
65
+ }
66
+ return result as T;
67
+ }
package/src/utils/dock.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  import type { AgentWidgetConfig, AgentWidgetDockConfig } from "../types";
2
+ import { getPanelAliasProvenance } from "./panel-config";
3
+ import { getActiveTheme } from "./theme";
4
+ import { resolveTokenValue } from "./tokens";
2
5
 
3
6
  const DEFAULT_DOCK_CONFIG: Required<AgentWidgetDockConfig> = {
4
7
  side: "right",
@@ -25,9 +28,12 @@ export const resolveDockConfig = (
25
28
  config?: AgentWidgetConfig
26
29
  ): Required<AgentWidgetDockConfig> => {
27
30
  const dock = config?.launcher?.dock;
31
+ const width = getPanelAliasProvenance(config).dockWidth
32
+ ? dock?.width
33
+ : resolveTokenValue(getActiveTheme(config), "components.panel.width");
28
34
  return {
29
35
  side: dock?.side ?? DEFAULT_DOCK_CONFIG.side,
30
- width: dock?.width ?? DEFAULT_DOCK_CONFIG.width,
36
+ width: width ?? DEFAULT_DOCK_CONFIG.width,
31
37
  animate: dock?.animate ?? DEFAULT_DOCK_CONFIG.animate,
32
38
  reveal: dock?.reveal ?? DEFAULT_DOCK_CONFIG.reveal,
33
39
  maxHeight: dock?.maxHeight ?? DEFAULT_DOCK_CONFIG.maxHeight,
@@ -0,0 +1,16 @@
1
+ import type { AgentWidgetConfig } from "../types";
2
+ import type { LauncherTokens } from "../types/theme";
3
+
4
+ export const resolveLauncherVariant = (config?: Pick<AgentWidgetConfig, "launcher" | "future">): "pill" | "circle" =>
5
+ config?.launcher?.variant ?? (config?.future?.v5Defaults === true ? "circle" : "pill");
6
+
7
+ export const CIRCLE_LAUNCHER_TOKENS: LauncherTokens = {
8
+ background: "semantic.colors.primary",
9
+ foreground: "semantic.colors.textInverse",
10
+ border: "transparent",
11
+ size: "48px",
12
+ iconSize: "24px",
13
+ borderRadius: "palette.radius.full",
14
+ shadow: "0 8px 24px rgba(0,0,0,.16)",
15
+ offset: "20px",
16
+ };
@@ -0,0 +1,97 @@
1
+ import type { AgentWidgetConfig, AgentWidgetConfigPatch } from "../types";
2
+
3
+ export const PANEL_WIDTH_VAR = "var(--persona-components-panel-width)";
4
+
5
+ /**
6
+ * Provenance for legacy panel geometry aliases. Config merging materializes
7
+ * launcher defaults, so values alone cannot distinguish a caller's alias from
8
+ * an inherited v4 default. The enumerable symbol survives object spreads while
9
+ * remaining absent from JSON, generated snippets, and the public config shape.
10
+ */
11
+ export const PANEL_ALIAS_PROVENANCE = Symbol("persona.panelAliasProvenance");
12
+
13
+ export type PanelAliasProvenance = {
14
+ launcherWidth: boolean;
15
+ legacyLauncherWidth: boolean;
16
+ sidebarWidth: boolean;
17
+ dockWidth: boolean;
18
+ };
19
+
20
+ type WithPanelAliasProvenance = {
21
+ [PANEL_ALIAS_PROVENANCE]?: PanelAliasProvenance;
22
+ };
23
+
24
+ const hasOwn = (value: object | undefined, key: string): boolean =>
25
+ value != null && Object.prototype.hasOwnProperty.call(value, key);
26
+
27
+ const hasOwnDefined = (value: object | undefined, key: string): boolean =>
28
+ hasOwn(value, key) && (value as Record<string, unknown>)[key] !== undefined;
29
+
30
+ const fromRawConfig = (
31
+ config?: Partial<AgentWidgetConfig>
32
+ ): PanelAliasProvenance => ({
33
+ launcherWidth: hasOwnDefined(config?.launcher, "width"),
34
+ legacyLauncherWidth: hasOwnDefined(config, "launcherWidth"),
35
+ sidebarWidth: hasOwnDefined(config?.launcher, "sidebarWidth"),
36
+ dockWidth: hasOwnDefined(config?.launcher?.dock, "width"),
37
+ });
38
+
39
+ export const getPanelAliasProvenance = (
40
+ config?: Partial<AgentWidgetConfig>
41
+ ): PanelAliasProvenance => {
42
+ const stored = (config as WithPanelAliasProvenance | undefined)?.[PANEL_ALIAS_PROVENANCE];
43
+ return stored ? { ...stored } : fromRawConfig(config);
44
+ };
45
+
46
+ export const setPanelAliasProvenance = <T extends object>(
47
+ config: T,
48
+ provenance: PanelAliasProvenance
49
+ ): T => {
50
+ Object.defineProperty(config, PANEL_ALIAS_PROVENANCE, {
51
+ value: provenance,
52
+ enumerable: true,
53
+ configurable: true,
54
+ });
55
+ return config;
56
+ };
57
+
58
+ /** Stamp a defaulted config with aliases supplied by the raw input. */
59
+ export const inheritPanelAliasProvenance = <T extends object>(
60
+ result: T,
61
+ rawConfig?: Partial<AgentWidgetConfig>
62
+ ): T => setPanelAliasProvenance(result, getPanelAliasProvenance(rawConfig));
63
+
64
+ /** Apply own alias keys in a live patch, including explicit-undefined resets. */
65
+ export const updatePanelAliasProvenance = (
66
+ previous: Partial<AgentWidgetConfig>,
67
+ patch: AgentWidgetConfigPatch
68
+ ): PanelAliasProvenance => {
69
+ // A config spread remains marked. Treat it as an already-normalized full
70
+ // config rather than mistaking its materialized launcher defaults for a
71
+ // patch that explicitly supplied every alias.
72
+ const patchProvenance = (patch as WithPanelAliasProvenance)[PANEL_ALIAS_PROVENANCE];
73
+ if (patchProvenance) return { ...patchProvenance };
74
+
75
+ const next = getPanelAliasProvenance(previous);
76
+ if (hasOwn(patch, "launcherWidth")) {
77
+ next.legacyLauncherWidth = patch.launcherWidth !== undefined;
78
+ }
79
+ if (hasOwn(patch, "launcher") && patch.launcher === undefined) {
80
+ next.launcherWidth = false;
81
+ next.sidebarWidth = false;
82
+ next.dockWidth = false;
83
+ return next;
84
+ }
85
+ if (hasOwn(patch.launcher, "width")) {
86
+ next.launcherWidth = patch.launcher?.width !== undefined;
87
+ }
88
+ if (hasOwn(patch.launcher, "sidebarWidth")) {
89
+ next.sidebarWidth = patch.launcher?.sidebarWidth !== undefined;
90
+ }
91
+ if (hasOwn(patch.launcher, "dock") && patch.launcher?.dock === undefined) {
92
+ next.dockWidth = false;
93
+ } else if (hasOwn(patch.launcher?.dock, "width")) {
94
+ next.dockWidth = patch.launcher?.dock?.width !== undefined;
95
+ }
96
+ return next;
97
+ };
@@ -0,0 +1,39 @@
1
+ import type { AgentWidgetStatusIndicatorConfig } from "../types";
2
+
3
+ /** Transient connection chrome excludes normal idle and streaming states. */
4
+ export const isStatusIndicatorVisible = (
5
+ config: AgentWidgetStatusIndicatorConfig,
6
+ status: string,
7
+ ): boolean => config.visible !== false && (
8
+ config.mode !== "transient" ||
9
+ status === "connecting" || status === "paused" ||
10
+ status === "resuming" || status === "error"
11
+ );
12
+
13
+ export const applyStatusIndicatorState = (
14
+ element: HTMLElement,
15
+ config: AgentWidgetStatusIndicatorConfig,
16
+ status: string,
17
+ ): void => {
18
+ const transient = config.mode === "transient";
19
+ const hasNotice = element.hasAttribute("data-persona-composer-reason") ||
20
+ element.hasAttribute("data-persona-composer-notice");
21
+ element.style.display = isStatusIndicatorVisible(config, status) ||
22
+ (transient && hasNotice && config.visible !== false) ? "" : "none";
23
+ element.classList.toggle("persona-mt-2", !transient);
24
+ element.classList.toggle("persona-mb-2", transient);
25
+ };
26
+
27
+ /** Move only sibling regions, leaving a plugin's internal structure intact. */
28
+ export const placeStatusIndicator = (
29
+ element: HTMLElement,
30
+ form: HTMLElement,
31
+ config: AgentWidgetStatusIndicatorConfig,
32
+ ): void => {
33
+ if (!form.parentElement || element.parentElement !== form.parentElement) return;
34
+ if (config.mode === "transient") {
35
+ if (element.nextElementSibling !== form) form.before(element);
36
+ } else if (form.nextElementSibling !== element) {
37
+ form.after(element);
38
+ }
39
+ };