@runtypelabs/persona 4.13.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 (64) 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.ts +43 -25
  56. package/src/ui.typing-indicator.test.ts +135 -0
  57. package/src/utils/artifact-display.test.ts +180 -16
  58. package/src/utils/artifact-display.ts +155 -14
  59. package/src/utils/config-merge.test.ts +60 -0
  60. package/src/utils/config-merge.ts +6 -1
  61. package/src/utils/feature-preferences.test.ts +326 -0
  62. package/src/utils/feature-preferences.ts +498 -0
  63. package/src/utils/theme.test.ts +92 -0
  64. 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
- | "card" // reference card in transcript; pane opens on click
1380
- | "panel" // reference card in transcript; pane also auto-opens on artifact_start (current default behavior)
1381
- | "inline"; // artifact preview renders directly in the transcript; no pane involvement
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 per-type overrides.
1501
+ * the object form sets a default plus scoped overrides.
1443
1502
  * Defaults to "panel" (current behavior).
1444
1503
  */
1445
1504
  display?:
1446
- | PersonaArtifactDisplayMode
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 "card" or "inline". The original implementation only
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 "card" mode until an explicit open', () => {
92
- const { mount, controller } = mountWithDisplay("card");
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("card");
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: "card",
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
+ });
package/src/ui.ts CHANGED
@@ -91,7 +91,7 @@ import { buildHeaderWithLayout } from "./components/header-layouts";
91
91
  import { positionMap } from "./utils/positioning";
92
92
  import type { HeaderElements as _HeaderElements, ComposerElements as _ComposerElements } from "./components/panel";
93
93
  import { MessageTransform, MessageActionCallbacks, LoadingIndicatorRenderer } from "./components/message-bubble";
94
- import { createStandardBubble, createTypingIndicator } from "./components/message-bubble";
94
+ import { createStandardBubble, createTypingIndicator, getBubbleClasses } from "./components/message-bubble";
95
95
  import { createReasoningBubble, reasoningExpansionState, updateReasoningBubbleUI } from "./components/reasoning-bubble";
96
96
  import { createToolBubble, toolExpansionState, updateToolBubbleUI } from "./components/tool-bubble";
