@runtypelabs/persona 1.48.0 → 2.0.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 (69) hide show
  1. package/README.md +140 -8
  2. package/dist/index.cjs +90 -39
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1055 -24
  5. package/dist/index.d.ts +1055 -24
  6. package/dist/index.global.js +111 -60
  7. package/dist/index.global.js.map +1 -1
  8. package/dist/index.js +90 -39
  9. package/dist/index.js.map +1 -1
  10. package/dist/install.global.js +1 -1
  11. package/dist/install.global.js.map +1 -1
  12. package/dist/widget.css +836 -513
  13. package/package.json +1 -1
  14. package/src/artifacts-session.test.ts +80 -0
  15. package/src/client.test.ts +20 -21
  16. package/src/client.ts +153 -4
  17. package/src/components/approval-bubble.ts +45 -42
  18. package/src/components/artifact-card.ts +91 -0
  19. package/src/components/artifact-pane.ts +501 -0
  20. package/src/components/composer-builder.ts +32 -27
  21. package/src/components/event-stream-view.ts +40 -40
  22. package/src/components/feedback.ts +36 -36
  23. package/src/components/forms.ts +11 -11
  24. package/src/components/header-builder.test.ts +32 -0
  25. package/src/components/header-builder.ts +55 -36
  26. package/src/components/header-layouts.ts +58 -125
  27. package/src/components/launcher.ts +36 -21
  28. package/src/components/message-bubble.ts +92 -65
  29. package/src/components/messages.ts +2 -2
  30. package/src/components/panel.ts +42 -11
  31. package/src/components/reasoning-bubble.ts +23 -23
  32. package/src/components/registry.ts +4 -0
  33. package/src/components/suggestions.ts +1 -1
  34. package/src/components/tool-bubble.ts +32 -32
  35. package/src/defaults.ts +30 -4
  36. package/src/index.ts +80 -2
  37. package/src/install.ts +22 -0
  38. package/src/plugins/types.ts +23 -0
  39. package/src/postprocessors.ts +2 -2
  40. package/src/runtime/host-layout.ts +174 -0
  41. package/src/runtime/init.test.ts +236 -0
  42. package/src/runtime/init.ts +114 -55
  43. package/src/session.ts +135 -2
  44. package/src/styles/tailwind.css +1 -1
  45. package/src/styles/widget.css +836 -513
  46. package/src/types/theme.ts +354 -0
  47. package/src/types.ts +314 -15
  48. package/src/ui.docked.test.ts +104 -0
  49. package/src/ui.ts +940 -227
  50. package/src/utils/artifact-gate.test.ts +255 -0
  51. package/src/utils/artifact-gate.ts +142 -0
  52. package/src/utils/artifact-resize.test.ts +64 -0
  53. package/src/utils/artifact-resize.ts +67 -0
  54. package/src/utils/attachment-manager.ts +10 -10
  55. package/src/utils/code-generators.test.ts +52 -0
  56. package/src/utils/code-generators.ts +40 -36
  57. package/src/utils/dock.ts +17 -0
  58. package/src/utils/dom-context.test.ts +504 -0
  59. package/src/utils/dom-context.ts +896 -0
  60. package/src/utils/dom.ts +12 -1
  61. package/src/utils/message-fingerprint.test.ts +187 -0
  62. package/src/utils/message-fingerprint.ts +105 -0
  63. package/src/utils/migration.ts +179 -0
  64. package/src/utils/morph.ts +1 -1
  65. package/src/utils/plugins.ts +175 -0
  66. package/src/utils/positioning.ts +4 -4
  67. package/src/utils/theme.test.ts +125 -0
  68. package/src/utils/theme.ts +216 -60
  69. package/src/utils/tokens.ts +682 -0
