@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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/DevToolsFrame.d.ts +8 -0
- package/dist/DevToolsFrame.d.ts.map +1 -0
- package/dist/DevToolsFrame.js +49 -0
- package/dist/DevToolsFrame.js.map +1 -0
- package/dist/DevToolsHost.d.ts +35 -0
- package/dist/DevToolsHost.d.ts.map +1 -0
- package/dist/DevToolsHost.js +104 -0
- package/dist/DevToolsHost.js.map +1 -0
- package/dist/DevToolsModal.d.ts +2 -0
- package/dist/DevToolsModal.d.ts.map +1 -0
- package/dist/DevToolsModal.js +170 -0
- package/dist/DevToolsModal.js.map +1 -0
- package/dist/ExtensionHost.d.ts +7 -0
- package/dist/ExtensionHost.d.ts.map +1 -0
- package/dist/ExtensionHost.js +50 -0
- package/dist/ExtensionHost.js.map +1 -0
- package/dist/FrameClient.d.ts +35 -0
- package/dist/FrameClient.d.ts.map +1 -0
- package/dist/FrameClient.js +63 -0
- package/dist/FrameClient.js.map +1 -0
- package/dist/FrameHost.d.ts +8 -0
- package/dist/FrameHost.d.ts.map +1 -0
- package/dist/FrameHost.js +28 -0
- package/dist/FrameHost.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +8 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/styles/DevToolsModal.styles.d.ts +14 -0
- package/dist/styles/DevToolsModal.styles.d.ts.map +1 -0
- package/dist/styles/DevToolsModal.styles.js +121 -0
- package/dist/styles/DevToolsModal.styles.js.map +1 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/serialization.d.ts +5 -0
- package/dist/utils/serialization.d.ts.map +1 -0
- package/dist/utils/serialization.js +77 -0
- package/dist/utils/serialization.js.map +1 -0
- package/dist/utils/toolNormalization.d.ts +9 -0
- package/dist/utils/toolNormalization.d.ts.map +1 -0
- package/dist/utils/toolNormalization.js +58 -0
- package/dist/utils/toolNormalization.js.map +1 -0
- package/package.json +76 -0
- package/src/DevToolsFrame.tsx +55 -0
- package/src/DevToolsHost.ts +150 -0
- package/src/DevToolsModal.tsx +178 -0
- package/src/ExtensionHost.ts +57 -0
- package/src/FrameClient.ts +98 -0
- package/src/FrameHost.ts +31 -0
- package/src/constants.ts +2 -0
- package/src/index.ts +17 -0
- package/src/styles/DevToolsModal.styles.ts +137 -0
- package/src/types.ts +13 -0
- package/src/utils/serialization.ts +97 -0
- 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
|
+
}
|
package/src/FrameHost.ts
ADDED
|
@@ -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
|
+
}
|
package/src/constants.ts
ADDED
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";
|