@runtypelabs/persona 4.12.0 → 4.14.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 (68) hide show
  1. package/dist/codegen.cjs +1 -1
  2. package/dist/codegen.js +1 -1
  3. package/dist/context-mentions.d.cts +98 -6
  4. package/dist/context-mentions.d.ts +98 -6
  5. package/dist/index.cjs +64 -64
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +183 -8
  8. package/dist/index.d.ts +183 -8
  9. package/dist/index.global.js +54 -54
  10. package/dist/index.global.js.map +1 -1
  11. package/dist/index.js +67 -67
  12. package/dist/index.js.map +1 -1
  13. package/dist/launcher.global.js +1 -1
  14. package/dist/launcher.global.js.map +1 -1
  15. package/dist/smart-dom-reader.d.cts +98 -6
  16. package/dist/smart-dom-reader.d.ts +98 -6
  17. package/dist/theme-editor-preview.cjs +66 -66
  18. package/dist/theme-editor-preview.d.cts +100 -6
  19. package/dist/theme-editor-preview.d.ts +100 -6
  20. package/dist/theme-editor-preview.js +75 -75
  21. package/dist/theme-editor.cjs +10 -10
  22. package/dist/theme-editor.d.cts +134 -7
  23. package/dist/theme-editor.d.ts +134 -7
  24. package/dist/theme-editor.js +10 -10
  25. package/dist/theme-reference.cjs +1 -1
  26. package/dist/widget.css +1 -1
  27. package/package.json +1 -1
  28. package/src/artifacts-session.test.ts +106 -0
  29. package/src/client.test.ts +2 -2
  30. package/src/client.ts +4 -1
  31. package/src/components/artifact-card.ts +13 -2
  32. package/src/components/artifact-inline.ts +11 -2
  33. package/src/components/composer-builder.test.ts +2 -0
  34. package/src/components/composer-builder.ts +1 -1
  35. package/src/components/composer-parts.test.ts +2 -0
  36. package/src/components/composer-parts.ts +1 -1
  37. package/src/components/message-bubble.test.ts +7 -1
  38. package/src/components/message-bubble.ts +11 -8
  39. package/src/index-core.ts +34 -0
  40. package/src/session.ts +45 -4
  41. package/src/styles/widget.css +10 -1
  42. package/src/theme-editor/index.ts +5 -0
  43. package/src/theme-editor/presets.test.ts +43 -0
  44. package/src/theme-editor/presets.ts +101 -87
  45. package/src/theme-editor/sections.test.ts +39 -7
  46. package/src/theme-editor/sections.ts +60 -8
  47. package/src/theme-editor/state-preferences.test.ts +15 -0
  48. package/src/theme-editor/state.ts +46 -0
  49. package/src/theme-editor/types.ts +15 -1
  50. package/src/theme-editor/webmcp/tools.test.ts +13 -0
  51. package/src/theme-editor/webmcp/tools.ts +5 -0
  52. package/src/types.ts +139 -9
  53. package/src/ui.artifact-pane-gating.test.ts +5 -5
  54. package/src/ui.config-preferences.test.ts +166 -0
  55. package/src/ui.scroll-additive.test.ts +365 -0
  56. package/src/ui.scroll.test.ts +19 -6
  57. package/src/ui.ts +158 -53
  58. package/src/ui.typing-indicator.test.ts +135 -0
  59. package/src/utils/artifact-display.test.ts +180 -16
  60. package/src/utils/artifact-display.ts +155 -14
  61. package/src/utils/auto-follow.test.ts +38 -0
  62. package/src/utils/auto-follow.ts +35 -0
  63. package/src/utils/config-merge.test.ts +60 -0
  64. package/src/utils/config-merge.ts +6 -1
  65. package/src/utils/feature-preferences.test.ts +326 -0
  66. package/src/utils/feature-preferences.ts +498 -0
  67. package/src/utils/theme.test.ts +92 -0
  68. package/src/utils/tokens.ts +7 -7
@@ -2,100 +2,114 @@
2
2
 
3
3
  import type { ThemeEditorPreset } from './types';
4
4
 
