@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
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
+ });