@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.
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions.d.cts +98 -6
- package/dist/context-mentions.d.ts +98 -6
- package/dist/index.cjs +64 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +183 -8
- package/dist/index.d.ts +183 -8
- package/dist/index.global.js +54 -54
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +67 -67
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +1 -1
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +98 -6
- package/dist/smart-dom-reader.d.ts +98 -6
- package/dist/theme-editor-preview.cjs +66 -66
- package/dist/theme-editor-preview.d.cts +100 -6
- package/dist/theme-editor-preview.d.ts +100 -6
- package/dist/theme-editor-preview.js +75 -75
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +134 -7
- package/dist/theme-editor.d.ts +134 -7
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/widget.css +1 -1
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +106 -0
- package/src/client.test.ts +2 -2
- package/src/client.ts +4 -1
- package/src/components/artifact-card.ts +13 -2
- package/src/components/artifact-inline.ts +11 -2
- package/src/components/composer-builder.test.ts +2 -0
- package/src/components/composer-builder.ts +1 -1
- package/src/components/composer-parts.test.ts +2 -0
- package/src/components/composer-parts.ts +1 -1
- package/src/components/message-bubble.test.ts +7 -1
- package/src/components/message-bubble.ts +11 -8
- package/src/index-core.ts +34 -0
- package/src/session.ts +45 -4
- package/src/styles/widget.css +10 -1
- package/src/theme-editor/index.ts +5 -0
- package/src/theme-editor/presets.test.ts +43 -0
- package/src/theme-editor/presets.ts +101 -87
- package/src/theme-editor/sections.test.ts +39 -7
- package/src/theme-editor/sections.ts +60 -8
- package/src/theme-editor/state-preferences.test.ts +15 -0
- package/src/theme-editor/state.ts +46 -0
- package/src/theme-editor/types.ts +15 -1
- package/src/theme-editor/webmcp/tools.test.ts +13 -0
- package/src/theme-editor/webmcp/tools.ts +5 -0
- package/src/types.ts +139 -9
- package/src/ui.artifact-pane-gating.test.ts +5 -5
- package/src/ui.config-preferences.test.ts +166 -0
- package/src/ui.scroll-additive.test.ts +365 -0
- package/src/ui.scroll.test.ts +19 -6
- package/src/ui.ts +158 -53
- package/src/ui.typing-indicator.test.ts +135 -0
- package/src/utils/artifact-display.test.ts +180 -16
- package/src/utils/artifact-display.ts +155 -14
- package/src/utils/auto-follow.test.ts +38 -0
- package/src/utils/auto-follow.ts +35 -0
- package/src/utils/config-merge.test.ts +60 -0
- package/src/utils/config-merge.ts +6 -1
- package/src/utils/feature-preferences.test.ts +326 -0
- package/src/utils/feature-preferences.ts +498 -0
- package/src/utils/theme.test.ts +92 -0
- package/src/utils/tokens.ts +7 -7
package/src/types.ts
CHANGED
|
@@ -685,6 +685,15 @@ export type AgentToolsConfig = {
|
|
|
685
685
|
/** Artifact kinds for the Persona sidebar and dispatch payload */
|
|
686
686
|
export type PersonaArtifactKind = "markdown" | "component";
|
|
687
687
|
|
|
688
|
+
export type PersonaArtifactPresentation = {
|
|
689
|
+
/**
|
|
690
|
+
* Producer hint for where this artifact renders (content-disposition
|
|
691
|
+
* model). Beats configured defaults; loses to explicit `files`/`byKind`
|
|
692
|
+
* rules. Persona never infers it; the producer or host supplies it.
|
|
693
|
+
*/
|
|
694
|
+
preferredMode?: PersonaArtifactDisplayModeInput;
|
|
695
|
+
};
|
|
696
|
+
|
|
688
697
|
/**
|
|
689
698
|
* Agent configuration for agent execution mode.
|
|
690
699
|
* When provided in the widget config, enables agent loop execution instead of flow dispatch.
|
|
@@ -1375,10 +1384,60 @@ export type AgentWidgetArtifactsLayoutConfig = {
|
|
|
1375
1384
|
}) => void | Promise<void>;
|
|
1376
1385
|
};
|
|
1377
1386
|
|
|
1387
|
+
/**
|
|
1388
|
+
* Where the artifact body renders by default. Every mode shows the compact
|
|
1389
|
+
* reference in the transcript; the mode names where the body goes:
|
|
1390
|
+
* nowhere yet (`collapsed`), the side panel (`panel`), or the conversation
|
|
1391
|
+
* itself (`inline`).
|
|
1392
|
+
*/
|
|
1378
1393
|
export type PersonaArtifactDisplayMode =
|
|
1379
|
-
| "
|
|
1380
|
-
| "panel"
|
|
1381
|
-
| "inline"; //
|
|
1394
|
+
| "collapsed" // body renders nowhere until the user selects the reference; pane opens on click
|
|
1395
|
+
| "panel" // body renders in the side panel, which auto-opens on artifact_start (default behavior)
|
|
1396
|
+
| "inline"; // body renders directly in the transcript; no pane involvement
|
|
1397
|
+
|
|
1398
|
+
/**
|
|
1399
|
+
* Accepted wherever a display mode is written. `"card"` is a deprecated
|
|
1400
|
+
* alias of `"collapsed"` (the old name described the reference card, which
|
|
1401
|
+
* every mode shows, instead of where the body renders). Resolution and
|
|
1402
|
+
* parsing always canonicalize to `"collapsed"`.
|
|
1403
|
+
*/
|
|
1404
|
+
export type PersonaArtifactDisplayModeInput =
|
|
1405
|
+
| PersonaArtifactDisplayMode
|
|
1406
|
+
| "card";
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Display rules for file-backed artifacts. `byMediaType` keys are exact MIME
|
|
1410
|
+
* types (`"text/html"`) or major-type wildcards (`"image/*"`), matched
|
|
1411
|
+
* case-insensitively without parameters; exact beats wildcard.
|
|
1412
|
+
*/
|
|
1413
|
+
export type PersonaArtifactFilesDisplayRules = {
|
|
1414
|
+
default?: PersonaArtifactDisplayModeInput;
|
|
1415
|
+
byMediaType?: Record<string, PersonaArtifactDisplayModeInput>;
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Scoped artifact display rules. Matching resolves from the most specific
|
|
1420
|
+
* selector to `default`; see `resolveArtifactDisplay`.
|
|
1421
|
+
*
|
|
1422
|
+
* Across preference layers, one merge rule applies everywhere: a string value
|
|
1423
|
+
* replaces the whole subtree it names; an object refines it per key. So
|
|
1424
|
+
* `files: "inline"` overrides every lower-layer file rule, while
|
|
1425
|
+
* `files: { default: "inline" }` keeps lower-layer MIME exceptions.
|
|
1426
|
+
*/
|
|
1427
|
+
export type PersonaArtifactDisplayRules = {
|
|
1428
|
+
default?: PersonaArtifactDisplayModeInput;
|
|
1429
|
+
byKind?: Partial<
|
|
1430
|
+
Record<PersonaArtifactKind, PersonaArtifactDisplayModeInput>
|
|
1431
|
+
>;
|
|
1432
|
+
/**
|
|
1433
|
+
* @deprecated Use `byKind`. When both are present, `byKind` wins.
|
|
1434
|
+
*/
|
|
1435
|
+
byType?: Partial<
|
|
1436
|
+
Record<PersonaArtifactKind, PersonaArtifactDisplayModeInput>
|
|
1437
|
+
>;
|
|
1438
|
+
/** Rules for any artifact carrying file metadata. */
|
|
1439
|
+
files?: PersonaArtifactDisplayModeInput | PersonaArtifactFilesDisplayRules;
|
|
1440
|
+
};
|
|
1382
1441
|
|
|
1383
1442
|
/** Context handed to custom artifact actions. Content fields are best-effort: resolved from live session state or, after a refresh, from the persisted reference-card payload. */
|
|
1384
1443
|
export type PersonaArtifactActionContext = {
|
|
@@ -1439,15 +1498,12 @@ export type AgentWidgetArtifactsFeature = {
|
|
|
1439
1498
|
allowedTypes?: PersonaArtifactKind[];
|
|
1440
1499
|
/**
|
|
1441
1500
|
* Where artifact bodies render. A single mode applies to all artifacts;
|
|
1442
|
-
* the object form sets a default plus
|
|
1501
|
+
* the object form sets a default plus scoped overrides.
|
|
1443
1502
|
* Defaults to "panel" (current behavior).
|
|
1444
1503
|
*/
|
|
1445
1504
|
display?:
|
|
1446
|
-
|
|
|
1447
|
-
|
|
|
1448
|
-
default?: PersonaArtifactDisplayMode;
|
|
1449
|
-
byType?: Partial<Record<PersonaArtifactKind, PersonaArtifactDisplayMode>>;
|
|
1450
|
-
};
|
|
1505
|
+
| PersonaArtifactDisplayModeInput
|
|
1506
|
+
| PersonaArtifactDisplayRules;
|
|
1451
1507
|
/** Split / drawer dimensions and launcher widen behavior */
|
|
1452
1508
|
layout?: AgentWidgetArtifactsLayoutConfig;
|
|
1453
1509
|
/**
|
|
@@ -2209,6 +2265,67 @@ export type AgentWidgetFeatureFlags = {
|
|
|
2209
2265
|
suggestReplies?: AgentWidgetSuggestRepliesFeature;
|
|
2210
2266
|
};
|
|
2211
2267
|
|
|
2268
|
+
export type WidgetPreferenceCapabilities = {
|
|
2269
|
+
artifacts?: boolean;
|
|
2270
|
+
tools?: boolean;
|
|
2271
|
+
reasoning?: boolean;
|
|
2272
|
+
};
|
|
2273
|
+
|
|
2274
|
+
/**
|
|
2275
|
+
* Persistable artifact-layout choices. The full layout config deliberately is
|
|
2276
|
+
* not exposed because it also contains callbacks and embedder-owned chrome.
|
|
2277
|
+
*/
|
|
2278
|
+
export type WidgetArtifactLayoutPreference = Pick<
|
|
2279
|
+
AgentWidgetArtifactsLayoutConfig,
|
|
2280
|
+
| "paneWidth"
|
|
2281
|
+
| "paneMaxWidth"
|
|
2282
|
+
| "paneMinWidth"
|
|
2283
|
+
| "expandLauncherPanelWhenOpen"
|
|
2284
|
+
| "expandedPanelWidth"
|
|
2285
|
+
| "resizable"
|
|
2286
|
+
| "resizableMinWidth"
|
|
2287
|
+
| "resizableMaxWidth"
|
|
2288
|
+
>;
|
|
2289
|
+
|
|
2290
|
+
/**
|
|
2291
|
+
* Curated, JSON-serializable display preferences in native `features` shape.
|
|
2292
|
+
*
|
|
2293
|
+
* Absence means inherit the next preference layer and ultimately Persona's
|
|
2294
|
+
* current default. Persist only explicit choices; resetting a choice deletes
|
|
2295
|
+
* its key.
|
|
2296
|
+
*
|
|
2297
|
+
* Capability facts (`artifacts.enabled`, `allowedTypes`), trusted iframe
|
|
2298
|
+
* security, callbacks, actions, renderers, labels, and theme styling are
|
|
2299
|
+
* intentionally excluded because they remain embedder/code-owned.
|
|
2300
|
+
*/
|
|
2301
|
+
export type WidgetPreferenceSlice = {
|
|
2302
|
+
showToolCalls?: boolean;
|
|
2303
|
+
showReasoning?: boolean;
|
|
2304
|
+
toolCallDisplay?: Pick<
|
|
2305
|
+
AgentWidgetToolCallDisplayFeature,
|
|
2306
|
+
| "collapsedMode"
|
|
2307
|
+
| "expandable"
|
|
2308
|
+
| "grouped"
|
|
2309
|
+
| "groupedMode"
|
|
2310
|
+
| "loadingAnimation"
|
|
2311
|
+
>;
|
|
2312
|
+
reasoningDisplay?: Pick<
|
|
2313
|
+
AgentWidgetReasoningDisplayFeature,
|
|
2314
|
+
"expandable" | "activePreview"
|
|
2315
|
+
>;
|
|
2316
|
+
artifacts?: {
|
|
2317
|
+
/**
|
|
2318
|
+
* String and object forms are both persistable and mean different things
|
|
2319
|
+
* when layers merge: a string replaces every lower-layer display rule; an
|
|
2320
|
+
* object refines lower layers per selector key (same rule applies to
|
|
2321
|
+
* `display.files`).
|
|
2322
|
+
*/
|
|
2323
|
+
display?: PersonaArtifactDisplayModeInput | PersonaArtifactDisplayRules;
|
|
2324
|
+
filePreview?: { enabled?: boolean };
|
|
2325
|
+
layout?: WidgetArtifactLayoutPreference;
|
|
2326
|
+
};
|
|
2327
|
+
};
|
|
2328
|
+
|
|
2212
2329
|
/**
|
|
2213
2330
|
* Feature config for the built-in `suggest_replies` quick-reply chips.
|
|
2214
2331
|
* Chips render in the existing suggestions slot above the composer and are
|
|
@@ -4902,6 +5019,15 @@ export type AgentWidgetConfig = {
|
|
|
4902
5019
|
*/
|
|
4903
5020
|
colorScheme?: 'auto' | 'light' | 'dark';
|
|
4904
5021
|
features?: AgentWidgetFeatureFlags;
|
|
5022
|
+
/**
|
|
5023
|
+
* Per-instance display overrides, applied over `features` through the
|
|
5024
|
+
* preference allowlist (security/capability keys cannot pass). String
|
|
5025
|
+
* `display` values replace the base rule subtree; objects refine it per
|
|
5026
|
+
* key. Each `controller.update({ preferences })` replaces the previous
|
|
5027
|
+
* slice wholesale (dropped keys revert to the base `features`); an
|
|
5028
|
+
* explicit-undefined patch clears it entirely.
|
|
5029
|
+
*/
|
|
5030
|
+
preferences?: WidgetPreferenceSlice;
|
|
4905
5031
|
/**
|
|
4906
5032
|
* When true, focus the chat input after the panel opens and the open animation completes.
|
|
4907
5033
|
* Applies to launcher mode (user click, controller.open(), autoExpand) and inline mode (on init).
|
|
@@ -5864,6 +5990,8 @@ export type PersonaArtifactRecord = {
|
|
|
5864
5990
|
props?: Record<string, unknown>;
|
|
5865
5991
|
/** Present when this markdown artifact is a previewable file (see PersonaArtifactFileMeta). */
|
|
5866
5992
|
file?: PersonaArtifactFileMeta;
|
|
5993
|
+
/** Optional presentation metadata such as the producer's display-mode hint. */
|
|
5994
|
+
presentation?: PersonaArtifactPresentation;
|
|
5867
5995
|
};
|
|
5868
5996
|
|
|
5869
5997
|
/** Programmatic artifact upsert (controller / window API) */
|
|
@@ -5875,6 +6003,7 @@ export type PersonaArtifactManualUpsert =
|
|
|
5875
6003
|
content: string;
|
|
5876
6004
|
/** Optional file metadata for previewable file artifacts. */
|
|
5877
6005
|
file?: PersonaArtifactFileMeta;
|
|
6006
|
+
presentation?: PersonaArtifactPresentation;
|
|
5878
6007
|
/** Set false to update the registry/pane without a transcript block (pre-4.x behavior). */
|
|
5879
6008
|
transcript?: boolean;
|
|
5880
6009
|
}
|
|
@@ -5884,6 +6013,7 @@ export type PersonaArtifactManualUpsert =
|
|
|
5884
6013
|
title?: string;
|
|
5885
6014
|
component: string;
|
|
5886
6015
|
props?: Record<string, unknown>;
|
|
6016
|
+
presentation?: PersonaArtifactPresentation;
|
|
5887
6017
|
/** Set false to update the registry/pane without a transcript block (pre-4.x behavior). */
|
|
5888
6018
|
transcript?: boolean;
|
|
5889
6019
|
};
|
|
@@ -27,7 +27,7 @@ beforeAll(() => {
|
|
|
27
27
|
*
|
|
28
28
|
* The pane's own `update()` unhides its shell whenever records exist, so the
|
|
29
29
|
* gate in `syncArtifactPane` must re-assert `persona-hidden` for artifacts
|
|
30
|
-
* whose resolved mode is "
|
|
30
|
+
* whose resolved mode is "collapsed" or "inline". The original implementation only
|
|
31
31
|
* removed the class in the show branch and never re-added it, so inline-mode
|
|
32
32
|
* artifacts still auto-opened the pane.
|
|
33
33
|
*/
|
|
@@ -88,8 +88,8 @@ describe("artifact pane auto-open gating by display mode", () => {
|
|
|
88
88
|
controller.destroy();
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
it('keeps the pane hidden for "
|
|
92
|
-
const { mount, controller } = mountWithDisplay("
|
|
91
|
+
it('keeps the pane hidden for "collapsed" mode until an explicit open', () => {
|
|
92
|
+
const { mount, controller } = mountWithDisplay("collapsed");
|
|
93
93
|
upsertSample(controller);
|
|
94
94
|
expect(paneEl(mount).classList.contains("persona-hidden")).toBe(true);
|
|
95
95
|
|
|
@@ -109,7 +109,7 @@ describe("artifact pane auto-open gating by display mode", () => {
|
|
|
109
109
|
urlWithBlob.createObjectURL = () => "blob:persona-test";
|
|
110
110
|
urlWithBlob.revokeObjectURL = () => {};
|
|
111
111
|
try {
|
|
112
|
-
const { mount, controller } = mountWithDisplay("
|
|
112
|
+
const { mount, controller } = mountWithDisplay("collapsed");
|
|
113
113
|
upsertSample(controller);
|
|
114
114
|
|
|
115
115
|
// Recreate the reference card the way the default renderer shapes it
|
|
@@ -170,7 +170,7 @@ describe("artifact card custom actions (full widget)", () => {
|
|
|
170
170
|
artifacts: {
|
|
171
171
|
enabled: true,
|
|
172
172
|
allowedTypes: ["markdown", "component"],
|
|
173
|
-
display: "
|
|
173
|
+
display: "collapsed",
|
|
174
174
|
cardActions,
|
|
175
175
|
},
|
|
176
176
|
},
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { createAgentExperience } from "./ui";
|
|
6
|
+
import type {
|
|
7
|
+
AgentWidgetArtifactsFeature,
|
|
8
|
+
AgentWidgetConfig,
|
|
9
|
+
WidgetPreferenceSlice,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
// jsdom does not implement matchMedia; the pane's layout code touches it.
|
|
14
|
+
if (!window.matchMedia) {
|
|
15
|
+
window.matchMedia = ((query: string) => ({
|
|
16
|
+
matches: false,
|
|
17
|
+
media: query,
|
|
18
|
+
onchange: null,
|
|
19
|
+
addListener: () => {},
|
|
20
|
+
removeListener: () => {},
|
|
21
|
+
addEventListener: () => {},
|
|
22
|
+
removeEventListener: () => {},
|
|
23
|
+
dispatchEvent: () => false,
|
|
24
|
+
})) as unknown as typeof window.matchMedia;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Config-level `preferences` are per-instance display overrides baked over
|
|
30
|
+
* `features` (utils/feature-preferences.ts `resolveConfigPreferences`). The
|
|
31
|
+
* pane's auto-open state is the observable for the effective display mode:
|
|
32
|
+
* "panel" opens it on upsert, "inline"/"collapsed" keep it hidden.
|
|
33
|
+
*
|
|
34
|
+
* The update() path must re-resolve from the pre-preference base features:
|
|
35
|
+
* changing or clearing preferences may not stack onto previously overlaid
|
|
36
|
+
* values.
|
|
37
|
+
*/
|
|
38
|
+
describe("config preferences overrides", () => {
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
document.body.innerHTML = "";
|
|
41
|
+
try {
|
|
42
|
+
window.localStorage.clear();
|
|
43
|
+
} catch {
|
|
44
|
+
/* jsdom edge cases */
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function mountWith(
|
|
49
|
+
display: AgentWidgetArtifactsFeature["display"],
|
|
50
|
+
preferences?: WidgetPreferenceSlice
|
|
51
|
+
) {
|
|
52
|
+
const mount = document.createElement("div");
|
|
53
|
+
document.body.appendChild(mount);
|
|
54
|
+
const config: AgentWidgetConfig = {
|
|
55
|
+
apiUrl: "https://api.example.com/chat",
|
|
56
|
+
launcher: { enabled: false },
|
|
57
|
+
features: {
|
|
58
|
+
artifacts: {
|
|
59
|
+
enabled: true,
|
|
60
|
+
allowedTypes: ["markdown", "component"],
|
|
61
|
+
...(display ? { display } : {}),
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
...(preferences ? { preferences } : {}),
|
|
65
|
+
};
|
|
66
|
+
const controller = createAgentExperience(mount, config);
|
|
67
|
+
return { mount, controller };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function paneHidden(mount: HTMLElement): boolean {
|
|
71
|
+
const el = mount.querySelector<HTMLElement>(".persona-artifact-pane");
|
|
72
|
+
expect(el).not.toBeNull();
|
|
73
|
+
return el!.classList.contains("persona-hidden");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function upsertSample(
|
|
77
|
+
controller: ReturnType<typeof createAgentExperience>,
|
|
78
|
+
id = "prefs-test"
|
|
79
|
+
) {
|
|
80
|
+
controller.upsertArtifact({
|
|
81
|
+
id,
|
|
82
|
+
title: "Preferences test",
|
|
83
|
+
artifactType: "markdown",
|
|
84
|
+
content: "# Hello",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it("applies initial preferences over base features", () => {
|
|
89
|
+
// Base says panel (auto-open); the instance preference says inline.
|
|
90
|
+
const { mount, controller } = mountWith("panel", {
|
|
91
|
+
artifacts: { display: "inline" },
|
|
92
|
+
});
|
|
93
|
+
upsertSample(controller);
|
|
94
|
+
expect(paneHidden(mount)).toBe(true);
|
|
95
|
+
controller.destroy();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("applies preferences passed through controller.update()", () => {
|
|
99
|
+
const { mount, controller } = mountWith("panel");
|
|
100
|
+
controller.update({ preferences: { artifacts: { display: "collapsed" } } });
|
|
101
|
+
upsertSample(controller);
|
|
102
|
+
expect(paneHidden(mount)).toBe(true);
|
|
103
|
+
controller.destroy();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// A "panel" upsert marks the pane user-opened, which is sticky by design,
|
|
107
|
+
// so the remaining tests observe the effective mode through the transcript
|
|
108
|
+
// block type instead: refreshArtifactReferenceBlocks re-materializes blocks
|
|
109
|
+
// on a live display change ("inline" → .persona-artifact-inline preview,
|
|
110
|
+
// otherwise the reference card).
|
|
111
|
+
function inlineBlockCount(mount: HTMLElement): number {
|
|
112
|
+
return mount.querySelectorAll(".persona-artifact-inline").length;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
it("clearing preferences reverts to the base features", () => {
|
|
116
|
+
const { mount, controller } = mountWith("inline", {
|
|
117
|
+
artifacts: { display: "panel" },
|
|
118
|
+
});
|
|
119
|
+
upsertSample(controller);
|
|
120
|
+
expect(inlineBlockCount(mount)).toBe(0);
|
|
121
|
+
|
|
122
|
+
// Explicit-undefined deletes the key (config-merge reset semantics); the
|
|
123
|
+
// effective mode must fall back to the base "inline", not keep "panel".
|
|
124
|
+
controller.update({ preferences: undefined });
|
|
125
|
+
expect(inlineBlockCount(mount)).toBe(1);
|
|
126
|
+
controller.destroy();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("an update's slice replaces the previous slice; dropped keys revert", () => {
|
|
130
|
+
const { mount, controller } = mountWith("collapsed");
|
|
131
|
+
controller.update({
|
|
132
|
+
preferences: {
|
|
133
|
+
showReasoning: false,
|
|
134
|
+
artifacts: { display: "inline" },
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
upsertSample(controller);
|
|
138
|
+
expect(inlineBlockCount(mount)).toBe(1);
|
|
139
|
+
|
|
140
|
+
// The new slice keeps showReasoning but drops the artifacts key. Patch
|
|
141
|
+
// semantics would strand display:"inline"; replace semantics revert the
|
|
142
|
+
// block to the base "collapsed" card.
|
|
143
|
+
controller.update({ preferences: { showReasoning: false } });
|
|
144
|
+
expect(inlineBlockCount(mount)).toBe(0);
|
|
145
|
+
controller.destroy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("replacing preferences re-resolves from the base, not the prior overlay", () => {
|
|
149
|
+
const { mount, controller } = mountWith(
|
|
150
|
+
{ default: "collapsed", byKind: { markdown: "inline" } },
|
|
151
|
+
{ artifacts: { display: "panel" } }
|
|
152
|
+
);
|
|
153
|
+
upsertSample(controller);
|
|
154
|
+
expect(inlineBlockCount(mount)).toBe(0);
|
|
155
|
+
|
|
156
|
+
// The object-form preference refines the base, so byKind.markdown →
|
|
157
|
+
// "inline" survives and the block converts to an inline preview. If the
|
|
158
|
+
// prior "panel" string overlay had been folded into the base, byKind
|
|
159
|
+
// would be gone and this would resolve "collapsed" (a card).
|
|
160
|
+
controller.update({
|
|
161
|
+
preferences: { artifacts: { display: { default: "collapsed" } } },
|
|
162
|
+
});
|
|
163
|
+
expect(inlineBlockCount(mount)).toBe(1);
|
|
164
|
+
controller.destroy();
|
|
165
|
+
});
|
|
166
|
+
});
|