@runtypelabs/persona 4.23.2 → 4.24.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 (156) 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 +215 -37
  12. package/dist/approval-ui.d.ts +215 -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 +210 -25
  16. package/dist/artifacts-ui.d.ts +210 -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 +210 -25
  21. package/dist/context-mentions.d.ts +210 -25
  22. package/dist/event-stream-view.d.cts +210 -25
  23. package/dist/event-stream-view.d.ts +210 -25
  24. package/dist/forms-ui.d.cts +245 -27
  25. package/dist/forms-ui.d.ts +245 -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 +270 -39
  31. package/dist/index.d.ts +270 -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 +210 -25
  41. package/dist/session-reconnect.d.ts +210 -25
  42. package/dist/smart-dom-reader.d.cts +210 -25
  43. package/dist/smart-dom-reader.d.ts +210 -25
  44. package/dist/theme-editor-preview.cjs +74 -73
  45. package/dist/theme-editor-preview.d.cts +217 -25
  46. package/dist/theme-editor-preview.d.ts +217 -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 +230 -27
  50. package/dist/theme-editor.d.ts +230 -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/widget.css +1 -1
  57. package/package.json +3 -2
  58. package/src/__fixtures__/defaults-parity.v4.json +776 -0
  59. package/src/approval-ui-entry.ts +0 -1
  60. package/src/approval-ui-loader.ts +5 -4
  61. package/src/approval-ui.ts +0 -1
  62. package/src/client-identity-proof.test.ts +193 -0
  63. package/src/client-session-warm.test.ts +216 -0
  64. package/src/client-visitor-history.test.ts +6 -0
  65. package/src/client.ts +122 -30
  66. package/src/codegen.test.ts +24 -0
  67. package/src/components/activity-row.test.ts +100 -0
  68. package/src/components/activity-row.ts +136 -0
  69. package/src/components/approval-actions.test.ts +8 -5
  70. package/src/components/approval-actions.ts +19 -6
  71. package/src/components/approval-bubble.test.ts +56 -9
  72. package/src/components/approval-bubble.ts +36 -41
  73. package/src/components/composer-builder.ts +5 -2
  74. package/src/components/composer-parts.ts +10 -2
  75. package/src/components/header-builder.test.ts +32 -0
  76. package/src/components/header-builder.ts +7 -4
  77. package/src/components/header-layouts.test.ts +12 -0
  78. package/src/components/header-layouts.ts +3 -2
  79. package/src/components/launcher.ts +11 -2
  80. package/src/components/message-bubble.test.ts +22 -0
  81. package/src/components/message-bubble.ts +7 -3
  82. package/src/components/panel.test.ts +52 -1
  83. package/src/components/panel.ts +47 -8
  84. package/src/components/pill-composer-builder.test.ts +2 -0
  85. package/src/components/pill-composer-builder.ts +0 -1
  86. package/src/components/reasoning-bubble.ts +29 -17
  87. package/src/components/suggestions.test.ts +1 -0
  88. package/src/components/suggestions.ts +0 -16
  89. package/src/components/tool-bubble.ts +131 -114
  90. package/src/components/tool-details.test.ts +87 -0
  91. package/src/components/tool-details.ts +102 -0
  92. package/src/components/welcome.test.ts +8 -0
  93. package/src/components/welcome.ts +4 -2
  94. package/src/defaults-parity.test.ts +158 -0
  95. package/src/defaults.test.ts +69 -0
  96. package/src/defaults.ts +128 -49
  97. package/src/generated/runtype-openapi-contract.ts +2 -2
  98. package/src/index-core.ts +2 -0
  99. package/src/install.test.ts +24 -0
  100. package/src/install.ts +3 -1
  101. package/src/launcher-global.ts +2 -1
  102. package/src/launcher-variants.test.ts +52 -0
  103. package/src/runtime/init-update-reset.test.ts +21 -0
  104. package/src/session.ts +29 -6
  105. package/src/styles/widget-styles.test.ts +19 -0
  106. package/src/styles/widget.css +295 -23
  107. package/src/theme-editor/index.ts +2 -0
  108. package/src/theme-editor/preview-utils.test.ts +8 -0
  109. package/src/theme-editor/preview-utils.ts +7 -3
  110. package/src/theme-editor/sections.test.ts +31 -0
  111. package/src/theme-editor/sections.ts +55 -1
  112. package/src/theme-editor/state-preferences.test.ts +49 -0
  113. package/src/theme-editor/state.ts +73 -15
  114. package/src/theme-reference.test.ts +20 -0
  115. package/src/theme-reference.ts +11 -5
  116. package/src/tool-call-display-defaults.test.ts +33 -1
  117. package/src/types/theme.ts +80 -3
  118. package/src/types.ts +136 -22
  119. package/src/ui.activity-row.test.ts +91 -0
  120. package/src/ui.approval-plugin.test.ts +63 -0
  121. package/src/ui.composer-compact.test.ts +24 -2
  122. package/src/ui.composer-placement.test.ts +23 -0
  123. package/src/ui.detached-panel.test.ts +33 -6
  124. package/src/ui.docked.test.ts +19 -0
  125. package/src/ui.header-update-stability.test.ts +25 -0
  126. package/src/ui.identity-proof.test.ts +58 -0
  127. package/src/ui.launcher-teaser.test.ts +25 -0
  128. package/src/ui.message-width.test.ts +17 -2
  129. package/src/ui.reasoning-display.test.ts +92 -0
  130. package/src/ui.rebuild-composer.test.ts +20 -3
  131. package/src/ui.scroll.test.ts +32 -0
  132. package/src/ui.session-init.test.ts +312 -0
  133. package/src/ui.status-indicator.test.ts +82 -0
  134. package/src/ui.suggest-replies.test.ts +11 -0
  135. package/src/ui.ts +363 -74
  136. package/src/ui.v5-defaults.test.ts +144 -0
  137. package/src/ui.welcome.test.ts +92 -3
  138. package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
  139. package/src/utils/code-generators.ts +14 -0
  140. package/src/utils/composer-compact.test.ts +0 -2
  141. package/src/utils/composer-compact.ts +2 -4
  142. package/src/utils/composer-placement.ts +2 -3
  143. package/src/utils/config-merge.test.ts +81 -5
  144. package/src/utils/config-merge.ts +13 -3
  145. package/src/utils/defaults-provenance.ts +67 -0
  146. package/src/utils/dock.ts +7 -1
  147. package/src/utils/launcher-variant.ts +16 -0
  148. package/src/utils/panel-config.ts +97 -0
  149. package/src/utils/status-indicator.ts +39 -0
  150. package/src/utils/theme.test.ts +182 -0
  151. package/src/utils/theme.ts +131 -18
  152. package/src/utils/tokens.test.ts +122 -1
  153. package/src/utils/tokens.ts +177 -17
  154. package/src/welcome.test.ts +23 -0
  155. package/src/welcome.ts +16 -9
  156. package/dist/approval-ui-K7R5Q5OC.js +0 -1
