@runtypelabs/persona 4.14.0 → 4.16.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 (119) hide show
  1. package/README.md +4 -4
  2. package/dist/animations/glyph-cycle.d.cts +1 -1
  3. package/dist/animations/glyph-cycle.d.ts +1 -1
  4. package/dist/animations/{types-DveIaNx6.d.cts → types-BpWGm7_6.d.cts} +17 -0
  5. package/dist/animations/{types-DveIaNx6.d.ts → types-BpWGm7_6.d.ts} +17 -0
  6. package/dist/animations/wipe.d.cts +1 -1
  7. package/dist/animations/wipe.d.ts +1 -1
  8. package/dist/chunk-QJ4LV2KW.js +1 -0
  9. package/dist/codegen.cjs +12 -11
  10. package/dist/codegen.js +14 -13
  11. package/dist/{context-mentions-ONG7A7M6.js → context-mentions-BTSTIBFC.js} +1 -1
  12. package/dist/context-mentions.cjs +11 -8
  13. package/dist/context-mentions.d.cts +520 -21
  14. package/dist/context-mentions.d.ts +520 -21
  15. package/dist/context-mentions.js +11 -8
  16. package/dist/index.cjs +69 -68
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +595 -46
  19. package/dist/index.d.ts +595 -46
  20. package/dist/index.global.js +60 -57
  21. package/dist/index.global.js.map +1 -1
  22. package/dist/index.js +69 -68
  23. package/dist/index.js.map +1 -1
  24. package/dist/launcher.global.js +5 -2
  25. package/dist/launcher.global.js.map +1 -1
  26. package/dist/smart-dom-reader.d.cts +520 -21
  27. package/dist/smart-dom-reader.d.ts +520 -21
  28. package/dist/testing.cjs +3 -3
  29. package/dist/testing.d.cts +7 -0
  30. package/dist/testing.d.ts +7 -0
  31. package/dist/testing.js +3 -3
  32. package/dist/theme-editor-preview.cjs +67 -67
  33. package/dist/theme-editor-preview.d.cts +534 -22
  34. package/dist/theme-editor-preview.d.ts +534 -22
  35. package/dist/theme-editor-preview.js +57 -57
  36. package/dist/theme-editor.cjs +10 -10
  37. package/dist/theme-editor.d.cts +521 -23
  38. package/dist/theme-editor.d.ts +521 -23
  39. package/dist/theme-editor.js +10 -10
  40. package/dist/theme-reference.cjs +1 -1
  41. package/dist/theme-reference.d.cts +104 -3
  42. package/dist/theme-reference.d.ts +104 -3
  43. package/dist/theme-reference.js +1 -1
  44. package/dist/widget.css +1 -1
  45. package/package.json +1 -1
  46. package/src/ask-user-question-tool.ts +10 -7
  47. package/src/components/artifact-pane.test.ts +27 -0
  48. package/src/components/artifact-pane.ts +7 -3
  49. package/src/components/artifact-preview.ts +1 -1
  50. package/src/components/composer-parts.test.ts +103 -4
  51. package/src/components/composer-parts.ts +53 -15
  52. package/src/components/header-builder.ts +27 -2
  53. package/src/components/header-layouts.test.ts +56 -0
  54. package/src/components/header-layouts.ts +29 -9
  55. package/src/components/launcher-surface.test.ts +403 -0
  56. package/src/components/launcher.ts +297 -2
  57. package/src/components/messages.ts +5 -2
  58. package/src/components/panel.ts +92 -71
  59. package/src/components/pill-composer-builder.test.ts +18 -0
  60. package/src/components/suggestions.test.ts +273 -0
  61. package/src/components/suggestions.ts +528 -86
  62. package/src/components/welcome.test.ts +121 -0
  63. package/src/components/welcome.ts +305 -0
  64. package/src/components/widget-view.test.ts +5 -3
  65. package/src/components/widget-view.ts +18 -7
  66. package/src/defaults.ts +7 -5
  67. package/src/generated/runtype-openapi-contract.ts +17 -0
  68. package/src/index-core.ts +29 -2
  69. package/src/launcher-global.ts +11 -8
  70. package/src/plugins/types.ts +136 -3
  71. package/src/presets.ts +3 -3
  72. package/src/session.ts +10 -8
  73. package/src/styles/widget-styles.test.ts +382 -0
  74. package/src/styles/widget.css +778 -4
  75. package/src/suggest-replies-tool.test.ts +233 -2
  76. package/src/suggest-replies-tool.ts +115 -39
  77. package/src/testing/mock-stream.test.ts +19 -0
  78. package/src/testing/mock-stream.ts +20 -0
  79. package/src/theme-editor/preview-utils.test.ts +34 -0
  80. package/src/theme-editor/preview-utils.ts +77 -2
  81. package/src/theme-editor/sections.test.ts +33 -0
  82. package/src/theme-editor/sections.ts +83 -6
  83. package/src/theme-reference.ts +34 -5
  84. package/src/types/theme.ts +72 -3
  85. package/src/types.ts +312 -18
  86. package/src/ui.body-top-slot.test.ts +95 -0
  87. package/src/ui.content-width.test.ts +91 -0
  88. package/src/ui.detached-panel.test.ts +1 -1
  89. package/src/ui.docked.test.ts +7 -4
  90. package/src/ui.launcher-teaser.test.ts +162 -0
  91. package/src/ui.rebuild-composer.test.ts +491 -0
  92. package/src/ui.render-welcome.test.ts +505 -0
  93. package/src/ui.scroll-additive.test.ts +91 -21
  94. package/src/ui.scroll.test.ts +302 -14
  95. package/src/ui.suggest-replies.test.ts +1112 -1
  96. package/src/ui.ts +1008 -149
  97. package/src/ui.welcome.test.ts +504 -0
  98. package/src/utils/artifact-file.test.ts +1 -1
  99. package/src/utils/artifact-gate.test.ts +27 -0
  100. package/src/utils/artifact-gate.ts +9 -0
  101. package/src/utils/attachment-manager.test.ts +37 -0
  102. package/src/utils/attachment-manager.ts +15 -0
  103. package/src/utils/auto-follow.test.ts +23 -0
  104. package/src/utils/auto-follow.ts +11 -1
  105. package/src/utils/code-generators.test.ts +40 -0
  106. package/src/utils/code-generators.ts +22 -0
  107. package/src/utils/config-merge.test.ts +24 -0
  108. package/src/utils/config-merge.ts +2 -0
  109. package/src/utils/content-width.test.ts +37 -0
  110. package/src/utils/content-width.ts +17 -0
  111. package/src/utils/icons.test.ts +26 -0
  112. package/src/utils/icons.ts +8 -0
  113. package/src/utils/plugin-storage.test.ts +94 -0
  114. package/src/utils/plugin-storage.ts +98 -0
  115. package/src/utils/theme.test.ts +216 -0
  116. package/src/utils/tokens.ts +113 -3
  117. package/src/welcome.test.ts +213 -0
  118. package/src/welcome.ts +130 -0
  119. package/dist/chunk-MMUPR2JW.js +0 -1