@@ -0,0 +1,236 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ type Listener = () => void;
6
+
7
+ const createAgentExperienceMock = vi.fn();
8
+
9
+ vi.mock("../ui", () => ({
10
+ createAgentExperience: createAgentExperienceMock,
11
+ }));
12
+
13
+ function createMockController(config?: { launcher?: { enabled?: boolean; autoExpand?: boolean } }) {
14
+ let open = (config?.launcher?.enabled ?? true) ? (config?.launcher?.autoExpand ?? false) : true;
15
+ const launcherEnabled = config?.launcher?.enabled ?? true;
16
+ const listeners = new Map<string, Set<Listener>>();
17
+
18
+ const emit = (event: string) => {
19
+ const handlers = listeners.get(event);
20
+ if (!handlers) return;
21
+ handlers.forEach((handler) => handler());
22
+ };
23
+
24
+ return {
25
+ update: vi.fn(),
26
+ destroy: vi.fn(),
27
+ getState: vi.fn(() => ({
28
+ open: launcherEnabled && open,
29
+ launcherEnabled,
30
+ voiceActive: false,
31
+ streaming: false,
32
+ })),
33
+ on: vi.fn((event: string, handler: Listener) => {
34
+ const handlers = listeners.get(event) ?? new Set<Listener>();
35
+ handlers.add(handler);
36
+ listeners.set(event, handlers);
37
+ return () => {
38
+ handlers.delete(handler);
39
+ };
40
+ }),
41
+ off: vi.fn(),
42
+ open: vi.fn(() => {
43
+ open = true;
44
+ emit("widget:opened");
45
+ }),
46
+ close: vi.fn(() => {
47
+ open = false;
48
+ emit("widget:closed");
49
+ }),
50
+ toggle: vi.fn(() => {
51
+ open = !open;
52
+ emit(open ? "widget:opened" : "widget:closed");
53
+ }),
54
+ clearChat: vi.fn(),
55
+ isOpen: vi.fn(() => open),
56
+ isVoiceActive: vi.fn(() => false),
57
+ getMessages: vi.fn(() => []),
58
+ getStatus: vi.fn(() => "idle"),
59
+ getPersistentMetadata: vi.fn(() => ({})),
60
+ updatePersistentMetadata: vi.fn(),
61
+ showCSATFeedback: vi.fn(),
62
+ submitCSATFeedback: vi.fn(),
63
+ showNPSFeedback: vi.fn(),
64
+ submitNPSFeedback: vi.fn(),
65
+ injectMessage: vi.fn(),
66
+ injectAssistantMessage: vi.fn(),
67
+ injectUserMessage: vi.fn(),
68
+ injectSystemMessage: vi.fn(),
69
+ injectMessageBatch: vi.fn(),
70
+ getMessageById: vi.fn(),
71
+ getLastMessage: vi.fn(),
72
+ focusInput: vi.fn(),
73
+ setComposerText: vi.fn(),
74
+ getComposerText: vi.fn(),
75
+ submitComposerText: vi.fn(),
76
+ showEventStream: vi.fn(),
77
+ hideEventStream: vi.fn(),
78
+ isEventStreamVisible: vi.fn(() => false),
79
+ startVoiceRecognition: vi.fn(() => false),
80
+ stopVoiceRecognition: vi.fn(() => false),
81
+ showArtifacts: vi.fn(),
82
+ hideArtifacts: vi.fn(),
83
+ clearArtifacts: vi.fn(),
84
+ onArtifactSelect: vi.fn(),
85
+ };
86
+ }
87
+
88
+ describe("initAgentWidget docked mode", () => {
89
+ beforeEach(() => {
90
+ document.body.innerHTML = "";
91
+ createAgentExperienceMock.mockReset();
92
+ createAgentExperienceMock.mockImplementation((_mount, config) => createMockController(config));
93
+ });
94
+
95
+ it("wraps the target in a dock shell and restores it on destroy", async () => {
96
+ const { initAgentWidget } = await import("./init");
97
+ const wrapper = document.createElement("div");
98
+ wrapper.innerHTML = `<div id="content">Workspace</div>`;
99
+ document.body.appendChild(wrapper);
100
+ const target = wrapper.querySelector<HTMLElement>("#content")!;
101
+
102
+ const handle = initAgentWidget({
103
+ target,
104
+ config: {
105
+ launcher: {
106
+ mountMode: "docked",
107
+ dock: { width: "420px", collapsedWidth: "72px" },
108
+ },
109
+ },
110
+ });
111
+
112
+ const shell = wrapper.querySelector<HTMLElement>('[data-persona-host-layout="docked"]');
113
+ expect(shell).not.toBeNull();
114
+ expect(shell?.querySelector('[data-persona-dock-role="content"]')?.firstElementChild).toBe(target);
115
+ expect(shell?.querySelector('[data-persona-dock-role="panel"]')).not.toBeNull();
116
+ expect(handle.host).toBe(shell?.querySelector('[data-persona-dock-role="host"]'));
117
+
118
+ handle.destroy();
119
+
120
+ expect(wrapper.firstElementChild).toBe(target);
121
+ expect(wrapper.querySelector('[data-persona-host-layout="docked"]')).toBeNull();
122
+ });
123
+
124
+ it("rejects body as a docked target", async () => {
125
+ const { initAgentWidget } = await import("./init");
126
+
127
+ expect(() =>
128
+ initAgentWidget({
129
+ target: document.body,
130
+ config: {
131
+ launcher: {
132
+ mountMode: "docked",
133
+ },
134
+ },
135
+ })
136
+ ).toThrow('Docked widget target must be a concrete container element');
137
+ });
138
+
139
+ it("updates dock width on open and close events", async () => {
140
+ const { initAgentWidget } = await import("./init");
141
+ document.body.innerHTML = `<div id="content">Workspace</div>`;
142
+
143
+ const handle = initAgentWidget({
144
+ target: "#content",
145
+ config: {
146
+ launcher: {
147
+ mountMode: "docked",
148
+ autoExpand: true,
149
+ dock: { width: "400px", collapsedWidth: "80px" },
150
+ },
151
+ },
152
+ });
153
+
154
+ const panelSlot = document.querySelector<HTMLElement>('[data-persona-dock-role="panel"]')!;
155
+ expect(panelSlot.style.width).toBe("400px");
156
+
157
+ handle.close();
158
+ expect(panelSlot.style.width).toBe("80px");
159
+
160
+ handle.open();
161
+ expect(panelSlot.style.width).toBe("400px");
162
+ });
163
+
164
+ it("rebuilds when mount mode changes from floating to docked", async () => {
165
+ const { initAgentWidget } = await import("./init");
166
+ document.body.innerHTML = `<div id="content">Workspace</div>`;
167
+
168
+ const handle = initAgentWidget({
169
+ target: "#content",
170
+ config: {
171
+ launcher: {
172
+ mountMode: "floating",
173
+ },
174
+ },
175
+ });
176
+
177
+ expect(createAgentExperienceMock).toHaveBeenCalledTimes(1);
178
+ handle.update({
179
+ launcher: {
180
+ mountMode: "docked",
181
+ dock: { side: "left", width: "460px", collapsedWidth: "88px" },
182
+ },
183
+ });
184
+
185
+ expect(createAgentExperienceMock).toHaveBeenCalledTimes(2);
186
+ const shell = document.querySelector<HTMLElement>('[data-persona-host-layout="docked"]');
187
+ expect(shell).not.toBeNull();
188
+ expect(shell?.firstElementChild?.getAttribute("data-persona-dock-role")).toBe("panel");
189
+ });
190
+
191
+ it("updates dock config in place without rebuilding the controller", async () => {
192
+ const { initAgentWidget } = await import("./init");
193
+ document.body.innerHTML = `<div id="content">Workspace</div>`;
194
+
195
+ const handle = initAgentWidget({
196
+ target: "#content",
197
+ config: {
198
+ launcher: {
199
+ mountMode: "docked",
200
+ dock: { side: "right", width: "420px", collapsedWidth: "72px" },
201
+ },
202
+ },
203
+ });
204
+
205
+ expect(createAgentExperienceMock).toHaveBeenCalledTimes(1);
206
+ handle.update({
207
+ launcher: {
208
+ dock: { side: "left", width: "500px", collapsedWidth: "96px" },
209
+ },
210
+ });
211
+
212
+ expect(createAgentExperienceMock).toHaveBeenCalledTimes(1);
213
+ const shell = document.querySelector<HTMLElement>('[data-persona-host-layout="docked"]');
214
+ const panelSlot = document.querySelector<HTMLElement>('[data-persona-dock-role="panel"]');
215
+ expect(shell?.firstElementChild?.getAttribute("data-persona-dock-role")).toBe("panel");
216
+ expect(panelSlot?.style.width).toBe("96px");
217
+ });
218
+
219
+ it("supports shadow DOM hosts in docked mode", async () => {
220
+ const { initAgentWidget } = await import("./init");
221
+ document.body.innerHTML = `<div id="content">Workspace</div>`;
222
+
223
+ const handle = initAgentWidget({
224
+ target: "#content",
225
+ useShadowDom: true,
226
+ config: {
227
+ launcher: {
228
+ mountMode: "docked",
229
+ },
230
+ },
231
+ });
232
+
233
+ expect(handle.host.shadowRoot).not.toBeNull();
234
+ expect(handle.host.shadowRoot?.querySelector("#persona-root")).not.toBeNull();
235
+ });
236
+ });
@@ -1,5 +1,7 @@
1
1
  import { createAgentExperience, AgentWidgetController } from "../ui";
