@assistant-ui/react-devtools 0.1.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/DevToolsFrame.d.ts +8 -0
  4. package/dist/DevToolsFrame.d.ts.map +1 -0
  5. package/dist/DevToolsFrame.js +49 -0
  6. package/dist/DevToolsFrame.js.map +1 -0
  7. package/dist/DevToolsHost.d.ts +35 -0
  8. package/dist/DevToolsHost.d.ts.map +1 -0
  9. package/dist/DevToolsHost.js +104 -0
  10. package/dist/DevToolsHost.js.map +1 -0
  11. package/dist/DevToolsModal.d.ts +2 -0
  12. package/dist/DevToolsModal.d.ts.map +1 -0
  13. package/dist/DevToolsModal.js +170 -0
  14. package/dist/DevToolsModal.js.map +1 -0
  15. package/dist/ExtensionHost.d.ts +7 -0
  16. package/dist/ExtensionHost.d.ts.map +1 -0
  17. package/dist/ExtensionHost.js +50 -0
  18. package/dist/ExtensionHost.js.map +1 -0
  19. package/dist/FrameClient.d.ts +35 -0
  20. package/dist/FrameClient.d.ts.map +1 -0
  21. package/dist/FrameClient.js +63 -0
  22. package/dist/FrameClient.js.map +1 -0
  23. package/dist/FrameHost.d.ts +8 -0
  24. package/dist/FrameHost.d.ts.map +1 -0
  25. package/dist/FrameHost.js +28 -0
  26. package/dist/FrameHost.js.map +1 -0
  27. package/dist/constants.d.ts +3 -0
  28. package/dist/constants.d.ts.map +1 -0
  29. package/dist/constants.js +8 -0
  30. package/dist/constants.js.map +1 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +27 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/styles/DevToolsModal.styles.d.ts +14 -0
  36. package/dist/styles/DevToolsModal.styles.d.ts.map +1 -0
  37. package/dist/styles/DevToolsModal.styles.js +121 -0
  38. package/dist/styles/DevToolsModal.styles.js.map +1 -0
  39. package/dist/types.d.ts +10 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +1 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/utils/serialization.d.ts +5 -0
  44. package/dist/utils/serialization.d.ts.map +1 -0
  45. package/dist/utils/serialization.js +77 -0
  46. package/dist/utils/serialization.js.map +1 -0
  47. package/dist/utils/toolNormalization.d.ts +9 -0
  48. package/dist/utils/toolNormalization.d.ts.map +1 -0
  49. package/dist/utils/toolNormalization.js +58 -0
  50. package/dist/utils/toolNormalization.js.map +1 -0
  51. package/package.json +76 -0
  52. package/src/DevToolsFrame.tsx +55 -0
  53. package/src/DevToolsHost.ts +150 -0
  54. package/src/DevToolsModal.tsx +178 -0
  55. package/src/ExtensionHost.ts +57 -0
  56. package/src/FrameClient.ts +98 -0
  57. package/src/FrameHost.ts +31 -0
  58. package/src/constants.ts +2 -0
  59. package/src/index.ts +17 -0
  60. package/src/styles/DevToolsModal.styles.ts +137 -0
  61. package/src/types.ts +13 -0
  62. package/src/utils/serialization.ts +97 -0
  63. package/src/utils/toolNormalization.ts +83 -0
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useRef } from "react";
4
+ import { FrameHost } from "./FrameHost";
5
+ import { DEFAULT_FRAME_URL } from "./constants";
6
+
7
+ export interface DevToolsFrameProps {
8
+ frameUrl?: string;
9
+ className?: string;
10
+ style?: React.CSSProperties;
11
+ title?: string;
12
+ }
13
+
14
+ export const DevToolsFrame: React.FC<DevToolsFrameProps> = ({
15
+ frameUrl = DEFAULT_FRAME_URL,
16
+ className,
17
+ style,
18
+ title = "assistant-ui DevTools",
19
+ }) => {
20
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
21
+ const frameHostRef = useRef<FrameHost | null>(null);
22
+
23
+ const resolvedFrameUrl = useMemo(() => frameUrl, [frameUrl]);
24
+
25
+ const handleFrameLoad = useCallback(() => {
26
+ if (frameHostRef.current) {
27
+ frameHostRef.current.destroy();
28
+ frameHostRef.current = null;
29
+ }
30
+
31
+ if (iframeRef.current) {
32
+ frameHostRef.current = new FrameHost(iframeRef.current);
33
+ }
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ return () => {
38
+ if (frameHostRef.current) {
39
+ frameHostRef.current.destroy();
40
+ frameHostRef.current = null;
41
+ }
42
+ };
43
+ }, []);
44
+
45
+ return (
46
+ <iframe
47
+ ref={iframeRef}
48
+ src={resolvedFrameUrl}
49
+ onLoad={handleFrameLoad}
50
+ className={className}
51
+ style={style}
52
+ title={title}
53
+ />
54
+ );
55
+ };
@@ -0,0 +1,150 @@
1
+ import { DevToolsHooks } from "@assistant-ui/react";
2
+ import {
3
+ sanitizeForMessage,
4
+ serializeModelContext,
5
+ } from "./utils/serialization";
6
+
7
+ interface FrameToHostMessage {
8
+ type: "subscription" | "clearEvents";
9
+ data: {
10
+ apiList?: boolean;
11
+ apis?: number[];
12
+ apiId?: number;
13
+ };
14
+ }
15
+
16
+ interface HostToFrameMessage {
17
+ type: "update";
18
+ data: {
19
+ apiList?: Array<{ apiId: number }>;
20
+ apis?: Array<{
21
+ apiId: number;
22
+ state: any;
23
+ events: any[];
24
+ modelContext?: any;
25
+ }>;
26
+ };
27
+ }
28
+
29
+ export class DevToolsHost {
30
+ private subscription: {
31
+ apiList: boolean;
32
+ apis: Set<number>;
33
+ } = {
34
+ apiList: false,
35
+ apis: new Set(),
36
+ };
37
+ private unsubscribe?: () => void;
38
+ private onSendMessage: (message: HostToFrameMessage) => void;
39
+
40
+ constructor(onSendMessage: (message: HostToFrameMessage) => void) {
41
+ this.onSendMessage = onSendMessage;
42
+ this.subscribeToDevTools();
43
+ }
44
+
45
+ onReceiveMessage(message: FrameToHostMessage) {
46
+ switch (message.type) {
47
+ case "subscription":
48
+ this.handleSubscription(message.data);
49
+ break;
50
+ case "clearEvents":
51
+ if (typeof message.data.apiId === "number") {
52
+ DevToolsHooks.clearEventLogs(message.data.apiId);
53
+ // The subscription will automatically trigger an update
54
+ }
55
+ break;
56
+ }
57
+ }
58
+
59
+ private handleSubscription(data: FrameToHostMessage["data"]) {
60
+ const prevApiList = this.subscription.apiList;
61
+ const prevApis = new Set(this.subscription.apis);
62
+
63
+ this.subscription.apiList = data.apiList || false;
64
+ this.subscription.apis = new Set(data.apis);
65
+
66
+ // Only send update if subscription actually changed
67
+ const apisChanged =
68
+ prevApis.size !== this.subscription.apis.size ||
69
+ [...this.subscription.apis].some((id) => !prevApis.has(id));
70
+
71
+ if (prevApiList !== this.subscription.apiList || apisChanged) {
72
+ this.sendUpdate();
73
+ }
74
+ }
75
+
76
+ private subscribeToDevTools() {
77
+ this.unsubscribe = DevToolsHooks.subscribe(() => {
78
+ this.sendUpdate();
79
+ });
80
+ }
81
+
82
+ private sendUpdate() {
83
+ const update: HostToFrameMessage = {
84
+ type: "update",
85
+ data: {},
86
+ };
87
+
88
+ const allApis = DevToolsHooks.getApis();
89
+ for (const subscriptionApiId of this.subscription.apis) {
90
+ if (!allApis.has(subscriptionApiId)) {
91
+ this.subscription.apis.delete(subscriptionApiId);
92
+ }
93
+ }
94
+
95
+ if (this.subscription.apiList) {
96
+ update.data.apiList = [...allApis.keys()].map((apiId) => ({ apiId }));
97
+
98
+ if (this.subscription.apis.size === 0 && allApis.size > 0) {
99
+ this.subscription.apis = new Set([allApis.keys().next().value!]);
100
+ }
101
+ }
102
+
103
+ if (this.subscription.apis.size > 0) {
104
+ update.data.apis = [];
105
+
106
+ for (const apiId of this.subscription.apis) {
107
+ const apiEntry = allApis.get(apiId);
108
+ if (apiEntry) {
109
+ // Collect state from api scopes (only root source)
110
+ const state: Record<string, unknown> = {};
111
+ if (apiEntry.api) {
112
+ for (const [name, scope] of Object.entries(apiEntry.api)) {
113
+ if (typeof scope === "function" && "source" in scope) {
114
+ // Only forward scopes with source === "root"
115
+ if (scope.source === "root") {
116
+ const scopeValue = scope();
117
+ state[name] = scopeValue?.getState?.() ?? scopeValue;
118
+ }
119
+ }
120
+ }
121
+ }
122
+
123
+ // Extract model context from thread runtime
124
+ const modelContext = serializeModelContext(
125
+ apiEntry.api?.thread?.().getModelContext(),
126
+ );
127
+
128
+ update.data.apis.push({
129
+ apiId,
130
+ state: sanitizeForMessage(state),
131
+ events: sanitizeForMessage(apiEntry.logs) as unknown[],
132
+ modelContext: modelContext,
133
+ });
134
+ }
135
+ }
136
+ }
137
+
138
+ if (Object.keys(update.data).length === 0) {
139
+ return;
140
+ }
141
+
142
+ this.onSendMessage(update);
143
+ }
144
+
145
+ destroy() {
146
+ if (this.unsubscribe) {
147
+ this.unsubscribe();
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,178 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { DevToolsFrame } from "./DevToolsFrame";
5
+ import { getStyles, ANIMATION_STYLES } from "./styles/DevToolsModal.styles";
6
+
7
+ const isDarkMode = (): boolean => {
8
+ if (typeof document === "undefined") return false;
9
+ return (
10
+ document.documentElement.classList.contains("dark") ||
11
+ document.body.classList.contains("dark")
12
+ );
13
+ };
14
+
15
+ const DevToolsModalImpl = () => {
16
+ const [isOpen, setIsOpen] = useState(false);
17
+ const [darkMode, setDarkMode] = useState(false);
18
+ const [buttonHover, setButtonHover] = useState(false);
19
+ const [closeHover, setCloseHover] = useState(false);
20
+
21
+ const styles = useMemo(() => getStyles(darkMode), [darkMode]);
22
+
23
+ useEffect(() => {
24
+ if (typeof document === "undefined") return;
25
+
26
+ const styleId = "devtools-modal-animations";
27
+ if (!document.getElementById(styleId)) {
28
+ const style = document.createElement("style");
29
+ style.id = styleId;
30
+ style.textContent = ANIMATION_STYLES;
31
+ document.head.appendChild(style);
32
+ }
33
+
34
+ return () => {
35
+ const style = document.getElementById(styleId);
36
+ if (style && !document.querySelector("[data-devtools-modal]")) {
37
+ style.remove();
38
+ }
39
+ };
40
+ }, []);
41
+
42
+ useEffect(() => {
43
+ if (typeof MutationObserver === "undefined") return;
44
+
45
+ const checkDarkMode = () => setDarkMode(isDarkMode());
46
+ const observer = new MutationObserver(checkDarkMode);
47
+
48
+ observer.observe(document.documentElement, {
49
+ attributes: true,
50
+ attributeFilter: ["class"],
51
+ });
52
+ if (document.body !== document.documentElement) {
53
+ observer.observe(document.body, {
54
+ attributes: true,
55
+ attributeFilter: ["class"],
56
+ });
57
+ }
58
+
59
+ return () => observer.disconnect();
60
+ }, []);
61
+
62
+ useEffect(() => {
63
+ if (!isOpen) return;
64
+
65
+ const handleEscape = (event: KeyboardEvent) => {
66
+ if (event.key === "Escape") {
67
+ setIsOpen(false);
68
+ }
69
+ };
70
+
71
+ document.addEventListener("keydown", handleEscape);
72
+ return () => document.removeEventListener("keydown", handleEscape);
73
+ }, [isOpen]);
74
+
75
+ return (
76
+ <>
77
+ <div style={styles.floatingContainer}>
78
+ <button
79
+ onClick={() => setIsOpen(true)}
80
+ onMouseEnter={() => setButtonHover(true)}
81
+ onMouseLeave={() => setButtonHover(false)}
82
+ style={{
83
+ ...styles.floatingButton,
84
+ ...(buttonHover ? styles.floatingButtonHover : {}),
85
+ }}
86
+ aria-label="Open assistant-ui DevTools"
87
+ title="Open assistant-ui DevTools"
88
+ >
89
+ <svg
90
+ width="20"
91
+ height="20"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ xmlns="http://www.w3.org/2000/svg"
95
+ style={{ width: "20px", height: "20px" }}
96
+ >
97
+ <path
98
+ d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
99
+ stroke="currentColor"
100
+ strokeWidth="1.8"
101
+ strokeLinecap="round"
102
+ strokeLinejoin="round"
103
+ fill="none"
104
+ />
105
+ </svg>
106
+ </button>
107
+ </div>
108
+
109
+ {isOpen && (
110
+ <>
111
+ <div style={styles.backdrop} onClick={() => setIsOpen(false)} />
112
+
113
+ <div style={styles.modal} data-devtools-modal>
114
+ <button
115
+ onClick={() => setIsOpen(false)}
116
+ onMouseEnter={() => setCloseHover(true)}
117
+ onMouseLeave={() => setCloseHover(false)}
118
+ style={{
119
+ ...styles.dismissButton,
120
+ ...(closeHover ? styles.dismissButtonHover : {}),
121
+ }}
122
+ aria-label="Close DevTools"
123
+ >
124
+ <svg
125
+ width="14"
126
+ height="14"
127
+ viewBox="0 0 24 24"
128
+ fill="none"
129
+ xmlns="http://www.w3.org/2000/svg"
130
+ >
131
+ <path
132
+ d="M18 6L6 18"
133
+ stroke="currentColor"
134
+ strokeWidth="1.6"
135
+ strokeLinecap="round"
136
+ strokeLinejoin="round"
137
+ />
138
+ <path
139
+ d="M6 6L18 18"
140
+ stroke="currentColor"
141
+ strokeWidth="1.6"
142
+ strokeLinecap="round"
143
+ strokeLinejoin="round"
144
+ />
145
+ </svg>
146
+ </button>
147
+
148
+ <div style={styles.modalContent}>
149
+ <DevToolsFrame
150
+ style={{
151
+ width: "100%",
152
+ height: "100%",
153
+ border: "none",
154
+ borderRadius: "12px",
155
+ backgroundColor: "transparent",
156
+ }}
157
+ />
158
+ </div>
159
+ </div>
160
+ </>
161
+ )}
162
+ </>
163
+ );
164
+ };
165
+
166
+ // Export a component that only renders in development
167
+ export const DevToolsModal = () => {
168
+ // Check if we're in production - most bundlers will replace process.env.NODE_ENV
169
+ // This allows the entire component to be eliminated via dead code elimination
170
+ if (
171
+ typeof process !== "undefined" &&
172
+ process.env?.["NODE_ENV"] === "production"
173
+ ) {
174
+ return null;
175
+ }
176
+
177
+ return <DevToolsModalImpl />;
178
+ };
@@ -0,0 +1,57 @@
1
+ import { DevToolsHost } from "./DevToolsHost";
2
+
3
+ export class ExtensionHost {
4
+ private devToolsHost: DevToolsHost;
5
+ private messageListener: (event: MessageEvent) => void;
6
+
7
+ constructor() {
8
+ // Create DevToolsHost with callback to send messages through extension
9
+ this.devToolsHost = new DevToolsHost((message) => {
10
+ console.log("[ExtensionHost] Sending message to iframe:", message);
11
+ window.postMessage(
12
+ {
13
+ source: "assistant-ui-devtools-page",
14
+ payload: message,
15
+ },
16
+ "*",
17
+ );
18
+ });
19
+
20
+ // Setup listener to forward messages from extension to DevToolsHost
21
+ this.messageListener = (event: MessageEvent) => {
22
+ if (event.source !== window) return;
23
+
24
+ // Log ALL messages to see what's coming through
25
+ if (event.data.source === "assistant-ui-devtools-iframe") {
26
+ console.log("[ExtensionHost] Received message from iframe:", {
27
+ source: event.data.source,
28
+ payload: event.data.payload,
29
+ fullData: event.data,
30
+ });
31
+ this.devToolsHost.onReceiveMessage(event.data.payload);
32
+ }
33
+ };
34
+
35
+ window.addEventListener("message", this.messageListener);
36
+
37
+ // Announce that a new host has connected
38
+ // This tells the iframe to re-send its subscription
39
+ setTimeout(() => {
40
+ console.log("[ExtensionHost] Announcing connection to iframe");
41
+ window.postMessage(
42
+ {
43
+ source: "assistant-ui-devtools-page",
44
+ payload: {
45
+ type: "host-connected",
46
+ },
47
+ },
48
+ "*",
49
+ );
50
+ }, 100);
51
+ }
52
+
53
+ destroy() {
54
+ window.removeEventListener("message", this.messageListener);
55
+ this.devToolsHost.destroy();
56
+ }
57
+ }
@@ -0,0 +1,98 @@
1
+ interface ApiData {
2
+ apiId: number;
3
+ state: any;
4
+ events: any[];
5
+ context?: any;
6
+ }
7
+
8
+ interface UpdateMessage {
9
+ type: "update";
10
+ data: {
11
+ apiList?: Array<{ apiId: number }>;
12
+ apis?: ApiData[];
13
+ };
14
+ }
15
+
16
+ interface HostConnectedMessage {
17
+ type: "host-connected";
18
+ }
19
+
20
+ type UpdateListener = (data: {
21
+ apiList?: Array<{ apiId: number }>;
22
+ apis?: ApiData[];
23
+ }) => void;
24
+
25
+ export class FrameClient {
26
+ private listeners = new Set<UpdateListener>();
27
+ private connectionListeners = new Set<() => void>();
28
+ private lastUpdate: {
29
+ apiList?: Array<{ apiId: number }>;
30
+ apis?: ApiData[];
31
+ } = {};
32
+
33
+ constructor() {
34
+ this.setupMessageListener();
35
+ }
36
+
37
+ private setupMessageListener() {
38
+ window.addEventListener("message", (event) => {
39
+ const message = event.data as UpdateMessage | HostConnectedMessage;
40
+
41
+ if (message.type === "update") {
42
+ this.lastUpdate = message.data;
43
+ this.notifyListeners(message.data);
44
+ } else if (message.type === "host-connected") {
45
+ // Host has reconnected (page refresh), notify listeners to re-subscribe
46
+ this.connectionListeners.forEach((listener) => listener());
47
+ }
48
+ });
49
+ }
50
+
51
+ onHostConnected(listener: () => void): () => void {
52
+ this.connectionListeners.add(listener);
53
+ return () => {
54
+ this.connectionListeners.delete(listener);
55
+ };
56
+ }
57
+
58
+ subscribe(listener: UpdateListener): () => void {
59
+ this.listeners.add(listener);
60
+
61
+ // Send the last update to the new listener
62
+ if (this.lastUpdate.apiList || this.lastUpdate.apis) {
63
+ listener(this.lastUpdate);
64
+ }
65
+
66
+ return () => {
67
+ this.listeners.delete(listener);
68
+ };
69
+ }
70
+
71
+ setSubscription(options: { apiList?: boolean; apis?: number[] }) {
72
+ window.parent.postMessage(
73
+ {
74
+ type: "subscription",
75
+ data: options,
76
+ },
77
+ "*",
78
+ );
79
+ }
80
+
81
+ clearEvents(apiId: number) {
82
+ window.parent.postMessage(
83
+ {
84
+ type: "clearEvents",
85
+ data: { apiId },
86
+ },
87
+ "*",
88
+ );
89
+ }
90
+
91
+ private notifyListeners(data: UpdateMessage["data"]) {
92
+ this.listeners.forEach((listener) => listener(data));
93
+ }
94
+
95
+ getLastUpdate() {
96
+ return this.lastUpdate;
97
+ }
98
+ }
@@ -0,0 +1,31 @@
1
+ import { DevToolsHost } from "./DevToolsHost";
2
+
3
+ export class FrameHost {
4
+ private frame: HTMLIFrameElement;
5
+ private devToolsHost: DevToolsHost;
6
+ private messageListener: (event: MessageEvent) => void;
7
+
8
+ constructor(frame: HTMLIFrameElement) {
9
+ this.frame = frame;
10
+
11
+ // Create DevToolsHost with callback to send messages to iframe
12
+ this.devToolsHost = new DevToolsHost((message) => {
13
+ if (this.frame.contentWindow) {
14
+ this.frame.contentWindow.postMessage(message, "*");
15
+ }
16
+ });
17
+
18
+ // Setup listener to forward messages from iframe to DevToolsHost
19
+ this.messageListener = (event: MessageEvent) => {
20
+ if (event.source !== this.frame.contentWindow) return;
21
+ this.devToolsHost.onReceiveMessage(event.data);
22
+ };
23
+
24
+ window.addEventListener("message", this.messageListener);
25
+ }
26
+
27
+ destroy() {
28
+ window.removeEventListener("message", this.messageListener);
29
+ this.devToolsHost.destroy();
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ export const DEFAULT_FRAME_URL = "https://devtools-frame.assistant-ui.com";
2
+ export const LOCAL_FRAME_URL = "http://localhost:3010";
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { DevToolsModal } from "./DevToolsModal";
2
+ export { DevToolsFrame } from "./DevToolsFrame";
3
+ export { FrameHost } from "./FrameHost";
4
+ export { DevToolsHost } from "./DevToolsHost";
5
+ export { ExtensionHost } from "./ExtensionHost";
6
+ export { FrameClient } from "./FrameClient";
7
+ export {
8
+ normalizeToolList,
9
+ type NormalizedTool,
10
+ } from "./utils/toolNormalization";
11
+ export {
12
+ sanitizeForMessage,
13
+ serializeModelContext,
14
+ } from "./utils/serialization";
15
+ // Export types
16
+ export type { SerializedModelContext, TabType, ViewMode } from "./types";
17
+ export * from "./constants";