5
- export const BUILT_IN_PRESETS: ThemeEditorPreset[] = [
6
- {
7
- id: 'default-light',
8
- name: 'Default Light',
9
- description: 'Clean monochrome light theme',
10
- theme: {
11
- components: {
12
- artifact: {
13
- pane: {
14
- background: 'semantic.colors.container',
15
- toolbarBackground: 'semantic.colors.container',
16
- },
5
+ const DEFAULT_LIGHT_PRESET: ThemeEditorPreset = {
6
+ id: 'default-light',
7
+ name: 'Default Light',
8
+ description: 'Clean monochrome light theme',
9
+ theme: {
10
+ components: {
11
+ artifact: {
12
+ pane: {
13
+ background: 'semantic.colors.container',
14
+ toolbarBackground: 'semantic.colors.container',
17
15
  },
18
16
  },
19
17
  },
20
- preview: { primary: '#171717', surface: '#ffffff', accent: '#0f0f0f' },
21
- tags: ['light'],
22
18
  },
23
- {
24
- id: 'default-dark',
25
- name: 'Default Dark',
26
- description: 'Monochrome dark theme',
27
- theme: {
28
- palette: {
29
- colors: {
30
- primary: {
31
- 50: '#ffffff', 100: '#f5f5f5', 200: '#d4d4d4', 300: '#a3a3a3',
32
- 400: '#737373', 500: '#171717', 600: '#0f0f0f', 700: '#0a0a0a',
33
- 800: '#050505', 900: '#030303', 950: '#000000',
34
- },
35
- gray: {
36
- 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db',
37
- 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151',
38
- 800: '#1f2937', 900: '#111827', 950: '#030712',
39
- },
40
- secondary: {
41
- 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd',
42
- 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9',
43
- 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065',
44
- },
45
- accent: {
46
- 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9',
47
- 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490',
48
- 800: '#155e75', 900: '#164e63', 950: '#083344',
49
- },
50
- success: {
51
- 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac',
52
- 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d',
53
- 800: '#166534', 900: '#14532d',
54
- },
55
- warning: {
56
- 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047',
57
- 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207',
58
- 800: '#854d0e', 900: '#713f12',
59
- },
60
- error: {
61
- 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5',
62
- 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c',
63
- 800: '#991b1b', 900: '#7f1d1d',
64
- },
19
+ preview: { primary: '#171717', surface: '#ffffff', accent: '#0f0f0f' },
20
+ tags: ['light'],
21
+ };
22
+
23
+ const DEFAULT_DARK_PRESET: ThemeEditorPreset = {
24
+ id: 'default-dark',
25
+ name: 'Default Dark',
26
+ description: 'Monochrome dark theme',
27
+ theme: {
28
+ palette: {
29
+ colors: {
30
+ primary: {
31
+ 50: '#ffffff', 100: '#f5f5f5', 200: '#d4d4d4', 300: '#a3a3a3',
32
+ 400: '#737373', 500: '#171717', 600: '#0f0f0f', 700: '#0a0a0a',
33
+ 800: '#050505', 900: '#030303', 950: '#000000',
65
34
  },
66
- } as any,
67
- semantic: {
68
- colors: {
69
- primary: 'palette.colors.primary.400',
70
- secondary: 'palette.colors.gray.400',
71
- accent: 'palette.colors.primary.500',
72
- surface: 'palette.colors.gray.800',
73
- background: 'palette.colors.gray.900',
74
- container: 'palette.colors.gray.800',
75
- text: 'palette.colors.gray.100',
76
- textMuted: 'palette.colors.gray.400',
77
- textInverse: 'palette.colors.gray.900',
78
- border: 'palette.colors.gray.700',
79
- divider: 'palette.colors.gray.700',
80
- interactive: {
81
- default: 'palette.colors.primary.400',
82
- hover: 'palette.colors.primary.300',
83
- focus: 'palette.colors.primary.500',
84
- active: 'palette.colors.primary.600',
85
- disabled: 'palette.colors.gray.600',
86
- },
87
- feedback: {
88
- success: 'palette.colors.success.400',
89
- warning: 'palette.colors.warning.400',
90
- error: 'palette.colors.error.400',
91
- info: 'palette.colors.primary.400',
92
- },
35
+ gray: {
36
+ 50: '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb', 300: '#d1d5db',
37
+ 400: '#9ca3af', 500: '#6b7280', 600: '#4b5563', 700: '#374151',
38
+ 800: '#1f2937', 900: '#111827', 950: '#030712',
93
39
  },
94
- } as any,
95
- },
96
- preview: { primary: '#171717', surface: '#1f2937', accent: '#0f0f0f' },
97
- tags: ['dark'],
40
+ secondary: {
41
+ 50: '#f5f3ff', 100: '#ede9fe', 200: '#ddd6fe', 300: '#c4b5fd',
42
+ 400: '#a78bfa', 500: '#8b5cf6', 600: '#7c3aed', 700: '#6d28d9',
43
+ 800: '#5b21b6', 900: '#4c1d95', 950: '#2e1065',
44
+ },
45
+ accent: {
46
+ 50: '#ecfeff', 100: '#cffafe', 200: '#a5f3fc', 300: '#67e8f9',
47
+ 400: '#22d3ee', 500: '#06b6d4', 600: '#0891b2', 700: '#0e7490',
48
+ 800: '#155e75', 900: '#164e63', 950: '#083344',
49
+ },
50
+ success: {
51
+ 50: '#f0fdf4', 100: '#dcfce7', 200: '#bbf7d0', 300: '#86efac',
52
+ 400: '#4ade80', 500: '#22c55e', 600: '#16a34a', 700: '#15803d',
53
+ 800: '#166534', 900: '#14532d',
54
+ },
55
+ warning: {
56
+ 50: '#fefce8', 100: '#fef9c3', 200: '#fef08a', 300: '#fde047',
57
+ 400: '#facc15', 500: '#eab308', 600: '#ca8a04', 700: '#a16207',
58
+ 800: '#854d0e', 900: '#713f12',
59
+ },
60
+ error: {
61
+ 50: '#fef2f2', 100: '#fee2e2', 200: '#fecaca', 300: '#fca5a5',
62
+ 400: '#f87171', 500: '#ef4444', 600: '#dc2626', 700: '#b91c1c',
63
+ 800: '#991b1b', 900: '#7f1d1d',
64
+ },
65
+ },
66
+ } as any,
67
+ semantic: {
68
+ colors: {
69
+ primary: 'palette.colors.primary.400',
70
+ secondary: 'palette.colors.gray.400',
71
+ accent: 'palette.colors.primary.500',
72
+ surface: 'palette.colors.gray.800',
73
+ background: 'palette.colors.gray.900',
74
+ container: 'palette.colors.gray.800',
75
+ text: 'palette.colors.gray.100',
76
+ textMuted: 'palette.colors.gray.400',
77
+ textInverse: 'palette.colors.gray.900',
78
+ border: 'palette.colors.gray.700',
79
+ divider: 'palette.colors.gray.700',
80
+ interactive: {
81
+ default: 'palette.colors.primary.400',
82
+ hover: 'palette.colors.primary.300',
83
+ focus: 'palette.colors.primary.500',
84
+ active: 'palette.colors.primary.600',
85
+ disabled: 'palette.colors.gray.600',
86
+ },
87
+ feedback: {
88
+ success: 'palette.colors.success.400',
89
+ warning: 'palette.colors.warning.400',
90
+ error: 'palette.colors.error.400',
91
+ info: 'palette.colors.primary.400',
92
+ },
93
+ },
94
+ } as any,
95
+ },
96
+ preview: { primary: '#171717', surface: '#1f2937', accent: '#0f0f0f' },
97
+ tags: ['dark'],
98
+ };
99
+
100
+ export const BUILT_IN_PRESETS: ThemeEditorPreset[] = [
101
+ {
102
+ id: 'persona-default',
103
+ name: 'Persona Default',
104
+ description: 'Adaptive Persona theme with light and dark modes',
105
+ theme: DEFAULT_LIGHT_PRESET.theme,
106
+ darkTheme: DEFAULT_DARK_PRESET.theme,
107
+ preview: DEFAULT_LIGHT_PRESET.preview,
108
+ darkPreview: DEFAULT_DARK_PRESET.preview,
109
+ tags: ['light', 'dark', 'adaptive'],
98
110
  },
111
+ DEFAULT_LIGHT_PRESET,
112
+ DEFAULT_DARK_PRESET,
99
113
  {
100
114
  id: 'high-contrast',
101
115
  name: 'High Contrast',
@@ -99,16 +99,45 @@ describe("theme editor scroll-to-bottom controls", () => {
99
99
  expect(INTERFACE_ROLES_SECTION.fields.some((field) => field.id === "role-scroll-to-bottom")).toBe(true);
100
100
  });
101
101
 
102
- it("exposes grouped and collapsed tool call preview controls", () => {
102
+ it("moves persistable stream controls out of developer debug without duplicates", () => {
103
103
  const debugSection = CONFIGURE_SECTIONS.find((section) => section.id === "debug-inspection");
104
+ const preferenceSection = CONFIGURE_SECTIONS.find(
105
+ (section) => section.id === "stream-display-preferences"
106
+ );
104
107
 
105
- expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.collapsedMode")).toBe(true);
108
+ expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.collapsedMode")).toBe(false);
106
109
  expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.activePreview")).toBe(true);
107
110
  expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.previewMaxLines")).toBe(true);
108
111
  expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.activeMinHeight")).toBe(true);
109
- expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.expandable")).toBe(true);
110
- expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.grouped")).toBe(true);
111
- expect(debugSection?.fields.some((field) => field.path === "features.toolCallDisplay.groupedMode")).toBe(true);
112
+ expect(preferenceSection?.fields.some((field) => field.path === "features.toolCallDisplay.collapsedMode")).toBe(true);
113
+ expect(preferenceSection?.fields.some((field) => field.path === "features.toolCallDisplay.expandable")).toBe(true);
114
+ expect(preferenceSection?.fields.some((field) => field.path === "features.toolCallDisplay.grouped")).toBe(true);
115
+ expect(preferenceSection?.fields.some((field) => field.path === "features.toolCallDisplay.groupedMode")).toBe(true);
116
+
117
+ const paths = CONFIGURE_SECTIONS.flatMap((section) =>
118
+ section.fields.map((field) => field.path)
119
+ );
120
+ expect(paths.filter((path) => path === "features.toolCallDisplay.grouped")).toHaveLength(1);
121
+ });
122
+
123
+ it("exposes inheritable capability-tagged artifact preference fields", () => {
124
+ const section = CONFIGURE_SECTIONS.find(
125
+ (entry) => entry.id === "artifact-display-preferences"
126
+ );
127
+ const paths = section?.fields.map((field) => field.path) ?? [];
128
+ expect(paths).toEqual(
129
+ expect.arrayContaining([
130
+ "features.artifacts.display.files",
131
+ "features.artifacts.display.byKind.component",
132
+ "features.artifacts.display.default",
133
+ "features.artifacts.filePreview.enabled",
134
+ ])
135
+ );
136
+ expect(paths).not.toContain("features.artifacts.display.byKind.markdown");
137
+ expect(paths).not.toContain("features.artifacts.display.byCategory.document");
138
+ expect(paths).not.toContain("features.artifacts.display.byCategory.application");
139
+ expect(section?.requiresCapability).toBe("artifacts");
140
+ expect(section?.fields.every((field) => field.unsettable)).toBe(true);
112
141
  });
113
142
 
114
143
  it("exposes product-facing artifact toolbar copy", () => {
@@ -129,9 +158,12 @@ describe("theme editor scroll-to-bottom controls", () => {
129
158
 
130
159
  it("exposes collapsed reasoning preview controls", () => {
131
160
  const debugSection = CONFIGURE_SECTIONS.find((section) => section.id === "debug-inspection");
161
+ const preferenceSection = CONFIGURE_SECTIONS.find(
162
+ (section) => section.id === "stream-display-preferences"
163
+ );
132
164
 
133
- expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.expandable")).toBe(true);
134
- expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.activePreview")).toBe(true);
165
+ expect(preferenceSection?.fields.some((field) => field.path === "features.reasoningDisplay.expandable")).toBe(true);
166
+ expect(preferenceSection?.fields.some((field) => field.path === "features.reasoningDisplay.activePreview")).toBe(true);
135
167
  expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.previewMaxLines")).toBe(true);
136
168
  expect(debugSection?.fields.some((field) => field.path === "features.reasoningDisplay.activeMinHeight")).toBe(true);
137
169
  });
@@ -869,20 +869,71 @@ const apiIntegrationSectionDef: SectionDef = {
869
869
  ],
870
870
  };
871
871
 
872
+ const DISPLAY_MODE_OPTIONS = [
873
+ { value: 'collapsed', label: 'Collapsed card' },
874
+ { value: 'panel', label: 'Side panel' },
875
+ { value: 'inline', label: 'In conversation' },
876
+ ];
877
+
878
+ const INHERITABLE_DISPLAY_FIELD = {
879
+ type: 'select' as const,
880
+ defaultValue: 'panel',
881
+ options: DISPLAY_MODE_OPTIONS,
882
+ unsettable: true,
883
+ unsetLabel: 'Use default',
884
+ requiresCapability: 'artifacts' as const,
885
+ };
886
+
887
+ export const ARTIFACT_DISPLAY_PREFERENCE_SECTION: SectionDef = {
888
+ id: 'artifact-display-preferences',
889
+ title: 'Artifact display',
890
+ description: 'Choose where recognizable artifact groups appear.',
891
+ requiresCapability: 'artifacts',
892
+ fields: [
893
+ { ...INHERITABLE_DISPLAY_FIELD, id: 'artifact-display-files', label: 'Generated files', description: 'Default for HTML, CSV, documents, and code files.', path: 'features.artifacts.display.files' },
894
+ { ...INHERITABLE_DISPLAY_FIELD, id: 'artifact-display-components', label: 'Registered UI components', description: 'Charts, forms, cards, and custom UI rendered by the host.', path: 'features.artifacts.display.byKind.component' },
895
+ { ...INHERITABLE_DISPLAY_FIELD, id: 'artifact-display-default', label: 'Other artifacts', description: 'Plain documents and anything without a narrower rule.', path: 'features.artifacts.display.default' },
896
+ {
897
+ id: 'artifact-file-preview',
898
+ label: 'Render supported file previews',
899
+ type: 'toggle',
900
+ path: 'features.artifacts.filePreview.enabled',
901
+ defaultValue: true,
902
+ unsettable: true,
903
+ unsetLabel: 'Use default',
904
+ requiresCapability: 'artifacts',
905
+ },
906
+ ],
907
+ };
908
+
909
+ const streamDisplayPreferencesSectionDef: SectionDef = {
910
+ id: 'stream-display-preferences',
911
+ title: 'Tool calls and reasoning',
912
+ description: 'Control how agent activity appears in the conversation.',
913
+ fields: [
914
+ { id: 'pref-show-tools', label: 'Show tool calls', type: 'toggle', path: 'features.showToolCalls', defaultValue: true, unsettable: true, requiresCapability: 'tools' },
915
+ { id: 'pref-tool-collapsed-mode', label: 'Tool call summary', type: 'select', path: 'features.toolCallDisplay.collapsedMode', defaultValue: 'tool-call', options: [{ value: 'tool-call', label: 'Tool call' }, { value: 'tool-name', label: 'Tool name' }, { value: 'tool-preview', label: 'Tool preview' }], unsettable: true, requiresCapability: 'tools' },
916
+ { id: 'pref-tool-expandable', label: 'Tool calls expandable', type: 'toggle', path: 'features.toolCallDisplay.expandable', defaultValue: true, unsettable: true, requiresCapability: 'tools' },
917
+ { id: 'pref-tool-grouped', label: 'Group sequential tool calls', type: 'toggle', path: 'features.toolCallDisplay.grouped', defaultValue: false, unsettable: true, requiresCapability: 'tools' },
918
+ { id: 'pref-tool-grouped-mode', label: 'Grouped tool detail', type: 'select', path: 'features.toolCallDisplay.groupedMode', defaultValue: 'stack', options: [{ value: 'stack', label: 'Show steps' }, { value: 'summary', label: 'Summary only' }], unsettable: true, requiresCapability: 'tools' },
919
+ { id: 'pref-tool-loading-animation', label: 'Tool loading animation', type: 'select', path: 'features.toolCallDisplay.loadingAnimation', defaultValue: 'none', options: [{ value: 'none', label: 'None' }, { value: 'pulse', label: 'Pulse' }, { value: 'shimmer', label: 'Shimmer' }, { value: 'shimmer-color', label: 'Color shimmer' }, { value: 'rainbow', label: 'Rainbow' }], unsettable: true, requiresCapability: 'tools' },
920
+ { id: 'pref-show-reasoning', label: 'Show reasoning', type: 'toggle', path: 'features.showReasoning', defaultValue: true, unsettable: true, requiresCapability: 'reasoning' },
921
+ { id: 'pref-reasoning-expandable', label: 'Reasoning expandable', type: 'toggle', path: 'features.reasoningDisplay.expandable', defaultValue: true, unsettable: true, requiresCapability: 'reasoning' },
922
+ { id: 'pref-reasoning-active-preview', label: 'Reasoning preview while active', type: 'toggle', path: 'features.reasoningDisplay.activePreview', defaultValue: false, unsettable: true, requiresCapability: 'reasoning' },
923
+ ],
924
+ };
925
+
926
+ export const STREAM_AND_ARTIFACTS_SUB_GROUP: SubGroupDef = {
927
+ label: 'Stream and artifacts',
928
+ sections: [streamDisplayPreferencesSectionDef, ARTIFACT_DISPLAY_PREFERENCE_SECTION],
929
+ };
930
+
872
931
  const debugSectionDef: SectionDef = {
873
932
  id: 'debug-inspection', title: 'Debug & Inspection', collapsed: true,
874
933
  fields: [
875
- { id: 'dev-reasoning', label: 'Show Reasoning', description: 'Display AI reasoning steps', type: 'toggle', path: 'features.showReasoning', defaultValue: false },
876
- { id: 'dev-tool-calls', label: 'Show Tool Calls', description: 'Display tool call details', type: 'toggle', path: 'features.showToolCalls', defaultValue: false },
877
- { id: 'dev-tool-collapsed-mode', label: 'Tool Call Summary', description: 'Choose what collapsed tool rows show by default', type: 'select', path: 'features.toolCallDisplay.collapsedMode', defaultValue: 'tool-call', options: [{ value: 'tool-call', label: 'Tool Call' }, { value: 'tool-name', label: 'Tool Name' }, { value: 'tool-preview', label: 'Tool Preview' }] },
878
934
  { id: 'dev-tool-active-preview', label: 'Tool Preview While Active', description: 'Show a lightweight preview in collapsed active tool rows', type: 'toggle', path: 'features.toolCallDisplay.activePreview', defaultValue: false },
879
935
  { id: 'dev-tool-preview-lines', label: 'Tool Preview Lines', type: 'select', path: 'features.toolCallDisplay.previewMaxLines', defaultValue: 3, options: [{ value: '1', label: '1' }, { value: '2', label: '2' }, { value: '3', label: '3' }, { value: '4', label: '4' }, { value: '5', label: '5' }], formatValue: (v: unknown) => String(v ?? 3), parseValue: (v: unknown) => Number(v) },
880
936
  { id: 'dev-tool-active-min-height', label: 'Tool Active Min Height', description: 'CSS min-height for collapsed active tool rows (e.g. 5rem)', type: 'text', path: 'features.toolCallDisplay.activeMinHeight', defaultValue: '' },
881
- { id: 'dev-tool-expandable', label: 'Tool Calls Expandable', description: 'Allow expanding tool call rows to see full details', type: 'toggle', path: 'features.toolCallDisplay.expandable', defaultValue: true },
882
- { id: 'dev-tool-grouped', label: 'Group Sequential Tool Calls', description: 'Render consecutive tool rows inside a grouped container', type: 'toggle', path: 'features.toolCallDisplay.grouped', defaultValue: false },
883
- { id: 'dev-tool-grouped-mode', label: 'Grouped Tool Detail', description: 'Show each tool step or one consolidated summary', type: 'select', path: 'features.toolCallDisplay.groupedMode', defaultValue: 'stack', options: [{ value: 'stack', label: 'Show Steps' }, { value: 'summary', label: 'Summary Only' }] },
884
- { id: 'dev-reasoning-expandable', label: 'Reasoning Expandable', description: 'Allow expanding reasoning rows to see full details', type: 'toggle', path: 'features.reasoningDisplay.expandable', defaultValue: true },
885
- { id: 'dev-reasoning-active-preview', label: 'Reasoning Preview While Active', description: 'Show a lightweight preview in collapsed active reasoning rows', type: 'toggle', path: 'features.reasoningDisplay.activePreview', defaultValue: false },
886
937
  { id: 'dev-reasoning-preview-lines', label: 'Reasoning Preview Lines', type: 'select', path: 'features.reasoningDisplay.previewMaxLines', defaultValue: 3, options: [{ value: '1', label: '1' }, { value: '2', label: '2' }, { value: '3', label: '3' }, { value: '4', label: '4' }, { value: '5', label: '5' }], formatValue: (v: unknown) => String(v ?? 3), parseValue: (v: unknown) => Number(v) },
887
938
  { id: 'dev-reasoning-active-min-height', label: 'Reasoning Active Min Height', description: 'CSS min-height for collapsed active reasoning rows (e.g. 5rem)', type: 'text', path: 'features.reasoningDisplay.activeMinHeight', defaultValue: '' },
888
939
  { id: 'dev-debug', label: 'Debug Mode', description: 'Show debug information', type: 'toggle', path: 'debug', defaultValue: false },
@@ -906,6 +957,7 @@ export const CONFIGURE_SUB_GROUPS: SubGroupDef[] = [
906
957
  { label: 'Layout', sections: [generalLayoutSectionDef, headerLayoutSectionDef, messagesLayoutSectionDef, messageActionsSectionDef] },
907
958
  { label: 'Widget', sections: [launcherBasicsSectionDef, launcherAdvancedSectionDef, sendButtonSectionDef, closeButtonSectionDef, clearChatSectionDef, statusIndicatorSectionDef] },
908
959
  { label: 'Features', sections: [featuresSectionDef, streamAnimationSectionDef, attachmentsSectionDef, artifactsSectionDef, artifactCustomizationSectionDef] },
960
+ STREAM_AND_ARTIFACTS_SUB_GROUP,
909
961
  { label: 'Developer', collapsedByDefault: true, sections: [apiIntegrationSectionDef, debugSectionDef, markdownSectionDef] },
910
962
  ];
911
963
 
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { ThemeEditorState } from "./state";
3
+
4
+ describe("ThemeEditorState preference reset", () => {
5
+ it("unsets a path and prunes empty parent objects", () => {
6
+ const state = new ThemeEditorState(undefined, {}, { mergeDefaults: false });
7
+ state.set("features.artifacts.display.files", "inline");
8
+ expect(state.get("features.artifacts.display.files")).toBe("inline");
9
+
10
+ state.unset("features.artifacts.display.files");
11
+
12
+ expect(state.get("features.artifacts.display.files")).toBeUndefined();
13
+ expect(state.getConfig()).not.toHaveProperty("features");
14
+ });
15
+ });
@@ -32,6 +32,30 @@ function setByPath(obj: unknown, path: string, value: unknown): unknown {
32
32
  };
33
33
  }
34
34
 
35
+ function unsetByPath(obj: unknown, path: string): unknown {
36
+ const parts = path.split('.');
37
+ if (parts.length === 1) {
38
+ const result = { ...(obj as Record<string, unknown>) };
39
+ delete result[parts[0]];
40
+ return result;
41
+ }
42
+ const [first, ...rest] = parts;
43
+ const current = obj as Record<string, unknown>;
44
+ const child = unsetByPath(current?.[first] ?? {}, rest.join('.'));
45
+ const result = { ...current };
46
+ if (
47
+ typeof child === 'object' &&
48
+ child !== null &&
49
+ !Array.isArray(child) &&
50
+ Object.keys(child as Record<string, unknown>).length === 0
51
+ ) {
52
+ delete result[first];
53
+ } else {
54
+ result[first] = child;
55
+ }
56
+ return result;
57
+ }
58
+
35
59
  // ─── ThemeEditorState ───────────────────────────────────────────
36
60
 
37
61
  export class ThemeEditorState {
@@ -111,6 +135,28 @@ export class ThemeEditorState {
111
135
  this.notifyListeners();
112
136
  }
113
137
 
138
+ /** Delete a dot-path and prune empty parents so sparse preferences inherit. */
139
+ unset(path: string): void {
140
+ if (path.startsWith('theme.')) {
141
+ const themePath = path.replace('theme.', '');
142
+ this.theme = unsetByPath(this.theme, themePath) as PersonaTheme;
143
+ this.syncThemeIntoConfig();
144
+ } else if (path.startsWith('darkTheme.')) {
145
+ const themePath = path.replace('darkTheme.', '');
146
+ this.config = {
147
+ ...this.config,
148
+ darkTheme: unsetByPath(
149
+ this.config.darkTheme ?? {},
150
+ themePath
151
+ ) as AgentWidgetConfig['darkTheme'],
152
+ };
153
+ } else {
154
+ this.config = unsetByPath(this.config, path) as AgentWidgetConfig;
155
+ }
156
+ this.recordHistory();
157
+ this.notifyListeners();
158
+ }
159
+
114
160
  /** Batch-set multiple paths at once */
115
161
  setBatch(updates: Record<string, unknown>): void {
116
162
  let themeChanged = false;
@@ -1,7 +1,11 @@
1
1
  /** Field definition types for the declarative configurator system (headless: no DOM) */
2
2
 
3
3
  import type { DeepPartial, PersonaTheme } from '../types/theme';
4
- import type { AgentWidgetConfig } from '../types';
4
+ import type {
5
+ AgentWidgetConfig,
6
+ WidgetPreferenceCapabilities,
7
+ WidgetPreferenceSlice,
8
+ } from '../types';
5
9
 
6
10
  // ─── Field System ────────────────────────────────────────────────
7
11
 
@@ -101,6 +105,12 @@ export interface FieldDef {
101
105
  formatValue?: (value: unknown) => unknown;
102
106
  /** Convert control input back into the stored value shape */
103
107
  parseValue?: (value: unknown) => unknown;
108
+ /** Capability required for hosts to render this field. */
109
+ requiresCapability?: keyof WidgetPreferenceCapabilities;
110
+ /** The host may offer an inherit/reset action which deletes this path. */
111
+ unsettable?: boolean;
112
+ /** Label for the inherit/reset action. Default: "Use default". */
113
+ unsetLabel?: string;
104
114
  }
105
115
 
106
116
  export interface SectionDef {
@@ -112,6 +122,8 @@ export interface SectionDef {
112
122
  collapsed?: boolean;
113
123
  /** Preset buttons for this section */
114
124
  presets?: SectionPreset[];
125
+ /** Capability required for hosts to render this section. */
126
+ requiresCapability?: keyof WidgetPreferenceCapabilities;
115
127
  }
116
128
 
117
129
  export interface SectionPreset {
@@ -149,6 +161,8 @@ export interface ThemeEditorPreset {
149
161
  toolCall?: AgentWidgetToolCallConfig;
150
162
  /** Tool call styling for dark mode (falls back to toolCall if not set) */
151
163
  darkToolCall?: AgentWidgetToolCallConfig;
164
+ /** Sparse, persistable widget behavior layered separately from theme tokens. */
165
+ behavior?: WidgetPreferenceSlice;
152
166
  preview: {
153
167
  primary: string;
154
168
  surface: string;
@@ -123,6 +123,19 @@ describe('createThemeEditorTools', () => {
123
123
  );
124
124
  });
125
125
 
126
+ it('apply_preset installs both Persona default slots with automatic scheme selection', async () => {
127
+ const out = await call(tools.get('apply_preset')!, { presetId: 'persona-default' });
128
+
129
+ expect(out.applied.appliedPreset.id).toBe('persona-default');
130
+ expect(state.get('colorScheme')).toBe('auto');
131
+ expect(state.get('theme.components.artifact.pane.background')).toBe(
132
+ 'semantic.colors.container'
133
+ );
134
+ expect(state.get('darkTheme.semantic.colors.surface')).toBe(
135
+ 'palette.colors.gray.800'
136
+ );
137
+ });
138
+
126
139
  it('configure_widget writes config paths', async () => {
127
140
  await call(tools.get('configure_widget')!, {
128
141
  launcherPosition: 'bottom-left',
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { createTheme } from '../../utils/theme';
13
+ import { applyFeaturePreferences } from '../../utils/feature-preferences';
13
14
  import type { AgentWidgetConfig } from '../../types';
14
15
  import type { PersonaTheme } from '../../types/theme';
15
16
  import { generateColorScale, SHADE_KEYS } from '../color-utils';
@@ -469,8 +470,12 @@ export function createThemeEditorTools(
469
470
  const config: AgentWidgetConfig = { ...state.getConfig() };
470
471
  if (preset.darkTheme) {
471
472
  config.darkTheme = createTheme(preset.darkTheme, { validate: false }) as PersonaTheme;
473
+ config.colorScheme = 'auto';
472
474
  }
473
475
  if (preset.toolCall) config.toolCall = preset.toolCall;
476
+ if (preset.behavior) {
477
+ config.features = applyFeaturePreferences(config.features, [preset.behavior]);
478
+ }
474
479
  state.setFullConfig(config, theme);
475
480
  const warnings = quickContrastWarnings(state, ['body', 'assistant-message'], 'light');
476
481
  return result({ appliedPreset: { id: preset.id, name: preset.name } }, warnings);