2
2
  import { AgentWidgetConfig as _AgentWidgetConfig, AgentWidgetInitOptions, AgentWidgetEvent as _AgentWidgetEvent } from "../types";
3
+ import { isDockedMountMode } from "../utils/dock";
4
+ import { createWidgetHostLayout } from "./host-layout";
3
5
 
4
6
  const ensureTarget = (target: string | HTMLElement): HTMLElement => {
5
7
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -29,7 +31,7 @@ const widgetCssHref = (): string | null => {
29
31
  return null;
30
32
  };
31
33
 
32
- const mountStyles = (root: ShadowRoot | HTMLElement) => {
34
+ const mountStyles = (root: ShadowRoot | HTMLElement, ownerDocument: Document) => {
33
35
  const href = widgetCssHref();
34
36
 
35
37
  const adoptExistingStylesheet = () => {
@@ -41,7 +43,7 @@ const mountStyles = (root: ShadowRoot | HTMLElement) => {
41
43
  return;
42
44
  }
43
45
 
44
- const globalLink = document.head.querySelector<HTMLLinkElement>(
46
+ const globalLink = ownerDocument.head.querySelector<HTMLLinkElement>(
45
47
  'link[data-persona]'
46
48
  );
47
49
  if (!globalLink) {
@@ -55,7 +57,7 @@ const mountStyles = (root: ShadowRoot | HTMLElement) => {
55
57
  if (root instanceof ShadowRoot) {
56
58
  // For shadow DOM, we need to load CSS into the shadow root
57
59
  if (href) {
58
- const link = document.createElement("link");
60
+ const link = ownerDocument.createElement("link");
59
61
  link.rel = "stylesheet";
60
62
  link.href = href;
61
63
  link.setAttribute("data-persona", "true");
@@ -66,17 +68,17 @@ const mountStyles = (root: ShadowRoot | HTMLElement) => {
66
68
  // If href is null (IIFE build), CSS should already be loaded globally
67
69
  } else {
68
70
  // For non-shadow DOM, check if CSS is already loaded
69
- const existing = document.head.querySelector<HTMLLinkElement>(
71
+ const existing = ownerDocument.head.querySelector<HTMLLinkElement>(
70
72
  "link[data-persona]"
71
73
  );
72
74
  if (!existing) {
73
75
  if (href) {
74
76
  // ESM build - load CSS dynamically
75
- const link = document.createElement("link");
77
+ const link = ownerDocument.createElement("link");
76
78
  link.rel = "stylesheet";
77
79
  link.href = href;
78
80
  link.setAttribute("data-persona", "true");
79
- document.head.appendChild(link);
81
+ ownerDocument.head.appendChild(link);
80
82
  }
81
83
  // IIFE build - CSS should be loaded via <link> tag before script
82
84
  // If not found, we'll assume it's loaded globally or warn in dev
@@ -90,75 +92,132 @@ export const initAgentWidget = (
90
92
  options: AgentWidgetInitOptions
91
93
  ): AgentWidgetInitHandle => {
92
94
  const target = ensureTarget(options.target);
93
- const host = document.createElement("div");
94
- host.className = "persona-host";
95
-
96
- // When launcher is disabled (inline embed mode), ensure the host fills its container
97
- const launcherEnabled = options.config?.launcher?.enabled ?? true;
98
- if (!launcherEnabled) {
99
- host.style.height = "100%";
100
- }
101
-
102
- target.appendChild(host);
103
-
104
95
  const useShadow = options.useShadowDom === true;
105
- let mount: HTMLElement;
106
- let _root: ShadowRoot | HTMLElement;
107
-
108
- if (useShadow) {
109
- const shadowRoot = host.attachShadow({ mode: "open" });
110
- _root = shadowRoot;
111
- mount = document.createElement("div");
96
+ const ownerDocument = target.ownerDocument;
97
+ let config = options.config;
98
+ let hostLayout = createWidgetHostLayout(target, config);
99
+ let controller: AgentWidgetController;
100
+ let stateUnsubs: Array<() => void> = [];
101
+
102
+ const createMount = (host: HTMLElement, nextConfig?: _AgentWidgetConfig): HTMLElement => {
103
+ const launcherEnabled = nextConfig?.launcher?.enabled ?? true;
104
+ const shouldFillHost = !launcherEnabled || isDockedMountMode(nextConfig);
105
+ const mount = ownerDocument.createElement("div");
112
106
  mount.id = "persona-root";
113
- // When launcher is disabled, ensure mount fills the host
114
- if (!launcherEnabled) {
107
+
108
+ if (shouldFillHost) {
115
109
  mount.style.height = "100%";
116
110
  mount.style.display = "flex";
117
111
  mount.style.flexDirection = "column";
118
112
  mount.style.flex = "1";
119
113
  mount.style.minHeight = "0";
120
114
  }
121
- shadowRoot.appendChild(mount);
122
- mountStyles(shadowRoot);
123
- } else {
124
- _root = host;
125
- mount = document.createElement("div");
126
- mount.id = "persona-root";
127
- // When launcher is disabled, ensure mount fills the host
128
- if (!launcherEnabled) {
129
- mount.style.height = "100%";
130
- mount.style.display = "flex";
131
- mount.style.flexDirection = "column";
132
- mount.style.flex = "1";
133
- mount.style.minHeight = "0";
115
+
116
+ if (useShadow) {
117
+ const shadowRoot = host.attachShadow({ mode: "open" });
118
+ shadowRoot.appendChild(mount);
119
+ mountStyles(shadowRoot, ownerDocument);
120
+ } else {
121
+ host.appendChild(mount);
122
+ mountStyles(host, ownerDocument);
134
123
  }
135
- host.appendChild(mount);
136
- mountStyles(host);
137
- }
138
124
 
139
- // Set instance id for window event scoping (mount.id is always "persona-root" for CSS)
140
- if (target.id) {
141
- mount.setAttribute("data-persona-instance", target.id);
142
- }
125
+ if (target.id) {
126
+ mount.setAttribute("data-persona-instance", target.id);
127
+ }
128
+
129
+ return mount;
130
+ };
143
131
 
144
- let controller = createAgentExperience(mount, options.config, {
145
- debugTools: options.debugTools
146
- });
132
+ const syncHostState = () => {
133
+ hostLayout.syncWidgetState(controller.getState());
134
+ };
135
+
136
+ const bindHostState = () => {
137
+ stateUnsubs.forEach((unsubscribe) => unsubscribe());
138
+ stateUnsubs = [
139
+ controller.on("widget:opened", syncHostState),
140
+ controller.on("widget:closed", syncHostState),
141
+ ];
142
+ syncHostState();
143
+ };
144
+
145
+ const mountController = () => {
146
+ const mount = createMount(hostLayout.host, config);
147
+ controller = createAgentExperience(mount, config, {
148
+ debugTools: options.debugTools
149
+ });
150
+ bindHostState();
151
+ };
152
+
153
+ const destroyCurrentController = () => {
154
+ stateUnsubs.forEach((unsubscribe) => unsubscribe());
155
+ stateUnsubs = [];
156
+ controller.destroy();
157
+ };
158
+
159
+ mountController();
147
160
  options.onReady?.();
148
161
 
149
- const handle: AgentWidgetInitHandle = {
150
- ...controller,
151
- host,
162
+ const rebuildLayout = (nextConfig?: _AgentWidgetConfig) => {
163
+ destroyCurrentController();
164
+ hostLayout.destroy();
165
+ hostLayout = createWidgetHostLayout(target, nextConfig);
166
+ config = nextConfig;
167
+ mountController();
168
+ };
169
+
170
+ const handleBase = {
171
+ update(nextConfig: _AgentWidgetConfig) {
172
+ const mergedConfig = {
173
+ ...config,
174
+ ...nextConfig,
175
+ launcher: {
176
+ ...(config?.launcher ?? {}),
177
+ ...(nextConfig?.launcher ?? {}),
178
+ dock: {
179
+ ...(config?.launcher?.dock ?? {}),
180
+ ...(nextConfig?.launcher?.dock ?? {}),
181
+ },
182
+ },
183
+ } as _AgentWidgetConfig;
184
+ const previousDocked = isDockedMountMode(config);
185
+ const nextDocked = isDockedMountMode(mergedConfig);
186
+
187
+ if (previousDocked !== nextDocked) {
188
+ rebuildLayout(mergedConfig);
189
+ return;
190
+ }
191
+
192
+ config = mergedConfig;
193
+ hostLayout.updateConfig(config);
194
+ controller.update(nextConfig);
195
+ syncHostState();
196
+ },
152
197
  destroy() {
153
- controller.destroy();
154
- host.remove();
198
+ destroyCurrentController();
199
+ hostLayout.destroy();
155
200
  if (options.windowKey && typeof window !== "undefined") {
156
201
  delete (window as any)[options.windowKey];
157
202
  }
158
203
  }
159
204
  };
160
205
 
161
- // Store on window if windowKey is provided
206
+ const handle = new Proxy(handleBase as AgentWidgetInitHandle, {
207
+ get(targetObject, prop, receiver) {
208
+ if (prop === "host") {
209
+ return hostLayout.host;
210
+ }
211
+
212
+ if (prop in targetObject) {
213
+ return Reflect.get(targetObject, prop, receiver);
214
+ }
215
+
216
+ const value = (controller as Record<PropertyKey, unknown>)[prop];
217
+ return typeof value === "function" ? (value as Function).bind(controller) : value;
218
+ }
219
+ }) as AgentWidgetInitHandle;
220
+
162
221
  if (options.windowKey && typeof window !== 'undefined') {
163
222
  (window as any)[options.windowKey] = handle;
164
223
  }
package/src/session.ts CHANGED
@@ -10,7 +10,9 @@ import {
10
10
  InjectMessageOptions,
11
11
  InjectAssistantMessageOptions,
12
12
  InjectUserMessageOptions,
13
- InjectSystemMessageOptions
13
+ InjectSystemMessageOptions,
14
+ PersonaArtifactRecord,
15
+ PersonaArtifactManualUpsert
14
16
  } from "./types";
15
17
  import {
16
18
  generateUserMessageId,
@@ -40,6 +42,10 @@ type SessionCallbacks = {
40
42
  onStreamingChanged: (streaming: boolean) => void;
41
43
  onError?: (error: Error) => void;
42
44
  onVoiceStatusChanged?: (status: VoiceStatus) => void;
45
+ onArtifactsState?: (state: {
46
+ artifacts: PersonaArtifactRecord[];
47
+ selectedId: string | null;
48
+ }) => void;
43
49
  };
44
50
 
45
51
  export class AgentWidgetSession {
@@ -56,6 +62,9 @@ export class AgentWidgetSession {
56
62
  // Agent execution state
57
63
  private agentExecution: AgentExecutionState | null = null;
58
64
 
65
+ private artifacts = new Map<string, PersonaArtifactRecord>();
66
+ private selectedArtifactId: string | null = null;
67
+
59
68
  // Voice support
60
69
  private voiceProvider: VoiceProvider | null = null;
61
70
  private voiceActive = false;
@@ -972,11 +981,128 @@ export class AgentWidgetSession {
972
981
  this.abortController = null;
973
982
  this.messages = [];
974
983
  this.agentExecution = null;
984
+ this.clearArtifactState();
975
985
  this.setStreaming(false);
976
986
  this.setStatus("idle");
977
987
  this.callbacks.onMessagesChanged([...this.messages]);
978
988
  }
979
989
 
990
+ public getArtifacts(): PersonaArtifactRecord[] {
991
+ return [...this.artifacts.values()];
992
+ }
993
+
994
+ public getArtifactById(id: string): PersonaArtifactRecord | undefined {
995
+ return this.artifacts.get(id);
996
+ }
997
+
998
+ public getSelectedArtifactId(): string | null {
999
+ return this.selectedArtifactId;
1000
+ }
1001
+
1002
+ public selectArtifact(id: string | null): void {
1003
+ this.selectedArtifactId = id;
1004
+ this.emitArtifactsState();
1005
+ }
1006
+
1007
+ public clearArtifacts(): void {
1008
+ this.clearArtifactState();
1009
+ }
1010
+
1011
+ public upsertArtifact(manual: PersonaArtifactManualUpsert): PersonaArtifactRecord {
1012
+ const id =
1013
+ manual.id ||
1014
+ `art_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 9)}`;
1015
+ if (manual.artifactType === "markdown") {
1016
+ const rec: PersonaArtifactRecord = {
1017
+ id,
1018
+ artifactType: "markdown",
1019
+ title: manual.title,
1020
+ status: "complete",
1021
+ markdown: manual.content
1022
+ };
1023
+ this.artifacts.set(id, rec);
1024
+ this.selectedArtifactId = id;
1025
+ this.emitArtifactsState();
1026
+ return rec;
1027
+ }
1028
+ const rec: PersonaArtifactRecord = {
1029
+ id,
1030
+ artifactType: "component",
1031
+ title: manual.title,
1032
+ status: "complete",
1033
+ component: manual.component,
1034
+ props: manual.props ?? {}
1035
+ };
1036
+ this.artifacts.set(id, rec);
1037
+ this.selectedArtifactId = id;
1038
+ this.emitArtifactsState();
1039
+ return rec;
1040
+ }
1041
+
1042
+ private clearArtifactState(): void {
1043
+ if (this.artifacts.size === 0 && this.selectedArtifactId === null) return;
1044
+ this.artifacts.clear();
1045
+ this.selectedArtifactId = null;
1046
+ this.emitArtifactsState();
1047
+ }
1048
+
1049
+ private emitArtifactsState(): void {
1050
+ this.callbacks.onArtifactsState?.({
1051
+ artifacts: [...this.artifacts.values()],
1052
+ selectedId: this.selectedArtifactId
1053
+ });
1054
+ }
1055
+
1056
+ private applyArtifactStreamEvent(ev: AgentWidgetEvent): void {
1057
+ switch (ev.type) {
1058
+ case "artifact_start": {
1059
+ if (ev.artifactType === "markdown") {
1060
+ this.artifacts.set(ev.id, {
1061
+ id: ev.id,
1062
+ artifactType: "markdown",
1063
+ title: ev.title,
1064
+ status: "streaming",
1065
+ markdown: ""
1066
+ });
1067
+ } else {
1068
+ this.artifacts.set(ev.id, {
1069
+ id: ev.id,
1070
+ artifactType: "component",
1071
+ title: ev.title,
1072
+ status: "streaming",
1073
+ component: ev.component ?? "",
1074
+ props: {}
1075
+ });
1076
+ }
1077
+ this.selectedArtifactId = ev.id;
1078
+ break;
1079
+ }
1080
+ case "artifact_delta": {
1081
+ const row = this.artifacts.get(ev.id);
1082
+ if (row?.artifactType === "markdown") {
1083
+ row.markdown = (row.markdown ?? "") + ev.artDelta;
1084
+ }
1085
+ break;
1086
+ }
1087
+ case "artifact_update": {
1088
+ const row = this.artifacts.get(ev.id);
1089
+ if (row?.artifactType === "component") {
1090
+ row.props = { ...row.props, ...ev.props };
1091
+ if (ev.component) row.component = ev.component;
1092
+ }
1093
+ break;
1094
+ }
1095
+ case "artifact_complete": {
1096
+ const row = this.artifacts.get(ev.id);
1097
+ if (row) row.status = "complete";
1098
+ break;
1099
+ }
1100
+ default:
1101
+ return;
1102
+ }
1103
+ this.emitArtifactsState();
1104
+ }
1105
+
980
1106
  public hydrateMessages(messages: AgentWidgetMessage[]) {
981
1107
  this.abortController?.abort();
982
1108
  this.abortController = null;
@@ -1005,7 +1131,7 @@ export class AgentWidgetSession {
1005
1131
  agentName: event.message.agentMetadata.agentName ?? '',
1006
1132
  status: 'running',
1007
1133
  currentIteration: event.message.agentMetadata.iteration ?? 0,
1008
- maxIterations: 0
1134
+ maxTurns: 0
1009
1135
  };
1010
1136
  } else if (event.message.agentMetadata.iteration !== undefined) {
1011
1137
  this.agentExecution.currentIteration = event.message.agentMetadata.iteration;
@@ -1031,6 +1157,13 @@ export class AgentWidgetSession {
1031
1157
  this.agentExecution.status = 'error';
1032
1158
  }
1033
1159
  this.callbacks.onError?.(event.error);
1160
+ } else if (
1161
+ event.type === "artifact_start" ||
1162
+ event.type === "artifact_delta" ||
1163
+ event.type === "artifact_update" ||
1164
+ event.type === "artifact_complete"
1165
+ ) {
1166
+ this.applyArtifactStreamEvent(event);
1034
1167
  }
1035
1168
  };
1036
1169
 
@@ -14,7 +14,7 @@
14
14
  }
15
15
 
16
16
  @layer components {
17
- .tvw-widget-shadow {
17
+ .persona-widget-shadow {
18
18
  box-shadow: 0 25px 50px -12px rgba(15, 23, 42, 0.35);
19
19
  }
20
20
  }