@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.
- package/README.md +140 -8
- package/dist/index.cjs +90 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1055 -24
- package/dist/index.d.ts +1055 -24
- package/dist/index.global.js +111 -60
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +90 -39
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/widget.css +836 -513
- package/package.json +1 -1
- package/src/artifacts-session.test.ts +80 -0
- package/src/client.test.ts +20 -21
- package/src/client.ts +153 -4
- package/src/components/approval-bubble.ts +45 -42
- package/src/components/artifact-card.ts +91 -0
- package/src/components/artifact-pane.ts +501 -0
- package/src/components/composer-builder.ts +32 -27
- package/src/components/event-stream-view.ts +40 -40
- package/src/components/feedback.ts +36 -36
- package/src/components/forms.ts +11 -11
- package/src/components/header-builder.test.ts +32 -0
- package/src/components/header-builder.ts +55 -36
- package/src/components/header-layouts.ts +58 -125
- package/src/components/launcher.ts +36 -21
- package/src/components/message-bubble.ts +92 -65
- package/src/components/messages.ts +2 -2
- package/src/components/panel.ts +42 -11
- package/src/components/reasoning-bubble.ts +23 -23
- package/src/components/registry.ts +4 -0
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +32 -32
- package/src/defaults.ts +30 -4
- package/src/index.ts +80 -2
- package/src/install.ts +22 -0
- package/src/plugins/types.ts +23 -0
- package/src/postprocessors.ts +2 -2
- package/src/runtime/host-layout.ts +174 -0
- package/src/runtime/init.test.ts +236 -0
- package/src/runtime/init.ts +114 -55
- package/src/session.ts +135 -2
- package/src/styles/tailwind.css +1 -1
- package/src/styles/widget.css +836 -513
- package/src/types/theme.ts +354 -0
- package/src/types.ts +314 -15
- package/src/ui.docked.test.ts +104 -0
- package/src/ui.ts +940 -227
- package/src/utils/artifact-gate.test.ts +255 -0
- package/src/utils/artifact-gate.ts +142 -0
- package/src/utils/artifact-resize.test.ts +64 -0
- package/src/utils/artifact-resize.ts +67 -0
- package/src/utils/attachment-manager.ts +10 -10
- package/src/utils/code-generators.test.ts +52 -0
- package/src/utils/code-generators.ts +40 -36
- package/src/utils/dock.ts +17 -0
- package/src/utils/dom-context.test.ts +504 -0
- package/src/utils/dom-context.ts +896 -0
- package/src/utils/dom.ts +12 -1
- package/src/utils/message-fingerprint.test.ts +187 -0
- package/src/utils/message-fingerprint.ts +105 -0
- package/src/utils/migration.ts +179 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/plugins.ts +175 -0
- package/src/utils/positioning.ts +4 -4
- package/src/utils/theme.test.ts +125 -0
- package/src/utils/theme.ts +216 -60
- 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
|
+
});
|
package/src/runtime/init.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
77
|
+
const link = ownerDocument.createElement("link");
|
|
76
78
|
link.rel = "stylesheet";
|
|
77
79
|
link.href = href;
|
|
78
80
|
link.setAttribute("data-persona", "true");
|
|
79
|
-
|
|
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
|
-
|
|
106
|
-
let
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
if (
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
125
|
+
if (target.id) {
|
|
126
|
+
mount.setAttribute("data-persona-instance", target.id);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return mount;
|
|
130
|
+
};
|
|
143
131
|
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|