@@ -563,6 +563,46 @@ describe("Full Config with Hooks Integration", () => {
563
563
  // Backward Compatibility Tests
564
564
  // =============================================================================
565
565
 
566
+ describe("Suggestions Serialization", () => {
567
+ it("preserves structured suggestion configuration in every format", () => {
568
+ const config = {
569
+ ...minimalConfig,
570
+ suggestions: {
571
+ starters: {
572
+ variant: "card",
573
+ behavior: "fill",
574
+ items: [
575
+ {
576
+ id: "compare",
577
+ label: "Compare plans",
578
+ prompt: "Compare plans for my team",
579
+ },
580
+ ],
581
+ },
582
+ followUps: {
583
+ placement: "after-message",
584
+ overflow: "scroll",
585
+ },
586
+ },
587
+ };
588
+ const formats: CodeFormat[] = [
589
+ "esm",
590
+ "react-component",
591
+ "react-advanced",
592
+ "script-installer",
593
+ "script-manual",
594
+ "script-advanced",
595
+ ];
596
+
597
+ for (const format of formats) {
598
+ const code = generateCodeSnippet(config, format);
599
+ expect(code).toContain("suggestions");
600
+ expect(code).toContain("Compare plans");
601
+ expect(code).toContain("after-message");
602
+ }
603
+ });
604
+ });
605
+
566
606
  describe("Backward Compatibility", () => {
567
607
  it("should work without options parameter", () => {
568
608
  const code = generateCodeSnippet(minimalConfig, "esm");
@@ -430,6 +430,19 @@ function generateMarkdownConfig(config: any, indent: string): string[] {
430
430
  return lines;
431
431
  }
432
432
 
433
+ // Suggestions may contain nested rich items. JSON object syntax is also valid
434
+ // JavaScript/TypeScript, so preserve the complete public shape instead of
435
+ // duplicating every leaf in each output-format generator.
436
+ function generateSuggestionsConfig(config: any, indent: string): string[] {
437
+ if (!config.suggestions) return [];
438
+ const serialized = JSON.stringify(config.suggestions, null, 2).split("\n");
439
+ return serialized.map((line, index) => {
440
+ const prefix = index === 0 ? `${indent}suggestions: ` : indent;
441
+ const suffix = index === serialized.length - 1 ? "," : "";
442
+ return `${prefix}${line}${suffix}`;
443
+ });
444
+ }
445
+
433
446
  // Helper to generate layout config (excluding render functions and slots)
434
447
  function generateLayoutConfig(config: any, indent: string): string[] {
435
448
  const lines: string[] = [];
@@ -697,6 +710,8 @@ function generateESMCode(config: any, options?: CodeGeneratorOptions): string {
697
710
  lines.push(" },");
698
711
  }
699
712
 
713
+ lines.push(...generateSuggestionsConfig(config, " "));
714
+
700
715
  if (config.suggestionChips && config.suggestionChips.length > 0) {
701
716
  lines.push(" suggestionChips: [");
702
717
  config.suggestionChips.forEach((chip: string) => {
@@ -845,6 +860,8 @@ function generateReactComponentCode(config: any, options?: CodeGeneratorOptions)
845
860
  lines.push(" },");
846
861
  }
847
862
 
863
+ lines.push(...generateSuggestionsConfig(config, " "));
864
+
848
865
  if (config.suggestionChips && config.suggestionChips.length > 0) {
849
866
  lines.push(" suggestionChips: [");
850
867
  config.suggestionChips.forEach((chip: string) => {
@@ -1110,6 +1127,8 @@ function generateReactAdvancedCode(config: any, options?: CodeGeneratorOptions):
1110
1127
  lines.push(" },");
1111
1128
  }
1112
1129
 
1130
+ lines.push(...generateSuggestionsConfig(config, " "));
1131
+
1113
1132
  if (config.suggestionChips && config.suggestionChips.length > 0) {
1114
1133
  lines.push(" suggestionChips: [");
1115
1134
  config.suggestionChips.forEach((chip: string) => {
@@ -1312,6 +1331,7 @@ function buildSerializableConfig(config: any): Record<string, any> {
1312
1331
  if (config.voiceRecognition) serializableConfig.voiceRecognition = config.voiceRecognition;
1313
1332
  if (config.statusIndicator) serializableConfig.statusIndicator = config.statusIndicator;
1314
1333
  if (config.features) serializableConfig.features = config.features;
1334
+ if (config.suggestions) serializableConfig.suggestions = config.suggestions;
1315
1335
  if (config.suggestionChips?.length > 0) serializableConfig.suggestionChips = config.suggestionChips;
1316
1336
  if (config.suggestionChipsConfig) serializableConfig.suggestionChipsConfig = config.suggestionChipsConfig;
1317
1337
  if (config.debug) serializableConfig.debug = config.debug;
@@ -1520,6 +1540,8 @@ function generateScriptManualCode(config: any, options?: CodeGeneratorOptions):
1520
1540
  lines.push(" },");
1521
1541
  }
1522
1542
 
1543
+ lines.push(...generateSuggestionsConfig(config, " "));
1544
+
1523
1545
  if (config.suggestionChips && config.suggestionChips.length > 0) {
1524
1546
  lines.push(" suggestionChips: [");
1525
1547
  config.suggestionChips.forEach((chip: string) => {
@@ -32,6 +32,30 @@ describe("mergeConfigUpdate", () => {
32
32
  expect(next.suggestionChips).toEqual(["x"]);
33
33
  });
34
34
 
35
+ it("patches suggestion surfaces without dropping sibling behavior", () => {
36
+ const prev = base({
37
+ suggestions: {
38
+ starters: { variant: "card", behavior: "fill" },
39
+ followUps: { variant: "chip", placement: "after-message" },
40
+ },
41
+ });
42
+ const next = mergeConfigUpdate(prev, {
43
+ suggestions: {
44
+ followUps: { overflow: "wrap" },
45
+ },
46
+ });
47
+
48
+ expect(next.suggestions?.starters).toEqual({
49
+ variant: "card",
50
+ behavior: "fill",
51
+ });
52
+ expect(next.suggestions?.followUps).toEqual({
53
+ variant: "chip",
54
+ placement: "after-message",
55
+ overflow: "wrap",
56
+ });
57
+ });
58
+
35
59
  it("replaces callbacks wholesale", () => {
36
60
  const first = vi.fn();
37
61
  const second = vi.fn();
@@ -25,6 +25,8 @@ const REPLACE_LEAF_PATHS = new Set<string>([
25
25
  "features.artifacts.display",
26
26
  "features.streamAnimation.plugins",
27
27
  "preferences",
28
+ // Discriminated union: recursive merge would splice two icon variants.
29
+ "welcome.icon",
28
30
  ]);
29
31
 
30
32
  // Recursive patch merge. A key merges recursively iff both previous and patch
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { resolveContentMaxWidth } from "./content-width";
4
+ import type { AgentWidgetConfig } from "../types";
5
+
6
+ const cfg = (value?: Partial<AgentWidgetConfig>) =>
7
+ value as AgentWidgetConfig | undefined;
8
+
9
+ describe("resolveContentMaxWidth", () => {
10
+ it("defaults panels to the surveyed 768px column", () => {
11
+ expect(resolveContentMaxWidth(cfg({}), false)).toBe("768px");
12
+ expect(resolveContentMaxWidth(undefined, false)).toBe("768px");
13
+ });
14
+
15
+ it("keeps the composer-bar's own 720px fallback", () => {
16
+ expect(resolveContentMaxWidth(cfg({}), true)).toBe("720px");
17
+ });
18
+
19
+ it("lets layout.contentMaxWidth win everywhere", () => {
20
+ const config = cfg({ layout: { contentMaxWidth: "90ch" } });
21
+ expect(resolveContentMaxWidth(config, false)).toBe("90ch");
22
+ expect(resolveContentMaxWidth(config, true)).toBe("90ch");
23
+ });
24
+
25
+ it("passes the \"none\" opt-out through untouched", () => {
26
+ expect(
27
+ resolveContentMaxWidth(cfg({ layout: { contentMaxWidth: "none" } }), false)
28
+ ).toBe("none");
29
+ });
30
+
31
+ it("prefers composerBar.contentMaxWidth over the 720px fallback", () => {
32
+ const config = cfg({
33
+ launcher: { composerBar: { contentMaxWidth: "600px" } },
34
+ });
35
+ expect(resolveContentMaxWidth(config, true)).toBe("600px");
36
+ });
37
+ });
@@ -0,0 +1,17 @@
1
+ import type { AgentWidgetConfig } from "../types";
2
+
3
+ /**
4
+ * Resolver-owned default: every surveyed product caps the conversation
5
+ * column (704 to 896px, modal value 768px,
6
+ * "Hero width versus transcript width"). Kept out of defaults.ts so a
7
+ * materialized value cannot override the composer-bar's own 720px fallback.
8
+ * `"none"` opts out (applied as `max-width: none`).
9
+ */
10
+ export const resolveContentMaxWidth = (
11
+ config: AgentWidgetConfig | undefined,
12
+ composerBar: boolean
13
+ ): string =>
14
+ config?.layout?.contentMaxWidth ??
15
+ (composerBar
16
+ ? config?.launcher?.composerBar?.contentMaxWidth ?? "720px"
17
+ : "768px");
@@ -0,0 +1,26 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ import { renderLucideIcon } from "./icons";
6
+
7
+ describe("renderLucideIcon", () => {
8
+ it.each(["lightbulb", "chart-column", "image", "pen-line"] as const)(
9
+ "renders %s as an inline svg",
10
+ (name) => {
11
+ const svg = renderLucideIcon(name);
12
+ expect(svg).not.toBeNull();
13
+ expect(svg?.tagName.toLowerCase()).toBe("svg");
14
+ expect(svg?.getAttribute("viewBox")).toBe("0 0 24 24");
15
+ // Geometry, not an empty shell.
16
+ expect(svg!.childNodes.length).toBeGreaterThan(0);
17
+ }
18
+ );
19
+
20
+ it("returns null and warns for a name outside the registry", () => {
21
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
22
+ expect(renderLucideIcon("definitely-not-an-icon")).toBeNull();
23
+ expect(warn).toHaveBeenCalled();
24
+ warn.mockRestore();
25
+ });
26
+ });
@@ -70,6 +70,7 @@ import {
70
70
  Plus,
71
71
  Minus,
72
72
  Pencil,
73
+ PenLine,
73
74
  Trash,
74
75
  Trash2,
75
76
  Save,
@@ -117,12 +118,15 @@ import {
117
118
  Folder,
118
119
  FolderOpen,
119
120
  Files,
121
+ // ---------- Data ----------
122
+ ChartColumn,
120
123
  // ---------- Decorative ----------
121
124
  Sparkles,
122
125
  Zap,
123
126
  Sun,
124
127
  Moon,
125
128
  Flag,
129
+ Lightbulb,
126
130
  // ---------- Devices ----------
127
131
  Monitor,
128
132
  Smartphone,
@@ -220,6 +224,7 @@ const LUCIDE_ICONS = {
220
224
  "plus": Plus,
221
225
  "minus": Minus,
222
226
  "pencil": Pencil,
227
+ "pen-line": PenLine,
223
228
  "trash": Trash,
224
229
  "trash-2": Trash2,
225
230
  "save": Save,
@@ -267,12 +272,15 @@ const LUCIDE_ICONS = {
267
272
  "folder": Folder,
268
273
  "folder-open": FolderOpen,
269
274
  "files": Files,
275
+ // Data
276
+ "chart-column": ChartColumn,
270
277
  // Decorative
271
278
  "sparkles": Sparkles,
272
279
  "zap": Zap,
273
280
  "sun": Sun,
274
281
  "moon": Moon,
275
282
  "flag": Flag,
283
+ "lightbulb": Lightbulb,
276
284
  // Devices
277
285
  "monitor": Monitor,
278
286
  "smartphone": Smartphone,
@@ -0,0 +1,94 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { createPluginStorageFactory, pluginStorageKey } from "./plugin-storage";
6
+ import type { AgentWidgetConfig } from "../types";
7
+
8
+ const config = (patch: Partial<AgentWidgetConfig> = {}): AgentWidgetConfig =>
9
+ ({ apiUrl: "https://api.example.com/chat", ...patch }) as AgentWidgetConfig;
10
+
11
+ describe("plugin storage facade", () => {
12
+ afterEach(() => {
13
+ window.localStorage.clear();
14
+ vi.restoreAllMocks();
15
+ });
16
+
17
+ it("namespaces keys by keyPrefix and plugin id", () => {
18
+ expect(pluginStorageKey(config(), "home-screen", "view")).toBe(
19
+ "persona-plugin:home-screen:view"
20
+ );
21
+ expect(
22
+ pluginStorageKey(
23
+ config({ persistState: { keyPrefix: "myapp-" } }),
24
+ "home-screen",
25
+ "view"
26
+ )
27
+ ).toBe("myapp-plugin:home-screen:view");
28
+ });
29
+
30
+ it("reads and writes localStorage under the namespaced key", () => {
31
+ const storage = createPluginStorageFactory(() => config())("pre-chat");
32
+ storage.set("identity", "ada");
33
+
34
+ expect(window.localStorage.getItem("persona-plugin:pre-chat:identity")).toBe(
35
+ "ada"
36
+ );
37
+ expect(storage.get("identity")).toBe("ada");
38
+
39
+ storage.remove("identity");
40
+ expect(storage.get("identity")).toBeNull();
41
+ });
42
+
43
+ it("keeps two plugins from colliding on the same key", () => {
44
+ const factory = createPluginStorageFactory(() => config());
45
+ factory("a").set("view", "home");
46
+ factory("b").set("view", "chat");
47
+
48
+ expect(factory("a").get("view")).toBe("home");
49
+ expect(factory("b").get("view")).toBe("chat");
50
+ });
51
+
52
+ it("downgrades to an in-memory map under persistState: false", () => {
53
+ const storage = createPluginStorageFactory(() =>
54
+ config({ persistState: false })
55
+ )("pre-chat");
56
+ storage.set("identity", "ada");
57
+
58
+ expect(storage.get("identity")).toBe("ada");
59
+ expect(window.localStorage.getItem("persona-plugin:pre-chat:identity")).toBeNull();
60
+ });
61
+
62
+ it("falls back to memory when localStorage access throws (private mode)", () => {
63
+ const setItem = vi
64
+ .spyOn(Storage.prototype, "setItem")
65
+ .mockImplementation(() => {
66
+ throw new Error("blocked");
67
+ });
68
+ const getItem = vi
69
+ .spyOn(Storage.prototype, "getItem")
70
+ .mockImplementation(() => {
71
+ throw new Error("blocked");
72
+ });
73
+
74
+ const storage = createPluginStorageFactory(() => config())("home-screen");
75
+ expect(() => storage.set("view", "home")).not.toThrow();
76
+ expect(storage.get("view")).toBe("home");
77
+
78
+ setItem.mockRestore();
79
+ getItem.mockRestore();
80
+ });
81
+
82
+ it("follows a live keyPrefix change through controller.update()", () => {
83
+ let current = config();
84
+ const storage = createPluginStorageFactory(() => current)("home-screen");
85
+ storage.set("view", "home");
86
+
87
+ current = config({ persistState: { keyPrefix: "next-" } });
88
+ expect(storage.get("view")).toBeNull();
89
+ storage.set("view", "chat");
90
+ expect(window.localStorage.getItem("next-plugin:home-screen:view")).toBe(
91
+ "chat"
92
+ );
93
+ });
94
+ });
@@ -0,0 +1,98 @@
1
+ import type { AgentWidgetConfig } from "../types";
2
+
3
+ /**
4
+ * Synchronous key-value store handed to plugin render hooks. Backed by
5
+ * `localStorage`, namespaced per plugin id. The async `storageAdapter` cannot
6
+ * back a synchronous API and `persistState` is not a general key-value surface.
7
+ */
8
+ export interface AgentWidgetPluginStorage {
9
+ get: (key: string) => string | null;
10
+ set: (key: string, value: string) => void;
11
+ remove: (key: string) => void;
12
+ }
13
+
14
+ /** One factory per widget instance; the in-memory fallback is shared by it. */
15
+ export type PluginStorageFactory = (
16
+ pluginId: string
17
+ ) => AgentWidgetPluginStorage;
18
+
19
+ const resolveKeyPrefix = (config: AgentWidgetConfig | undefined): string => {
20
+ const persist = config?.persistState;
21
+ return (
22
+ (typeof persist === "object" ? persist?.keyPrefix : undefined) ?? "persona-"
23
+ );
24
+ };
25
+
26
+ export const pluginStorageKey = (
27
+ config: AgentWidgetConfig | undefined,
28
+ pluginId: string,
29
+ key: string
30
+ ): string => `${resolveKeyPrefix(config)}plugin:${pluginId}:${key}`;
31
+
32
+ /**
33
+ * `persistState: false` and blocked storage both downgrade to a per-instance
34
+ * Map. localStorage access can throw (Safari private mode, partitioned
35
+ * iframes), so the first throw switches this instance to the Map for good.
36
+ */
37
+ export const createPluginStorageFactory = (
38
+ getConfig: () => AgentWidgetConfig | undefined
39
+ ): PluginStorageFactory => {
40
+ const memory = new Map<string, string>();
41
+ let blocked = false;
42
+
43
+ const store = (): Storage | null => {
44
+ if (blocked) return null;
45
+ if (getConfig()?.persistState === false) return null;
46
+ try {
47
+ if (typeof window === "undefined" || !window.localStorage) return null;
48
+ return window.localStorage;
49
+ } catch {
50
+ blocked = true;
51
+ return null;
52
+ }
53
+ };
54
+
55
+ return (pluginId: string): AgentWidgetPluginStorage => {
56
+ const keyFor = (key: string) =>
57
+ pluginStorageKey(getConfig(), pluginId, key);
58
+
59
+ return {
60
+ get: (key) => {
61
+ const full = keyFor(key);
62
+ const local = store();
63
+ if (local) {
64
+ try {
65
+ return local.getItem(full);
66
+ } catch {
67
+ blocked = true;
68
+ }
69
+ }
70
+ return memory.get(full) ?? null;
71
+ },
72
+ set: (key, value) => {
73
+ const full = keyFor(key);
74
+ const local = store();
75
+ if (local) {
76
+ try {
77
+ local.setItem(full, value);
78
+ return;
79
+ } catch {
80
+ blocked = true;
81
+ }
82
+ }
83
+ memory.set(full, value);
84
+ },
85
+ remove: (key) => {
86
+ const full = keyFor(key);
87
+ memory.delete(full);
88
+ const local = store();
89
+ if (!local) return;
90
+ try {
91
+ local.removeItem(full);
92
+ } catch {
93
+ blocked = true;
94
+ }
95
+ },
96
+ };
97
+ };
98
+ };