@@ -5,12 +5,43 @@ import {
5
5
  COMPONENT_SHAPE_SECTIONS,
6
6
  COMPONENTS_SECTIONS,
7
7
  CONFIGURE_SECTIONS,
8
+ getThemeEditorTabs,
8
9
  INTERFACE_ROLES_SECTION,
10
+ resolveEditorFieldDefault,
9
11
  STYLE_SECTIONS,
10
12
  } from "./sections";
13
+ import { resolveDefaults } from "../defaults";
14
+ import { resolveThemeDefaults } from "../utils/tokens";
11
15
  import { ALL_ROLES } from "./role-mappings";
12
16
 
13
17
  describe("theme editor scroll-to-bottom controls", () => {
18
+ it("resolves field hints from the selected shared defaults tree", () => {
19
+ const v4Tabs = getThemeEditorTabs();
20
+ const v5Tabs = getThemeEditorTabs({ v5Defaults: true });
21
+ const headerIcon = v4Tabs
22
+ .flatMap((tab) => tab.sections)
23
+ .flatMap((section) => section.fields)
24
+ .find((field) => field.path === "launcher.headerIconSize");
25
+
26
+ expect(headerIcon?.defaultValue).toBe(
27
+ resolveDefaults().launcher?.headerIconSize
28
+ );
29
+ expect(resolveEditorFieldDefault({
30
+ id: "theme-surface",
31
+ label: "Surface",
32
+ type: "token-ref",
33
+ path: "theme.semantic.colors.surface",
34
+ }, { v5Defaults: true })).toBe(
35
+ resolveThemeDefaults({ v5Defaults: true }).semantic.colors.surface
36
+ );
37
+ const v5HeaderIcon = v5Tabs.flatMap((tab) => tab.sections)
38
+ .flatMap((section) => section.fields)
39
+ .find((field) => field.path === "launcher.headerIconSize");
40
+ expect(v5HeaderIcon?.defaultValue).toBe("20px");
41
+ expect(headerIcon?.defaultValue).toBe("40px");
42
+ expect(v5Tabs.map((tab) => tab.id)).toEqual(v4Tabs.map((tab) => tab.id));
43
+ });
44
+
14
45
  it("exposes clear style semantics and independent role width controls", () => {
15
46
  const section = CONFIGURE_SECTIONS.find((entry) => entry.id === "messages-layout");
16
47
  const fieldsByPath = new Map(
@@ -5,11 +5,14 @@ import {
5
5
  DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
6
6
  DEFAULT_FLOATING_LAUNCHER_WIDTH,
7
7
  DEFAULT_INPUT_PLACEHOLDER,
8
+ resolveDefaults,
8
9
  } from '../defaults';
9
10
  import {
10
11
  DEFAULT_PANEL_CANVAS_BACKGROUND,
11
12
  DEFAULT_PANEL_INSET,
13
+ resolveThemeDefaults,
12
14
  } from '../utils/tokens';
15
+ import type { AgentWidgetConfig } from '../types';
13
16
  import { COLOR_FAMILIES } from './color-utils';
14
17
  import {
15
18
  ROLE_SURFACES,
@@ -1336,12 +1339,63 @@ export const STYLE_SECTIONS_V2: SectionDef[] = [
1336
1339
  // ALL TABS
1337
1340
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1338
1341
 
1339
- export const ALL_TABS: TabDef[] = [
1342
+ const ALL_TABS_BASE: TabDef[] = [
1340
1343
  { id: 'style', label: 'Style', sections: STYLE_SECTIONS },
1341
1344
  { id: 'design-system', label: 'Design System', sections: COMPONENTS_SECTIONS },
1342
1345
  { id: 'configure', label: 'Configure', sections: CONFIGURE_SECTIONS },
1343
1346
  ];
1344
1347
 
1348
+ const getByPath = (value: unknown, path: string): unknown =>
1349
+ path.split('.').reduce<unknown>((current, key) =>
1350
+ current && typeof current === 'object'
1351
+ ? (current as Record<string, unknown>)[key]
1352
+ : undefined, value);
1353
+
1354
+ /**
1355
+ * Resolves editor field metadata from the same versioned config and token
1356
+ * defaults as the widget. The static field value remains the fallback for
1357
+ * presentation-only values (such as intentionally blank optional tokens).
1358
+ */
1359
+ export function resolveEditorFieldDefault(
1360
+ field: FieldDef,
1361
+ future?: AgentWidgetConfig['future']
1362
+ ): unknown {
1363
+ const configDefaults = resolveDefaults({ future });
1364
+ const themeDefaults = resolveThemeDefaults(future);
1365
+ const path = field.path;
1366
+ const themePath = path.startsWith('theme.')
1367
+ ? path.slice('theme.'.length)
1368
+ : path.startsWith('darkTheme.')
1369
+ ? path.slice('darkTheme.'.length)
1370
+ : undefined;
1371
+ const resolved = themePath
1372
+ ? getByPath(themeDefaults, themePath)
1373
+ : getByPath(configDefaults, path);
1374
+ return resolved === undefined ? field.defaultValue : resolved;
1375
+ }
1376
+
1377
+ /**
1378
+ * Returns editor tabs with default metadata resolved for the selected defaults
1379
+ * version. Callers keep authored values separate; this only drives UI hints.
1380
+ */
1381
+ export function getThemeEditorTabs(
1382
+ future?: AgentWidgetConfig['future']
1383
+ ): TabDef[] {
1384
+ return ALL_TABS_BASE.map((tab) => ({
1385
+ ...tab,
1386
+ sections: tab.sections.map((section) => ({
1387
+ ...section,
1388
+ fields: section.fields.map((field) => ({
1389
+ ...field,
1390
+ defaultValue: resolveEditorFieldDefault(field, future),
1391
+ })),
1392
+ })),
1393
+ }));
1394
+ }
1395
+
1396
+ /** Legacy editor metadata, resolved through the v4 defaults tree. */
1397
+ export const ALL_TABS: TabDef[] = getThemeEditorTabs();
1398
+
1345
1399
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1346
1400
  // HELPERS: light/dark scoping for dual-mode editing
1347
1401
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -12,4 +12,53 @@ describe("ThemeEditorState preference reset", () => {
12
12
  expect(state.get("features.artifacts.display.files")).toBeUndefined();
13
13
  expect(state.getConfig()).not.toHaveProperty("features");
14
14
  });
15
+
16
+ it("preserves an authored v5 defaults opt-in across state reset", () => {
17
+ const state = new ThemeEditorState(
18
+ undefined,
19
+ { future: { v5Defaults: true } },
20
+ );
21
+
22
+ expect(state.getConfig().future?.v5Defaults).toBe(true);
23
+ expect(state.exportSnapshot().config.future).toEqual({ v5Defaults: true });
24
+
25
+ state.resetToDefaults();
26
+
27
+ expect(state.getConfig().future?.v5Defaults).toBe(true);
28
+ });
29
+ });
30
+
31
+ describe('version switching preserves authored values', () => {
32
+ it('rebases config and theme in both directions, including undo/redo', () => {
33
+ const state = new ThemeEditorState();
34
+ state.set('theme.components.header.controlSize', '32px');
35
+ state.set('features.toolCallDisplay.autoExpand', true);
36
+ state.set('future.v5Defaults', true);
37
+ expect(state.get('features.toolCallDisplay.variant')).toBe('row');
38
+ expect(state.get('features.toolCallDisplay.autoExpand')).toBe(true);
39
+ expect(state.get('theme.components.header.controlSize')).toBe('32px');
40
+ expect(state.get('theme.components.header.controlIconSize')).toBe('18px');
41
+ state.undo();
42
+ state.redo();
43
+ state.unset('future.v5Defaults');
44
+ expect(state.get('features.toolCallDisplay.variant')).toBeUndefined();
45
+ expect(state.get('theme.components.header.controlIconSize')).toBeUndefined();
46
+ state.set('future.v5Defaults', true);
47
+ expect(state.get('theme.components.header.controlSize')).toBe('32px');
48
+ });
49
+ it('preserves constructor overrides and same-batch edits regardless of order', () => {
50
+ const state = new ThemeEditorState({ components: { header: { controlSize: '40px' } } }, { features: { toolCallDisplay: { autoExpand: true } } });
51
+ state.setBatch({ 'theme.components.header.controlIconSize': '22px', 'future.v5Defaults': true });
52
+ expect(state.get('features.toolCallDisplay.variant')).toBe('row');
53
+ expect(state.get('theme.components.header.controlSize')).toBe('40px');
54
+ expect(state.get('theme.components.header.controlIconSize')).toBe('22px');
55
+ state.setBatch({ 'future.v5Defaults': false, 'theme.components.header.controlSize': '44px' });
56
+ expect(state.get('theme.components.header.controlSize')).toBe('44px');
57
+ });
58
+ it('keeps sparse configs sparse when defaults merging is disabled', () => {
59
+ const state = new ThemeEditorState(undefined, {}, { mergeDefaults: false });
60
+ state.set('future.v5Defaults', true);
61
+ expect(state.get('features')).toBeUndefined();
62
+ expect(state.get('theme.components.header.controlSize')).toBe('28px');
63
+ });
15
64
  });
@@ -1,9 +1,10 @@
1
1
  /** Headless state management for the theme editor (no DOM, no localStorage, no side effects) */
2
2
 
3
3
  import type { AgentWidgetConfig } from '../types';
4
- import type { PersonaTheme } from '../types/theme';
4
+ import type { DeepPartial, PersonaTheme } from '../types/theme';
5
5
  import { createTheme } from '../utils/theme';
6
- import { DEFAULT_WIDGET_CONFIG } from '../defaults';
6
+ import { resolveDefaults, resolveDefaultsVersion } from '../defaults';
7
+ import { deepMerge } from '../utils/deep-merge';
7
8
  import type { ConfiguratorSnapshot, ConfigChangeListener } from './types';
8
9
 
9
10
  // ─── Dot-path utilities ─────────────────────────────────────────
@@ -65,18 +66,29 @@ export class ThemeEditorState {
65
66
  private history: ConfiguratorSnapshot[] = [];
66
67
  private historyIndex = -1;
67
68
  private suppressHistory = false;
69
+ private authoredConfig: Partial<AgentWidgetConfig> = {};
70
+ private authoredTheme: DeepPartial<PersonaTheme> = {};
71
+ private mergeDefaults = true;
72
+ private snapshotIntent = new WeakMap<ConfiguratorSnapshot, { config: Partial<AgentWidgetConfig>; theme: DeepPartial<PersonaTheme> }>();
68
73
 
69
74
  constructor(
70
- initialTheme?: Partial<PersonaTheme>,
75
+ initialTheme?: DeepPartial<PersonaTheme>,
71
76
  initialConfig?: Partial<AgentWidgetConfig>,
72
77
  options?: { mergeDefaults?: boolean }
73
78
  ) {
74
79
  const mergeDefaults = options?.mergeDefaults ?? true;
80
+ this.mergeDefaults = mergeDefaults;
81
+ this.authoredConfig = { ...initialConfig };
82
+ this.authoredTheme = initialTheme ?? {};
83
+ const defaults = resolveDefaults(initialConfig);
75
84
  this.config = (mergeDefaults
76
- ? { ...DEFAULT_WIDGET_CONFIG, ...initialConfig }
77
- : (initialConfig ?? DEFAULT_WIDGET_CONFIG)
85
+ ? { ...defaults, ...initialConfig }
86
+ : (initialConfig ?? defaults)
78
87
  ) as AgentWidgetConfig;
79
- this.theme = createTheme(initialTheme, { validate: false });
88
+ this.theme = createTheme(initialTheme, {
89
+ validate: false,
90
+ future: this.config.future,
91
+ });
80
92
  this.syncThemeIntoConfig();
81
93
  this.pushHistorySnapshot(this.exportSnapshot(), true);
82
94
  }
@@ -116,13 +128,15 @@ export class ThemeEditorState {
116
128
  * - everything else → writes into AgentWidgetConfig
117
129
  */
118
130
  set(path: string, value: unknown): void {
131
+ const previousVersion = resolveDefaultsVersion(this.config);
132
+ this.recordIntent(path, value);
119
133
  if (path.startsWith('theme.')) {
120
134
  const themePath = path.replace('theme.', '');
121
135
  this.theme = setByPath(this.theme, themePath, value) as PersonaTheme;
122
136
  this.syncThemeIntoConfig();
123
137
  } else if (path.startsWith('darkTheme.')) {
124
138
  const themePath = path.replace('darkTheme.', '');
125
- const dark = this.config.darkTheme ?? createTheme();
139
+ const dark = this.config.darkTheme ?? createTheme(undefined, { future: this.config.future });
126
140
  this.config = {
127
141
  ...this.config,
128
142
  darkTheme: setByPath(dark, themePath, value) as AgentWidgetConfig['darkTheme'],
@@ -131,12 +145,15 @@ export class ThemeEditorState {
131
145
  this.config = setByPath(this.config, path, value) as AgentWidgetConfig;
132
146
  }
133
147
 
148
+ this.rebaseDefaults(previousVersion);
134
149
  this.recordHistory();
135
150
  this.notifyListeners();
136
151
  }
137
152
 
138
153
  /** Delete a dot-path and prune empty parents so sparse preferences inherit. */
139
154
  unset(path: string): void {
155
+ const previousVersion = resolveDefaultsVersion(this.config);
156
+ this.recordIntent(path, undefined, true);
140
157
  if (path.startsWith('theme.')) {
141
158
  const themePath = path.replace('theme.', '');
142
159
  this.theme = unsetByPath(this.theme, themePath) as PersonaTheme;
@@ -153,24 +170,27 @@ export class ThemeEditorState {
153
170
  } else {
154
171
  this.config = unsetByPath(this.config, path) as AgentWidgetConfig;
155
172
  }
173
+ this.rebaseDefaults(previousVersion);
156
174
  this.recordHistory();
157
175
  this.notifyListeners();
158
176
  }
159
177
 
160
178
  /** Batch-set multiple paths at once */
161
179
  setBatch(updates: Record<string, unknown>): void {
180
+ const previousVersion = resolveDefaultsVersion(this.config);
162
181
  let themeChanged = false;
163
182
  let darkThemeChanged = false;
164
183
  let configChanged = false;
165
184
 
166
185
  for (const [path, value] of Object.entries(updates)) {
186
+ this.recordIntent(path, value);
167
187
  if (path.startsWith('theme.')) {
168
188
  const themePath = path.replace('theme.', '');
169
189
  this.theme = setByPath(this.theme, themePath, value) as PersonaTheme;
170
190
  themeChanged = true;
171
191
  } else if (path.startsWith('darkTheme.')) {
172
192
  const themePath = path.replace('darkTheme.', '');
173
- const dark = this.config.darkTheme ?? createTheme();
193
+ const dark = this.config.darkTheme ?? createTheme(undefined, { future: this.config.future });
174
194
  this.config = {
175
195
  ...this.config,
176
196
  darkTheme: setByPath(dark, themePath, value) as AgentWidgetConfig['darkTheme'],
@@ -185,6 +205,7 @@ export class ThemeEditorState {
185
205
  if (themeChanged) {
186
206
  this.syncThemeIntoConfig();
187
207
  }
208
+ this.rebaseDefaults(previousVersion);
188
209
  if (themeChanged || darkThemeChanged || configChanged) {
189
210
  this.recordHistory();
190
211
  this.notifyListeners();
@@ -193,6 +214,7 @@ export class ThemeEditorState {
193
214
 
194
215
  /** Replace the entire theme */
195
216
  setTheme(theme: PersonaTheme): void {
217
+ this.authoredTheme = theme;
196
218
  this.theme = theme;
197
219
  this.syncThemeIntoConfig();
198
220
  this.recordHistory();
@@ -201,6 +223,8 @@ export class ThemeEditorState {
201
223
 
202
224
  /** Replace the entire config (for preset loading) */
203
225
  setFullConfig(config: AgentWidgetConfig, theme?: PersonaTheme): void {
226
+ this.authoredConfig = { ...config };
227
+ if (theme) this.authoredTheme = theme;
204
228
  this.config = { ...config };
205
229
  if (theme) {
206
230
  this.theme = theme;
@@ -221,21 +245,27 @@ export class ThemeEditorState {
221
245
  if ('config' in parsed || 'theme' in parsed || parsed.version === 2) {
222
246
  const config = (parsed.config ?? this.config) as AgentWidgetConfig;
223
247
  const theme = createTheme(
224
- (parsed.theme ?? this.theme) as Partial<PersonaTheme>,
225
- { validate: false }
248
+ (parsed.theme ?? this.theme) as DeepPartial<PersonaTheme>,
249
+ { validate: false, future: config.future }
226
250
  );
227
251
  this.setFullConfig(config, theme);
228
252
  return;
229
253
  }
230
254
 
231
- const theme = createTheme(parsed as Partial<PersonaTheme>, { validate: false });
255
+ const theme = createTheme(parsed as DeepPartial<PersonaTheme>, { validate: false, future: this.config.future });
232
256
  this.setTheme(theme);
233
257
  }
234
258
 
235
259
  /** Reset to defaults */
236
260
  resetToDefaults(): void {
237
- this.config = { ...DEFAULT_WIDGET_CONFIG } as AgentWidgetConfig;
238
- this.theme = createTheme();
261
+ const future = this.config.future;
262
+ this.authoredConfig = future ? { future } : {};
263
+ this.authoredTheme = {};
264
+ this.config = {
265
+ ...resolveDefaults({ future }),
266
+ ...(future ? { future } : {}),
267
+ } as AgentWidgetConfig;
268
+ this.theme = createTheme(undefined, { future: this.config.future });
239
269
  this.syncThemeIntoConfig();
240
270
  this.history = [];
241
271
  this.historyIndex = -1;
@@ -295,6 +325,27 @@ export class ThemeEditorState {
295
325
 
296
326
  // ─── Private ────────────────────────────────────────────────
297
327
 
328
+ private recordIntent(path: string, value: unknown, remove = false): void {
329
+ const write = (target: object, key: string) => remove || value === undefined
330
+ ? unsetByPath(target, key) : setByPath(target, key, value);
331
+ if (path.startsWith('theme.')) {
332
+ this.authoredTheme = write(this.authoredTheme, path.slice(6)) as DeepPartial<PersonaTheme>;
333
+ } else if (path === 'theme') {
334
+ this.authoredTheme = (value ?? {}) as DeepPartial<PersonaTheme>;
335
+ } else {
336
+ this.authoredConfig = write(this.authoredConfig, path) as Partial<AgentWidgetConfig>;
337
+ }
338
+ }
339
+
340
+ private rebaseDefaults(previousVersion: string): void {
341
+ if (previousVersion === resolveDefaultsVersion(this.config)) return;
342
+ this.config = (this.mergeDefaults
343
+ ? deepMerge(resolveDefaults(this.authoredConfig), this.authoredConfig)
344
+ : { ...this.authoredConfig }) as AgentWidgetConfig;
345
+ this.theme = createTheme(this.authoredTheme, { validate: false, future: this.config.future });
346
+ this.syncThemeIntoConfig();
347
+ }
348
+
298
349
  private syncThemeIntoConfig(): void {
299
350
  this.config = {
300
351
  ...this.config,
@@ -314,6 +365,7 @@ export class ThemeEditorState {
314
365
 
315
366
  private pushHistorySnapshot(snapshot: ConfiguratorSnapshot, replaceCurrent = false): void {
316
367
  if (this.suppressHistory) return;
368
+ this.snapshotIntent.set(snapshot, { config: this.authoredConfig, theme: this.authoredTheme });
317
369
 
318
370
  const serialized = JSON.stringify(snapshot);
319
371
  const currentSerialized =
@@ -326,7 +378,10 @@ export class ThemeEditorState {
326
378
  return;
327
379
  }
328
380
 
329
- if (serialized === currentSerialized) return;
381
+ const currentIntent = this.historyIndex >= 0
382
+ ? this.snapshotIntent.get(this.history[this.historyIndex]) : undefined;
383
+ if (serialized === currentSerialized &&
384
+ JSON.stringify(this.snapshotIntent.get(snapshot)) === JSON.stringify(currentIntent)) return;
330
385
 
331
386
  this.history = this.history.slice(0, this.historyIndex + 1);
332
387
  this.history.push(snapshot);
@@ -335,8 +390,11 @@ export class ThemeEditorState {
335
390
 
336
391
  private restoreSnapshot(snapshot: ConfiguratorSnapshot): void {
337
392
  this.suppressHistory = true;
393
+ const intent = this.snapshotIntent.get(snapshot);
394
+ this.authoredConfig = intent?.config ?? snapshot.config as Partial<AgentWidgetConfig>;
395
+ this.authoredTheme = intent?.theme ?? snapshot.theme;
338
396
  this.config = snapshot.config as unknown as AgentWidgetConfig;
339
- this.theme = createTheme(snapshot.theme, { validate: false });
397
+ this.theme = createTheme(snapshot.theme, { validate: false, future: this.config.future });
340
398
  this.syncThemeIntoConfig();
341
399
  this.suppressHistory = false;
342
400
  this.notifyListeners();
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { getThemeReference } from "./theme-reference";
4
+ import { resolveDefaults } from "./defaults";
5
+ import { resolveThemeDefaults } from "./utils/tokens";
6
+
7
+ describe.each([false, true])("theme reference defaults versioning (v5=%s)", (v5Defaults) => {
8
+ it("reads its documented defaults from the shared future-aware resolvers", () => {
9
+ const reference = getThemeReference({ v5Defaults });
10
+ const future = { v5Defaults };
11
+
12
+ expect(reference.defaultsVersion).toBe(v5Defaults ? "v5" : "v4");
13
+ expect(reference.themeDefaultsVersion).toBe(v5Defaults ? "v5" : "v4");
14
+ expect(reference.defaultColorPalette).toBe(
15
+ resolveThemeDefaults(future).palette.colors
16
+ );
17
+ expect(reference.defaultRadius).toBe(resolveThemeDefaults(future).palette.radius);
18
+ expect(reference.defaultLauncher).toEqual(resolveDefaults({ future }).launcher);
19
+ });
20
+ });
@@ -5,7 +5,8 @@
5
5
  * out of the IIFE widget bundle. Intended for AI/MCP tool consumption.
6
6
  */
7
7
 
8
- import { DEFAULT_PALETTE } from './utils/tokens'
8
+ import { resolveThemeDefaults, resolveThemeDefaultsVersion } from './utils/tokens'
9
+ import { resolveDefaults, resolveDefaultsVersion } from './defaults'
9
10
  import type { DeepPartial, PersonaTheme } from './types/theme'
10
11
 
11
12
  // ---------------------------------------------------------------------------
@@ -173,7 +174,7 @@ export const THEME_TOKEN_DOCS = {
173
174
  'Shared scrollbar appearance for every scroller: thumb (default semantic.colors.border), track (default transparent).',
174
175
  reasoningBubble: 'shadow: reasoning/thinking row box-shadow.',
175
176
  composer:
176
- 'Message input form: shadow (box-shadow), borderColor, padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type), controlSize (edge of every icon control in the action row: attachment, mention, mic, action buttons, overflow trigger, icon-mode send; default 40px) and controlIconSize (glyph inside them, default 24px). Per-control config keys (sendButton.size, voiceRecognition.iconSize) override controlSize; coarse-pointer devices floor the hit area at 40px. overlayBand paints the band behind an overlaid footer (composer.placement: "overlay"). segmented styles a segmented mode-group track (trackBackground, trackBorderRadius, padding, activeBackground, activeForeground, activeShadow, inactiveForeground, itemPadding); every key is optional and the stylesheet owns the defaults. modelPicker styles the model picker (background, hoverBackground, borderColor, borderRadius and labelColor on the closed control, which the native select and the popover trigger share; menuBackground, menuBorderRadius, rowHoverBackground, descriptionColor and suffixColor on the popover only), also all optional. overflowMenu styles the composer overflow "+" menu panel (background, borderColor, borderRadius, foreground, shadow), also all optional.',
177
+ 'Message input form: sendIconSize (send/stop glyph; explicit sendButton.iconSize wins), sendButtonRadius (icon-mode submit radius), footerBorder (footer top border shorthand; none hides it), shadow (box-shadow), borderColor, focusBorderColor (focused form border; V4 keeps the resting border, V5 uses a stronger neutral mix), focusRing (CSS outline shorthand, 1px offset; V4 none, V5 a thin neutral ring; preserves shadow and causes no layout shift), padding (form inset), gap (textarea row to actions row), fontSize and lineHeight (textarea type), controlSize (edge of every icon control in the action row: attachment, mention, mic, action buttons, overflow trigger, icon-mode send; default 40px) and controlIconSize (glyph inside them, default 24px). Per-control config keys (sendButton.size, voiceRecognition.iconSize) override controlSize; coarse-pointer devices floor the hit area at 40px. overlayBand paints the band behind an overlaid footer (composer.placement: "overlay"). segmented styles a segmented mode-group track (trackBackground, trackBorderRadius, padding, activeBackground, activeForeground, activeShadow, inactiveForeground, itemPadding); every key is optional and the stylesheet owns the defaults. modelPicker styles the model picker (background, hoverBackground, borderColor, borderRadius and labelColor on the closed control, which the native select and the popover trigger share; menuBackground, menuBorderRadius, rowHoverBackground, descriptionColor and suffixColor on the popover only), also all optional. overflowMenu styles the composer overflow "+" menu panel (background, borderColor, borderRadius, foreground, shadow), also all optional.',
177
178
  artifact:
178
179
  'toolbar (icon styling, copy menu), tab (background, active states), pane (background, toolbarBackground).',
179
180
  },
@@ -466,11 +467,16 @@ export const THEME_EXAMPLES: Record<string, ThemeExample> = {
466
467
  * Returns token system docs, the default color palette and radius scale,
467
468
  * example themes, and a list of SDK-bundled presets.
468
469
  */
469
- export function getThemeReference() {
470
+ export function getThemeReference(future?: { v5Defaults?: boolean }) {
471
+ const themeDefaults = resolveThemeDefaults(future)
472
+ const defaults = resolveDefaults({ future })
470
473
  return {
471
474
  tokenDocs: THEME_TOKEN_DOCS,
472
- defaultColorPalette: DEFAULT_PALETTE.colors,
473
- defaultRadius: DEFAULT_PALETTE.radius,
475
+ defaultsVersion: resolveDefaultsVersion({ future }),
476
+ themeDefaultsVersion: resolveThemeDefaultsVersion(future),
477
+ defaultColorPalette: themeDefaults.palette.colors,
478
+ defaultRadius: themeDefaults.palette.radius,
479
+ defaultLauncher: defaults.launcher,
474
480
  examples: THEME_EXAMPLES,
475
481
  sdkPresets: ['shop', 'minimal', 'fullscreen'],
476
482
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { DEFAULT_WIDGET_CONFIG } from "./defaults";
3
+ import { DEFAULT_WIDGET_CONFIG, mergeWithDefaults } from "./defaults";
4
4
 
5
5
  describe("tool call display defaults", () => {
6
6
  it("keeps advanced tool call transcript modes disabled by default", () => {
@@ -23,4 +23,36 @@ describe("tool call display defaults", () => {
23
23
  loadingAnimation: "none",
24
24
  });
25
25
  });
26
+
27
+ it("fills omitted tool display options when one option is overridden", () => {
28
+ const override = { collapsedMode: "tool-name" as const, expandable: false };
29
+ expect(mergeWithDefaults({ features: { toolCallDisplay: override } }).features?.toolCallDisplay)
30
+ .toEqual({ ...DEFAULT_WIDGET_CONFIG.features?.toolCallDisplay, ...override });
31
+ expect(DEFAULT_WIDGET_CONFIG.features?.toolCallDisplay?.collapsedMode).toBe("tool-call");
32
+ });
33
+
34
+ it("fills omitted reasoning display options without losing explicit false or zero", () => {
35
+ const override = { activePreview: false, previewMaxLines: 0, expandable: false };
36
+ expect(mergeWithDefaults({ features: { reasoningDisplay: override } }).features?.reasoningDisplay)
37
+ .toEqual({ ...DEFAULT_WIDGET_CONFIG.features?.reasoningDisplay, ...override });
38
+ expect(DEFAULT_WIDGET_CONFIG.features?.reasoningDisplay?.previewMaxLines).toBe(3);
39
+ });
40
+
41
+ it("retains optional display settings and leaves unrelated features intact", () => {
42
+ const result = mergeWithDefaults({
43
+ features: {
44
+ showToolCalls: false,
45
+ toolCallDisplay: { grouped: true, activeMinHeight: "100px", completedVisibility: "removed" },
46
+ reasoningDisplay: { loadingAnimation: "shimmer" },
47
+ },
48
+ });
49
+ expect(result.features?.showToolCalls).toBe(false);
50
+ expect(result.features?.toolCallDisplay?.activeMinHeight).toBe("100px");
51
+ expect(result.features?.toolCallDisplay?.completedVisibility).toBe("removed");
52
+ expect(result.features?.toolCallDisplay?.grouped).toBe(true);
53
+ expect(result.features?.reasoningDisplay).toEqual({
54
+ ...DEFAULT_WIDGET_CONFIG.features?.reasoningDisplay,
55
+ loadingAnimation: "shimmer",
56
+ });
57
+ });
26
58
  });