@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.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CI7OckWI.d.cts → types-Db61aI9J.d.cts} +2 -0
- package/dist/animations/{types-CI7OckWI.d.ts → types-Db61aI9J.d.ts} +2 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +2 -0
- package/dist/animations-extra.d.ts +2 -0
- package/dist/approval-ui-3NMYNXBD.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +215 -37
- package/dist/approval-ui.d.ts +215 -37
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-F4N6VMKF.js → artifacts-ui-IGB5YKLO.js} +4 -4
- package/dist/artifacts-ui.d.cts +210 -25
- package/dist/artifacts-ui.d.ts +210 -25
- package/dist/{chunk-NIKYKSNZ.js → chunk-7CO4T5KB.js} +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +13 -13
- package/dist/context-mentions.d.cts +210 -25
- package/dist/context-mentions.d.ts +210 -25
- package/dist/event-stream-view.d.cts +210 -25
- package/dist/event-stream-view.d.ts +210 -25
- package/dist/forms-ui.d.cts +245 -27
- package/dist/forms-ui.d.ts +245 -27
- package/dist/history-view.d.cts +2 -0
- package/dist/history-view.d.ts +2 -0
- package/dist/index.cjs +70 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +270 -39
- package/dist/index.d.ts +270 -39
- package/dist/index.global.js +57 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +70 -69
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/session-reconnect.d.cts +210 -25
- package/dist/session-reconnect.d.ts +210 -25
- package/dist/smart-dom-reader.d.cts +210 -25
- package/dist/smart-dom-reader.d.ts +210 -25
- package/dist/theme-editor-preview.cjs +74 -73
- package/dist/theme-editor-preview.d.cts +217 -25
- package/dist/theme-editor-preview.d.ts +217 -25
- package/dist/theme-editor-preview.js +51 -50
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +230 -27
- package/dist/theme-editor.d.ts +230 -27
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1522 -11
- package/dist/theme-reference.d.ts +1522 -11
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +1 -1
- package/package.json +3 -2
- package/src/__fixtures__/defaults-parity.v4.json +776 -0
- package/src/approval-ui-entry.ts +0 -1
- package/src/approval-ui-loader.ts +5 -4
- package/src/approval-ui.ts +0 -1
- package/src/client-identity-proof.test.ts +193 -0
- package/src/client-session-warm.test.ts +216 -0
- package/src/client-visitor-history.test.ts +6 -0
- package/src/client.ts +122 -30
- package/src/codegen.test.ts +24 -0
- package/src/components/activity-row.test.ts +100 -0
- package/src/components/activity-row.ts +136 -0
- package/src/components/approval-actions.test.ts +8 -5
- package/src/components/approval-actions.ts +19 -6
- package/src/components/approval-bubble.test.ts +56 -9
- package/src/components/approval-bubble.ts +36 -41
- package/src/components/composer-builder.ts +5 -2
- package/src/components/composer-parts.ts +10 -2
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +7 -4
- package/src/components/header-layouts.test.ts +12 -0
- package/src/components/header-layouts.ts +3 -2
- package/src/components/launcher.ts +11 -2
- package/src/components/message-bubble.test.ts +22 -0
- package/src/components/message-bubble.ts +7 -3
- package/src/components/panel.test.ts +52 -1
- package/src/components/panel.ts +47 -8
- package/src/components/pill-composer-builder.test.ts +2 -0
- package/src/components/pill-composer-builder.ts +0 -1
- package/src/components/reasoning-bubble.ts +29 -17
- package/src/components/suggestions.test.ts +1 -0
- package/src/components/suggestions.ts +0 -16
- package/src/components/tool-bubble.ts +131 -114
- package/src/components/tool-details.test.ts +87 -0
- package/src/components/tool-details.ts +102 -0
- package/src/components/welcome.test.ts +8 -0
- package/src/components/welcome.ts +4 -2
- package/src/defaults-parity.test.ts +158 -0
- package/src/defaults.test.ts +69 -0
- package/src/defaults.ts +128 -49
- package/src/generated/runtype-openapi-contract.ts +2 -2
- package/src/index-core.ts +2 -0
- package/src/install.test.ts +24 -0
- package/src/install.ts +3 -1
- package/src/launcher-global.ts +2 -1
- package/src/launcher-variants.test.ts +52 -0
- package/src/runtime/init-update-reset.test.ts +21 -0
- package/src/session.ts +29 -6
- package/src/styles/widget-styles.test.ts +19 -0
- package/src/styles/widget.css +295 -23
- package/src/theme-editor/index.ts +2 -0
- package/src/theme-editor/preview-utils.test.ts +8 -0
- package/src/theme-editor/preview-utils.ts +7 -3
- package/src/theme-editor/sections.test.ts +31 -0
- package/src/theme-editor/sections.ts +55 -1
- package/src/theme-editor/state-preferences.test.ts +49 -0
- package/src/theme-editor/state.ts +73 -15
- package/src/theme-reference.test.ts +20 -0
- package/src/theme-reference.ts +11 -5
- package/src/tool-call-display-defaults.test.ts +33 -1
- package/src/types/theme.ts +80 -3
- package/src/types.ts +136 -22
- package/src/ui.activity-row.test.ts +91 -0
- package/src/ui.approval-plugin.test.ts +63 -0
- package/src/ui.composer-compact.test.ts +24 -2
- package/src/ui.composer-placement.test.ts +23 -0
- package/src/ui.detached-panel.test.ts +33 -6
- package/src/ui.docked.test.ts +19 -0
- package/src/ui.header-update-stability.test.ts +25 -0
- package/src/ui.identity-proof.test.ts +58 -0
- package/src/ui.launcher-teaser.test.ts +25 -0
- package/src/ui.message-width.test.ts +17 -2
- package/src/ui.reasoning-display.test.ts +92 -0
- package/src/ui.rebuild-composer.test.ts +20 -3
- package/src/ui.scroll.test.ts +32 -0
- package/src/ui.session-init.test.ts +312 -0
- package/src/ui.status-indicator.test.ts +82 -0
- package/src/ui.suggest-replies.test.ts +11 -0
- package/src/ui.ts +363 -74
- package/src/ui.v5-defaults.test.ts +144 -0
- package/src/ui.welcome.test.ts +92 -3
- package/src/utils/__snapshots__/tokens-parity.test.ts.snap +811 -140
- package/src/utils/code-generators.ts +14 -0
- package/src/utils/composer-compact.test.ts +0 -2
- package/src/utils/composer-compact.ts +2 -4
- package/src/utils/composer-placement.ts +2 -3
- package/src/utils/config-merge.test.ts +81 -5
- package/src/utils/config-merge.ts +13 -3
- package/src/utils/defaults-provenance.ts +67 -0
- package/src/utils/dock.ts +7 -1
- package/src/utils/launcher-variant.ts +16 -0
- package/src/utils/panel-config.ts +97 -0
- package/src/utils/status-indicator.ts +39 -0
- package/src/utils/theme.test.ts +182 -0
- package/src/utils/theme.ts +131 -18
- package/src/utils/tokens.test.ts +122 -1
- package/src/utils/tokens.ts +177 -17
- package/src/welcome.test.ts +23 -0
- package/src/welcome.ts +16 -9
- 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
|
-
|
|
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 {
|
|
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?:
|
|
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
|
-
? { ...
|
|
77
|
-
: (initialConfig ??
|
|
85
|
+
? { ...defaults, ...initialConfig }
|
|
86
|
+
: (initialConfig ?? defaults)
|
|
78
87
|
) as AgentWidgetConfig;
|
|
79
|
-
this.theme = createTheme(initialTheme, {
|
|
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
|
|
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
|
|
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
|
|
238
|
-
this.
|
|
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
|
-
|
|
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
|
+
});
|
package/src/theme-reference.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* out of the IIFE widget bundle. Intended for AI/MCP tool consumption.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
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
|
-
|
|
473
|
-
|
|
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
|
});
|