97
97
  import {
@@ -133,6 +133,7 @@ import {
133
133
  shouldExpandLauncherForArtifacts
134
134
  } from "./utils/artifact-gate";
135
135
  import { resolveArtifactDisplayMode } from "./utils/artifact-display";
136
+ import { resolveConfigPreferences } from "./utils/feature-preferences";
136
137
  import { readFlexGapPx, resolveArtifactPaneWidthPx } from "./utils/artifact-resize";
137
138
  import { enhanceWithForms } from "./components/forms";
138
139
  import { pluginRegistry } from "./plugins/registry";
@@ -596,6 +597,11 @@ export const createAgentExperience = (
596
597
  }
597
598
 
598
599
  let config = mergeWithDefaults(initialConfig) as AgentWidgetConfig;
600
+ // `config.features` holds the preference-resolved view every read site uses;
601
+ // `baseFeatures` keeps the pre-preference base so update() re-resolves from
602
+ // it instead of stacking new preferences onto already-overlaid values.
603
+ let baseFeatures = config.features;
604
+ config = resolveConfigPreferences(config);
599
605
  // Note: applyThemeVariables is called after applyFullHeightStyles() below
600
606
  // because applyFullHeightStyles resets mount.style.cssText
601
607
 
@@ -1800,7 +1806,7 @@ export const createAgentExperience = (
1800
1806
  let artifactPaneExpandedPinned = false;
1801
1807
  // Whether the user explicitly opened the pane (card click, inline Expand,
1802
1808
  // showArtifacts(), programmatic upsert). Auto-open is otherwise reserved for
1803
- // artifacts whose resolved display mode is "panel": "card" keeps the card as
1809
+ // artifacts whose resolved display mode is "panel": "collapsed" keeps the card as
1804
1810
  // the only affordance and "inline" renders in the transcript. "inline" no
1805
1811
  // longer *auto*-opens the pane, but its Expand control is a deliberate,
1806
1812
  // user-driven open (setting artifactsPaneUserOpened, same as a card click);
@@ -1811,7 +1817,7 @@ export const createAgentExperience = (
1811
1817
  artifactsPaneUserOpened ||
1812
1818
  lastArtifactsState.artifacts.some(
1813
1819
  (a) =>
1814
- resolveArtifactDisplayMode(config.features?.artifacts, a.artifactType) === "panel"
1820
+ resolveArtifactDisplayMode(config.features?.artifacts, a) === "panel"
1815
1821
  );
1816
1822
  const artifactPaneVisible = () =>
1817
1823
  lastArtifactsState.artifacts.length > 0 &&
@@ -2010,7 +2016,7 @@ export const createAgentExperience = (
2010
2016
  event.preventDefault();
2011
2017
  event.stopPropagation();
2012
2018
  artifactsPaneUserHidden = false;
2013
- // Card click is an explicit open: it overrides the "card"/"inline"
2019
+ // Card click is an explicit open: it overrides the "collapsed"/"inline"
2014
2020
  // auto-open suppression for as long as artifacts exist.
2015
2021
  artifactsPaneUserOpened = true;
2016
2022
  session.selectArtifact(artifactId);
@@ -2518,7 +2524,7 @@ export const createAgentExperience = (
2518
2524
  } else if (lastArtifactsState.artifacts.length > 0 && artifactPaneCanShow()) {
2519
2525
  // User chose “show” again (e.g. programmatic showArtifacts): clear dismiss chrome
2520
2526
  // and force drawer open so narrow-host / mobile slide-out is not stuck off-screen.
2521
- // Artifacts whose display mode is "card" or "inline" don't auto-open the
2527
+ // Artifacts whose display mode is "collapsed" or "inline" don't auto-open the
2522
2528
  // pane (artifactPaneCanShow); it stays hidden until an explicit open or
2523
2529
  // until a "panel"-mode artifact arrives.
2524
2530
  artifactPaneApi.element.classList.remove("persona-hidden");
@@ -5045,28 +5051,30 @@ export const createAgentExperience = (
5045
5051
 
5046
5052
  // Only render if we have an indicator (allows hiding via returning null)
5047
5053
  if (typingIndicator) {
5048
- // Create a bubble wrapper for the typing indicator (similar to assistant messages)
5054
+ // Bubble wrapper for the typing indicator. It borrows the assistant
5055
+ // bubble's classes and background so the "thinking" bubble matches the
5056
+ // message bubbles that replace it under every layout preset.
5049
5057
  const typingBubble = document.createElement("div");
5050
5058
  const showBubble = config.loadingIndicator?.showBubble !== false; // default true
5059
+ const messageLayout = config.layout?.messages?.layout ?? "bubble";
5051
5060
  typingBubble.className = showBubble
5052
- ? [
5053
- "persona-rounded-2xl",
5054
- "persona-text-sm",
5055
- "persona-leading-relaxed",
5056
- "persona-shadow-sm",
5057
- "persona-bg-persona-surface",
5058
- "persona-border",
5059
- "persona-text-persona-primary",
5060
- "persona-px-5",
5061
- "persona-py-3"
5062
- ].join(" ")
5061
+ ? getBubbleClasses("assistant", messageLayout).join(" ")
5063
5062
  : [
5064
5063
  "persona-text-sm",
5065
5064
  "persona-leading-relaxed",
5066
- "persona-text-persona-primary"
5065
+ "persona-text-persona-text"
5067
5066
  ].join(" ");
5068
5067
  typingBubble.setAttribute("data-typing-indicator", "true");
5069
- typingBubble.style.borderColor = "var(--persona-message-assistant-border, var(--persona-border, #e5e7eb))";
5068
+ if (showBubble) {
5069
+ typingBubble.style.color =
5070
+ "var(--persona-message-assistant-text, var(--persona-text))";
5071
+ if (messageLayout !== "flat") {
5072
+ typingBubble.style.backgroundColor =
5073
+ "var(--persona-message-assistant-bg, var(--persona-container))";
5074
+ typingBubble.style.borderColor =
5075
+ "var(--persona-message-assistant-border, var(--persona-border, #e5e7eb))";
5076
+ }
5077
+ }
5070
5078
 
5071
5079
  typingBubble.appendChild(typingIndicator);
5072
5080
 
@@ -7846,8 +7854,11 @@ export const createAgentExperience = (
7846
7854
  const previousStreamAnimationType = config.features?.streamAnimation?.type;
7847
7855
  // One consistent recursive patch policy across the live controller and the
7848
7856
  // init handle. See utils/config-merge.ts for the replace-leaf list and
7849
- // explicit-undefined reset semantics.
7850
- config = mergeConfigUpdate(config, nextConfig);
7857
+ // explicit-undefined reset semantics. The patch merges over the
7858
+ // pre-preference base features, then preferences re-resolve on top.
7859
+ config = mergeConfigUpdate({ ...config, features: baseFeatures }, nextConfig);
7860
+ baseFeatures = config.features;
7861
+ config = resolveConfigPreferences(config);
7851
7862
  // applyFullHeightStyles resets mount.style.cssText, so call it before applyThemeVariables
7852
7863
  applyFullHeightStyles();
7853
7864
  applyThemeVariables(mount, config);
@@ -9282,13 +9293,20 @@ export const createAgentExperience = (
9282
9293
  if (!artifactsSidebarEnabled(config)) return null;
9283
9294
  // Programmatic upserts match the streamed UX: only "panel"-mode
9284
9295
  // artifacts auto-open the pane (overriding a previous Close), while
9285
- // "card"/"inline" stay calm — the injected transcript block is the
9296
+ // "collapsed"/"inline" stay calm — the injected transcript block is the
9286
9297
  // affordance. Independent of `transcript: false`: pane-only callers
9287
- // (e.g. the theme editor preview) rely on the panel-default surfacing;
9288
- // callers that want the pane in a non-panel mode call showArtifacts().
9298
+ // (e.g. the theme editor preview) rely on the auto-open default
9299
+ // surfacing; callers that want the pane in another mode call
9300
+ // showArtifacts().
9289
9301
  const mode = resolveArtifactDisplayMode(
9290
9302
  config.features?.artifacts,
9291
- manual.artifactType
9303
+ {
9304
+ artifactType: manual.artifactType,
9305
+ ...(manual.artifactType === "markdown" && manual.file
9306
+ ? { file: manual.file }
9307
+ : {}),
9308
+ ...(manual.presentation ? { presentation: manual.presentation } : {}),
9309
+ }
9292
9310
  );
9293
9311
  if (mode === "panel") {
9294
9312
  artifactsPaneUserHidden = false;