@nimrobo/wand-web 0.1.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/LICENSE +201 -0
- package/README.md +34 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/next.d.ts +22 -0
- package/dist/adapters/next.js +276 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/adapters/types.js +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +8 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +63 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +1559 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/runtime/capture.d.ts +20 -0
- package/dist/runtime/capture.js +16 -0
- package/dist/runtime/dom.d.ts +7 -0
- package/dist/runtime/dom.js +78 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +480 -0
- package/dist/runtime/panel.d.ts +2 -0
- package/dist/runtime/panel.js +26 -0
- package/dist/runtime/styles.d.ts +1 -0
- package/dist/runtime/styles.js +191 -0
- package/dist/server/agent-config.d.ts +7 -0
- package/dist/server/agent-config.js +63 -0
- package/dist/server/agent.d.ts +12 -0
- package/dist/server/agent.js +35 -0
- package/dist/server/http.d.ts +9 -0
- package/dist/server/http.js +166 -0
- package/dist/server/runs.d.ts +21 -0
- package/dist/server/runs.js +119 -0
- package/dist/server/storage.d.ts +13 -0
- package/dist/server/storage.js +73 -0
- package/dist/server/types.d.ts +50 -0
- package/dist/server/types.js +1 -0
- package/package.json +63 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ViewportCaptureMetrics = {
|
|
2
|
+
viewportWidth: number;
|
|
3
|
+
viewportHeight: number;
|
|
4
|
+
scrollX: number;
|
|
5
|
+
scrollY: number;
|
|
6
|
+
documentWidth: number;
|
|
7
|
+
documentHeight: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function viewportCaptureOptions(metrics: ViewportCaptureMetrics): {
|
|
10
|
+
cacheBust: boolean;
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
canvasWidth: number;
|
|
14
|
+
canvasHeight: number;
|
|
15
|
+
style: {
|
|
16
|
+
width: string;
|
|
17
|
+
height: string;
|
|
18
|
+
transform: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function viewportCaptureOptions(metrics) {
|
|
2
|
+
const documentWidth = Math.max(metrics.documentWidth, metrics.viewportWidth);
|
|
3
|
+
const documentHeight = Math.max(metrics.documentHeight, metrics.viewportHeight);
|
|
4
|
+
return {
|
|
5
|
+
cacheBust: true,
|
|
6
|
+
width: metrics.viewportWidth,
|
|
7
|
+
height: metrics.viewportHeight,
|
|
8
|
+
canvasWidth: metrics.viewportWidth,
|
|
9
|
+
canvasHeight: metrics.viewportHeight,
|
|
10
|
+
style: {
|
|
11
|
+
width: `${documentWidth}px`,
|
|
12
|
+
height: `${documentHeight}px`,
|
|
13
|
+
transform: `translate(${-metrics.scrollX}px, ${-metrics.scrollY}px)`,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReactOwnerHint } from "../server/types.js";
|
|
2
|
+
export declare function pickElementAtPoint(x: number, y: number): HTMLElement | null;
|
|
3
|
+
export declare function selectorFor(element: Element): string;
|
|
4
|
+
export declare function domPathFor(element: Element): string[];
|
|
5
|
+
export declare function attributesFor(element: Element): Record<string, string>;
|
|
6
|
+
export declare function htmlFor(element: Element): string;
|
|
7
|
+
export declare function reactOwnerHintsFor(element: Element): ReactOwnerHint[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const overlayAttribute = "data-wand-ui";
|
|
2
|
+
export function pickElementAtPoint(x, y) {
|
|
3
|
+
for (const element of document.elementsFromPoint(x, y)) {
|
|
4
|
+
if (!(element instanceof HTMLElement))
|
|
5
|
+
continue;
|
|
6
|
+
if (element.closest(`[${overlayAttribute}]`))
|
|
7
|
+
continue;
|
|
8
|
+
return element;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
export function selectorFor(element) {
|
|
13
|
+
if (element.id)
|
|
14
|
+
return `#${cssEscape(element.id)}`;
|
|
15
|
+
const segments = [];
|
|
16
|
+
for (let current = element; current && current !== document.body; current = current.parentElement) {
|
|
17
|
+
let segment = current.tagName.toLowerCase();
|
|
18
|
+
const siblings = current.parentElement
|
|
19
|
+
? Array.from(current.parentElement.children).filter((child) => child.tagName === current.tagName)
|
|
20
|
+
: [];
|
|
21
|
+
if (siblings.length > 1) {
|
|
22
|
+
segment += `:nth-of-type(${siblings.indexOf(current) + 1})`;
|
|
23
|
+
}
|
|
24
|
+
segments.unshift(segment);
|
|
25
|
+
}
|
|
26
|
+
return segments.join(" > ");
|
|
27
|
+
}
|
|
28
|
+
export function domPathFor(element) {
|
|
29
|
+
const path = [];
|
|
30
|
+
for (let current = element; current; current = current.parentElement) {
|
|
31
|
+
path.unshift(selectorSegment(current));
|
|
32
|
+
if (current === document.documentElement)
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
export function attributesFor(element) {
|
|
38
|
+
return Object.fromEntries(Array.from(element.attributes).map((attribute) => [attribute.name, attribute.value]));
|
|
39
|
+
}
|
|
40
|
+
export function htmlFor(element) {
|
|
41
|
+
const clone = element.cloneNode(true);
|
|
42
|
+
clone.querySelectorAll("[data-wand-root]").forEach((node) => node.remove());
|
|
43
|
+
return clone.outerHTML;
|
|
44
|
+
}
|
|
45
|
+
export function reactOwnerHintsFor(element) {
|
|
46
|
+
const fiberKey = Object.keys(element).find((key) => key.startsWith("__reactFiber$"));
|
|
47
|
+
const fiber = fiberKey ? element[fiberKey] : null;
|
|
48
|
+
const hints = [];
|
|
49
|
+
let cursor = asFiber(fiber);
|
|
50
|
+
while (cursor && hints.length < 6) {
|
|
51
|
+
const name = typeof cursor.type === "string"
|
|
52
|
+
? cursor.type
|
|
53
|
+
: cursor.type?.displayName ?? cursor.type?.name ?? null;
|
|
54
|
+
const source = cursor._debugSource
|
|
55
|
+
? `${cursor._debugSource.fileName}:${cursor._debugSource.lineNumber}`
|
|
56
|
+
: null;
|
|
57
|
+
if (name || source) {
|
|
58
|
+
hints.push({
|
|
59
|
+
name,
|
|
60
|
+
key: cursor.key ?? null,
|
|
61
|
+
source,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
cursor = cursor.return ?? null;
|
|
65
|
+
}
|
|
66
|
+
return hints;
|
|
67
|
+
}
|
|
68
|
+
function selectorSegment(element) {
|
|
69
|
+
if (element.id)
|
|
70
|
+
return `#${cssEscape(element.id)}`;
|
|
71
|
+
return element.tagName.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
function cssEscape(value) {
|
|
74
|
+
return globalThis.CSS?.escape ? globalThis.CSS.escape(value) : value.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
75
|
+
}
|
|
76
|
+
function asFiber(value) {
|
|
77
|
+
return value && typeof value === "object" ? value : null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { SelectionCapture } from "../server/types.js";
|
|
2
|
+
export type MountWandOptions = {
|
|
3
|
+
serverUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function mountWand(options?: MountWandOptions): void;
|
|
6
|
+
export declare function captureSelection(element: HTMLElement): Promise<SelectionCapture>;
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
import { toPng } from "html-to-image";
|
|
2
|
+
import { attributesFor, domPathFor, htmlFor, pickElementAtPoint, reactOwnerHintsFor, selectorFor, } from "./dom.js";
|
|
3
|
+
import { viewportCaptureOptions } from "./capture.js";
|
|
4
|
+
import { formatJsonPreview, setComposerSending } from "./panel.js";
|
|
5
|
+
import { styles } from "./styles.js";
|
|
6
|
+
const defaultServerUrl = typeof process !== "undefined" && process.env.NEXT_PUBLIC_WAND_SERVER_URL
|
|
7
|
+
? process.env.NEXT_PUBLIC_WAND_SERVER_URL
|
|
8
|
+
: "http://127.0.0.1:4711";
|
|
9
|
+
export function mountWand(options = {}) {
|
|
10
|
+
if (typeof window === "undefined" || document.querySelector("[data-wand-root]"))
|
|
11
|
+
return;
|
|
12
|
+
const serverUrl = options.serverUrl ?? defaultServerUrl;
|
|
13
|
+
const state = {
|
|
14
|
+
active: false,
|
|
15
|
+
selected: null,
|
|
16
|
+
hovered: null,
|
|
17
|
+
runs: [],
|
|
18
|
+
queue: [],
|
|
19
|
+
agentConfig: {
|
|
20
|
+
availableAdapters: [],
|
|
21
|
+
selectedAdapter: null,
|
|
22
|
+
},
|
|
23
|
+
runsEventSource: null,
|
|
24
|
+
composerFor: null,
|
|
25
|
+
composerDraft: "",
|
|
26
|
+
composerFocused: false,
|
|
27
|
+
composerSelectionStart: null,
|
|
28
|
+
composerSelectionEnd: null,
|
|
29
|
+
composerSelectionDirection: null,
|
|
30
|
+
};
|
|
31
|
+
const root = document.createElement("div");
|
|
32
|
+
root.dataset.wandRoot = "true";
|
|
33
|
+
root.dataset.wandUi = "true";
|
|
34
|
+
const shadow = root.attachShadow({ mode: "open" });
|
|
35
|
+
shadow.innerHTML = `<style>${styles}</style>`;
|
|
36
|
+
document.body.append(root);
|
|
37
|
+
const frame = document.createElement("div");
|
|
38
|
+
frame.className = "wand-frame";
|
|
39
|
+
frame.hidden = true;
|
|
40
|
+
frame.dataset.wandUi = "true";
|
|
41
|
+
const launcher = document.createElement("button");
|
|
42
|
+
launcher.type = "button";
|
|
43
|
+
launcher.className = "wand-launcher";
|
|
44
|
+
launcher.dataset.wandUi = "true";
|
|
45
|
+
launcher.setAttribute("aria-label", "Toggle Wand inspector");
|
|
46
|
+
launcher.innerHTML = inspectIcon();
|
|
47
|
+
shadow.append(frame, launcher);
|
|
48
|
+
launcher.addEventListener("click", async () => {
|
|
49
|
+
state.active = !state.active;
|
|
50
|
+
if (!state.active) {
|
|
51
|
+
state.selected = null;
|
|
52
|
+
state.hovered = null;
|
|
53
|
+
resetComposerState(state, null);
|
|
54
|
+
frame.hidden = true;
|
|
55
|
+
stopRunsStream(state);
|
|
56
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await Promise.all([
|
|
60
|
+
refreshQueue(state, serverUrl),
|
|
61
|
+
refreshRuns(state, serverUrl),
|
|
62
|
+
refreshAgentConfig(state, serverUrl),
|
|
63
|
+
]);
|
|
64
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
65
|
+
syncRunsStream(shadow, state, serverUrl, frame);
|
|
66
|
+
}
|
|
67
|
+
launcher.innerHTML = state.active ? closeIcon() : inspectIcon();
|
|
68
|
+
});
|
|
69
|
+
window.addEventListener("pointermove", (event) => {
|
|
70
|
+
if (!state.active || state.selected)
|
|
71
|
+
return;
|
|
72
|
+
state.hovered = pickElementAtPoint(event.clientX, event.clientY);
|
|
73
|
+
renderFrame(frame, state.hovered);
|
|
74
|
+
}, true);
|
|
75
|
+
window.addEventListener("pointerdown", (event) => {
|
|
76
|
+
if (!state.active)
|
|
77
|
+
return;
|
|
78
|
+
if (event.button !== 0)
|
|
79
|
+
return;
|
|
80
|
+
if (event.target instanceof Element && event.target.closest("[data-wand-root]"))
|
|
81
|
+
return;
|
|
82
|
+
const target = pickElementAtPoint(event.clientX, event.clientY);
|
|
83
|
+
if (!target)
|
|
84
|
+
return;
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
if (state.selected !== target)
|
|
88
|
+
resetComposerState(state, target);
|
|
89
|
+
state.selected = target;
|
|
90
|
+
renderFrame(frame, target);
|
|
91
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
92
|
+
}, true);
|
|
93
|
+
window.addEventListener("click", (event) => {
|
|
94
|
+
if (!state.active)
|
|
95
|
+
return;
|
|
96
|
+
if (event.target instanceof Element && event.target.closest("[data-wand-root]"))
|
|
97
|
+
return;
|
|
98
|
+
if (!pickElementAtPoint(event.clientX, event.clientY))
|
|
99
|
+
return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
}, true);
|
|
103
|
+
window.addEventListener("keydown", (event) => {
|
|
104
|
+
if (!state.active || event.key !== "Escape")
|
|
105
|
+
return;
|
|
106
|
+
state.active = false;
|
|
107
|
+
state.selected = null;
|
|
108
|
+
state.hovered = null;
|
|
109
|
+
resetComposerState(state, null);
|
|
110
|
+
frame.hidden = true;
|
|
111
|
+
stopRunsStream(state);
|
|
112
|
+
launcher.innerHTML = inspectIcon();
|
|
113
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
114
|
+
}, true);
|
|
115
|
+
window.addEventListener("resize", () => renderFrame(frame, state.selected ?? state.hovered));
|
|
116
|
+
window.addEventListener("scroll", () => renderFrame(frame, state.selected ?? state.hovered), true);
|
|
117
|
+
}
|
|
118
|
+
function renderPanel(shadow, state, serverUrl, frame) {
|
|
119
|
+
rememberComposerState(shadow, state);
|
|
120
|
+
shadow.querySelector(".wand-panel")?.remove();
|
|
121
|
+
if (!state.active)
|
|
122
|
+
return;
|
|
123
|
+
const panel = document.createElement("section");
|
|
124
|
+
panel.className = "wand-panel";
|
|
125
|
+
panel.dataset.wandUi = "true";
|
|
126
|
+
panel.innerHTML = `
|
|
127
|
+
<div class="wand-panel-header">
|
|
128
|
+
<span>${state.selected ? "Describe the change" : "Select an element"}</span>
|
|
129
|
+
<span class="wand-muted wand-run-count">${runCountText(state)}</span>
|
|
130
|
+
</div>
|
|
131
|
+
${state.selected
|
|
132
|
+
? `
|
|
133
|
+
<div class="wand-panel-section">
|
|
134
|
+
<textarea placeholder="What should change?"></textarea>
|
|
135
|
+
<div class="wand-actions">
|
|
136
|
+
<button type="button" data-action="send" ${state.agentConfig.selectedAdapter ? "" : "disabled"}>Send to agent</button>
|
|
137
|
+
<button type="button" data-action="queue">Queue it</button>
|
|
138
|
+
</div>
|
|
139
|
+
<label class="wand-agent-picker">
|
|
140
|
+
<span class="wand-muted">Agent</span>
|
|
141
|
+
<select data-action="select-adapter" ${state.agentConfig.availableAdapters.length === 0 ? "disabled" : ""}>
|
|
142
|
+
${adapterOptionsHtml(state.agentConfig)}
|
|
143
|
+
</select>
|
|
144
|
+
</label>
|
|
145
|
+
</div>
|
|
146
|
+
`
|
|
147
|
+
: `<div class="wand-panel-section wand-muted">Move the cursor, then click a page element.</div>`}
|
|
148
|
+
<div class="wand-queue">
|
|
149
|
+
${queueItemsHtml(state)}
|
|
150
|
+
</div>
|
|
151
|
+
`;
|
|
152
|
+
panel.addEventListener("change", async (event) => {
|
|
153
|
+
const target = event.target;
|
|
154
|
+
if (target.dataset.action !== "select-adapter")
|
|
155
|
+
return;
|
|
156
|
+
const adapter = target.value;
|
|
157
|
+
if (!state.agentConfig.availableAdapters.includes(adapter))
|
|
158
|
+
return;
|
|
159
|
+
try {
|
|
160
|
+
state.agentConfig = (await request(serverUrl, "/api/agent-config", {
|
|
161
|
+
method: "PUT",
|
|
162
|
+
headers: { "content-type": "application/json" },
|
|
163
|
+
body: JSON.stringify({ adapter }),
|
|
164
|
+
}));
|
|
165
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
toast(shadow, error instanceof Error ? error.message : String(error));
|
|
169
|
+
await refreshAgentConfig(state, serverUrl);
|
|
170
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
panel.addEventListener("click", async (event) => {
|
|
174
|
+
const target = event.target;
|
|
175
|
+
const action = target.dataset.action;
|
|
176
|
+
if (!action)
|
|
177
|
+
return;
|
|
178
|
+
try {
|
|
179
|
+
if (action === "send" || action === "queue") {
|
|
180
|
+
const textarea = panel.querySelector("textarea");
|
|
181
|
+
const prompt = textarea.value.trim();
|
|
182
|
+
if (!prompt || !state.selected)
|
|
183
|
+
return;
|
|
184
|
+
if (action === "send" && !state.agentConfig.selectedAdapter)
|
|
185
|
+
return;
|
|
186
|
+
if (action === "send")
|
|
187
|
+
setComposerSending(panel, true);
|
|
188
|
+
try {
|
|
189
|
+
const capture = await captureSelection(state.selected);
|
|
190
|
+
if (action === "send") {
|
|
191
|
+
toast(shadow, "Sending to agent...");
|
|
192
|
+
await post(serverUrl, "/api/send", {
|
|
193
|
+
prompt,
|
|
194
|
+
capture,
|
|
195
|
+
adapter: state.agentConfig.selectedAdapter,
|
|
196
|
+
});
|
|
197
|
+
toast(shadow, "Agent started.");
|
|
198
|
+
state.selected = null;
|
|
199
|
+
resetComposerState(state, null);
|
|
200
|
+
frame.hidden = true;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
await post(serverUrl, "/api/queue", { prompt, capture });
|
|
204
|
+
toast(shadow, "Queued.");
|
|
205
|
+
state.selected = null;
|
|
206
|
+
resetComposerState(state, null);
|
|
207
|
+
}
|
|
208
|
+
await Promise.all([refreshQueue(state, serverUrl), refreshRuns(state, serverUrl)]);
|
|
209
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
210
|
+
syncRunsStream(shadow, state, serverUrl, frame);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
if (action === "send" && panel.isConnected)
|
|
215
|
+
setComposerSending(panel, false);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (action === "stop-run") {
|
|
219
|
+
const item = target.closest("[data-run-id]");
|
|
220
|
+
const id = item?.dataset.runId;
|
|
221
|
+
if (!id)
|
|
222
|
+
return;
|
|
223
|
+
await request(serverUrl, `/api/runs/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
224
|
+
await refreshRuns(state, serverUrl);
|
|
225
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const item = target.closest("[data-queue-id]");
|
|
229
|
+
const id = item?.dataset.queueId;
|
|
230
|
+
if (!id)
|
|
231
|
+
return;
|
|
232
|
+
const queued = state.queue.find((entry) => entry.id === id);
|
|
233
|
+
if (!queued)
|
|
234
|
+
return;
|
|
235
|
+
if (action === "send-queued") {
|
|
236
|
+
toast(shadow, "Sending queued request...");
|
|
237
|
+
await post(serverUrl, "/api/send", { prompt: queued.prompt, context: queued.context });
|
|
238
|
+
await request(serverUrl, `/api/queue/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
239
|
+
toast(shadow, "Queued request started.");
|
|
240
|
+
}
|
|
241
|
+
else if (action === "delete-queued") {
|
|
242
|
+
await request(serverUrl, `/api/queue/${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
243
|
+
}
|
|
244
|
+
await Promise.all([refreshQueue(state, serverUrl), refreshRuns(state, serverUrl)]);
|
|
245
|
+
renderPanel(shadow, state, serverUrl, frame);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
toast(shadow, error instanceof Error ? error.message : String(error));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
shadow.append(panel);
|
|
252
|
+
restoreComposerState(shadow, panel, state);
|
|
253
|
+
}
|
|
254
|
+
export async function captureSelection(element) {
|
|
255
|
+
const rect = element.getBoundingClientRect();
|
|
256
|
+
return {
|
|
257
|
+
id: crypto.randomUUID(),
|
|
258
|
+
createdAt: new Date().toISOString(),
|
|
259
|
+
url: window.location.href,
|
|
260
|
+
route: `${window.location.pathname}${window.location.search}${window.location.hash}`,
|
|
261
|
+
title: document.title,
|
|
262
|
+
selector: selectorFor(element),
|
|
263
|
+
domPath: domPathFor(element),
|
|
264
|
+
tagName: element.tagName,
|
|
265
|
+
text: (element.textContent ?? "").trim().slice(0, 500),
|
|
266
|
+
attributes: attributesFor(element),
|
|
267
|
+
rect: {
|
|
268
|
+
x: rect.x,
|
|
269
|
+
y: rect.y,
|
|
270
|
+
width: rect.width,
|
|
271
|
+
height: rect.height,
|
|
272
|
+
},
|
|
273
|
+
reactOwnerHints: reactOwnerHintsFor(element),
|
|
274
|
+
elementHtml: htmlFor(element),
|
|
275
|
+
viewportDataUrl: await toPng(document.documentElement, viewportCaptureOptions({
|
|
276
|
+
viewportWidth: window.innerWidth,
|
|
277
|
+
viewportHeight: window.innerHeight,
|
|
278
|
+
scrollX: window.scrollX,
|
|
279
|
+
scrollY: window.scrollY,
|
|
280
|
+
documentWidth: document.documentElement.scrollWidth,
|
|
281
|
+
documentHeight: document.documentElement.scrollHeight,
|
|
282
|
+
})),
|
|
283
|
+
elementDataUrl: await toPng(element, { cacheBust: true }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function renderFrame(frame, target) {
|
|
287
|
+
if (!target) {
|
|
288
|
+
frame.hidden = true;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const rect = target.getBoundingClientRect();
|
|
292
|
+
frame.hidden = false;
|
|
293
|
+
frame.style.left = `${rect.left}px`;
|
|
294
|
+
frame.style.top = `${rect.top}px`;
|
|
295
|
+
frame.style.width = `${rect.width}px`;
|
|
296
|
+
frame.style.height = `${rect.height}px`;
|
|
297
|
+
}
|
|
298
|
+
async function refreshQueue(state, serverUrl) {
|
|
299
|
+
try {
|
|
300
|
+
state.queue = (await request(serverUrl, "/api/queue"));
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
state.queue = [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async function refreshRuns(state, serverUrl) {
|
|
307
|
+
try {
|
|
308
|
+
state.runs = (await request(serverUrl, "/api/runs"));
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
state.runs = [];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async function refreshAgentConfig(state, serverUrl) {
|
|
315
|
+
try {
|
|
316
|
+
state.agentConfig = (await request(serverUrl, "/api/agent-config"));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
state.agentConfig = {
|
|
320
|
+
availableAdapters: [],
|
|
321
|
+
selectedAdapter: null,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function syncRunsStream(shadow, state, serverUrl, frame) {
|
|
326
|
+
if (!state.active || state.runsEventSource !== null)
|
|
327
|
+
return;
|
|
328
|
+
const source = new EventSource(`${serverUrl}/api/runs/events`);
|
|
329
|
+
source.addEventListener("runs", (event) => {
|
|
330
|
+
state.runs = JSON.parse(event.data);
|
|
331
|
+
if (state.active) {
|
|
332
|
+
renderQueue(shadow, state);
|
|
333
|
+
renderRunCount(shadow, state);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
state.runsEventSource = source;
|
|
337
|
+
}
|
|
338
|
+
function stopRunsStream(state) {
|
|
339
|
+
state.runsEventSource?.close();
|
|
340
|
+
state.runsEventSource = null;
|
|
341
|
+
}
|
|
342
|
+
async function post(serverUrl, pathname, body) {
|
|
343
|
+
return request(serverUrl, pathname, {
|
|
344
|
+
method: "POST",
|
|
345
|
+
headers: { "content-type": "application/json" },
|
|
346
|
+
body: JSON.stringify(body),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function request(serverUrl, pathname, init) {
|
|
350
|
+
const response = await fetch(`${serverUrl}${pathname}`, init);
|
|
351
|
+
const payload = (await response.json());
|
|
352
|
+
if (!response.ok)
|
|
353
|
+
throw new Error(payload.error ?? `Request failed: ${response.status}`);
|
|
354
|
+
return payload;
|
|
355
|
+
}
|
|
356
|
+
function toast(shadow, message) {
|
|
357
|
+
shadow.querySelector(".wand-toast")?.remove();
|
|
358
|
+
const node = document.createElement("div");
|
|
359
|
+
node.className = "wand-toast";
|
|
360
|
+
node.textContent = message;
|
|
361
|
+
shadow.append(node);
|
|
362
|
+
window.setTimeout(() => node.remove(), 2400);
|
|
363
|
+
}
|
|
364
|
+
function escapeHtml(value) {
|
|
365
|
+
return value
|
|
366
|
+
.replaceAll("&", "&")
|
|
367
|
+
.replaceAll("<", "<")
|
|
368
|
+
.replaceAll(">", ">")
|
|
369
|
+
.replaceAll('"', """);
|
|
370
|
+
}
|
|
371
|
+
function renderQueue(shadow, state) {
|
|
372
|
+
const queue = shadow.querySelector(".wand-queue");
|
|
373
|
+
if (!queue)
|
|
374
|
+
return;
|
|
375
|
+
queue.innerHTML = queueItemsHtml(state);
|
|
376
|
+
}
|
|
377
|
+
function renderRunCount(shadow, state) {
|
|
378
|
+
const count = shadow.querySelector(".wand-run-count");
|
|
379
|
+
if (!count)
|
|
380
|
+
return;
|
|
381
|
+
count.textContent = runCountText(state);
|
|
382
|
+
}
|
|
383
|
+
function runCountText(state) {
|
|
384
|
+
return `${state.runs.length} running / ${state.queue.length} queued`;
|
|
385
|
+
}
|
|
386
|
+
function adapterOptionsHtml(config) {
|
|
387
|
+
const placeholder = config.selectedAdapter ? "" : `<option value="">Choose agent</option>`;
|
|
388
|
+
return `${placeholder}${config.availableAdapters
|
|
389
|
+
.map((adapter) => `<option value="${adapter}" ${adapter === config.selectedAdapter ? "selected" : ""}>${adapterLabel(adapter)}</option>`)
|
|
390
|
+
.join("")}`;
|
|
391
|
+
}
|
|
392
|
+
function adapterLabel(adapter) {
|
|
393
|
+
switch (adapter) {
|
|
394
|
+
case "claude-code":
|
|
395
|
+
return "Claude Code";
|
|
396
|
+
case "opencode":
|
|
397
|
+
return "OpenCode";
|
|
398
|
+
case "codex":
|
|
399
|
+
return "Codex";
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function queueItemsHtml(state) {
|
|
403
|
+
return `
|
|
404
|
+
${state.runs
|
|
405
|
+
.map((run) => `
|
|
406
|
+
<div class="wand-queue-item wand-agent-run" data-run-id="${run.id}">
|
|
407
|
+
<div class="wand-run-header">
|
|
408
|
+
<div class="wand-run-title">${escapeHtml(run.prompt)}</div>
|
|
409
|
+
<button type="button" data-action="stop-run">Stop</button>
|
|
410
|
+
</div>
|
|
411
|
+
<pre class="wand-run-message">${escapeHtml(formatJsonPreview(run.latestMessage))}</pre>
|
|
412
|
+
</div>
|
|
413
|
+
`)
|
|
414
|
+
.join("")}
|
|
415
|
+
${state.queue
|
|
416
|
+
.map((item) => `
|
|
417
|
+
<div class="wand-queue-item" data-queue-id="${item.id}">
|
|
418
|
+
<div>${escapeHtml(item.prompt)}</div>
|
|
419
|
+
<div class="wand-muted">${new Date(item.createdAt).toLocaleString()}</div>
|
|
420
|
+
<div class="wand-queue-controls">
|
|
421
|
+
<button type="button" data-action="send-queued">Send</button>
|
|
422
|
+
<button type="button" data-action="delete-queued">Delete</button>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
`)
|
|
426
|
+
.join("")}
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
function rememberComposerState(shadow, state) {
|
|
430
|
+
const textarea = shadow.querySelector(".wand-panel textarea");
|
|
431
|
+
if (!textarea || state.composerFor !== state.selected)
|
|
432
|
+
return;
|
|
433
|
+
syncComposerState(shadow, textarea, state);
|
|
434
|
+
}
|
|
435
|
+
function restoreComposerState(shadow, panel, state) {
|
|
436
|
+
const textarea = panel.querySelector("textarea");
|
|
437
|
+
if (!textarea || !state.selected)
|
|
438
|
+
return;
|
|
439
|
+
if (state.composerFor !== state.selected)
|
|
440
|
+
resetComposerState(state, state.selected);
|
|
441
|
+
textarea.value = state.composerDraft;
|
|
442
|
+
const sync = () => syncComposerState(shadow, textarea, state);
|
|
443
|
+
textarea.addEventListener("input", sync);
|
|
444
|
+
textarea.addEventListener("focus", sync);
|
|
445
|
+
textarea.addEventListener("blur", sync);
|
|
446
|
+
textarea.addEventListener("select", sync);
|
|
447
|
+
textarea.addEventListener("keyup", sync);
|
|
448
|
+
textarea.addEventListener("click", sync);
|
|
449
|
+
if (!state.composerFocused)
|
|
450
|
+
return;
|
|
451
|
+
const selectionStart = state.composerSelectionStart;
|
|
452
|
+
const selectionEnd = state.composerSelectionEnd;
|
|
453
|
+
const selectionDirection = state.composerSelectionDirection;
|
|
454
|
+
textarea.focus();
|
|
455
|
+
if (selectionStart !== null && selectionEnd !== null) {
|
|
456
|
+
textarea.setSelectionRange(selectionStart, selectionEnd, selectionDirection ?? "none");
|
|
457
|
+
}
|
|
458
|
+
sync();
|
|
459
|
+
}
|
|
460
|
+
function syncComposerState(shadow, textarea, state) {
|
|
461
|
+
state.composerDraft = textarea.value;
|
|
462
|
+
state.composerFocused = shadow.activeElement === textarea;
|
|
463
|
+
state.composerSelectionStart = textarea.selectionStart;
|
|
464
|
+
state.composerSelectionEnd = textarea.selectionEnd;
|
|
465
|
+
state.composerSelectionDirection = textarea.selectionDirection;
|
|
466
|
+
}
|
|
467
|
+
function resetComposerState(state, selected) {
|
|
468
|
+
state.composerFor = selected;
|
|
469
|
+
state.composerDraft = "";
|
|
470
|
+
state.composerFocused = false;
|
|
471
|
+
state.composerSelectionStart = null;
|
|
472
|
+
state.composerSelectionEnd = null;
|
|
473
|
+
state.composerSelectionDirection = null;
|
|
474
|
+
}
|
|
475
|
+
function inspectIcon() {
|
|
476
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>`;
|
|
477
|
+
}
|
|
478
|
+
function closeIcon() {
|
|
479
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6 6 18M6 6l12 12"/></svg>`;
|
|
480
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function setComposerSending(panel, sending) {
|
|
2
|
+
const sendButton = panel.querySelector('[data-action="send"]');
|
|
3
|
+
const queueButton = panel.querySelector('[data-action="queue"]');
|
|
4
|
+
if (!sendButton || !queueButton)
|
|
5
|
+
return;
|
|
6
|
+
sendButton.disabled = sending;
|
|
7
|
+
queueButton.disabled = sending;
|
|
8
|
+
sendButton.classList.toggle("wand-sending", sending);
|
|
9
|
+
sendButton.innerHTML = sending
|
|
10
|
+
? `<span class="wand-spinner" aria-hidden="true"></span><span>Sending...</span>`
|
|
11
|
+
: "Send to agent";
|
|
12
|
+
}
|
|
13
|
+
export function formatJsonPreview(value, maxLength = 180) {
|
|
14
|
+
let json;
|
|
15
|
+
try {
|
|
16
|
+
json = JSON.stringify(value) ?? "null";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
json = "[unserializable]";
|
|
20
|
+
}
|
|
21
|
+
if (json.length <= maxLength)
|
|
22
|
+
return json;
|
|
23
|
+
if (maxLength <= 3)
|
|
24
|
+
return ".".repeat(maxLength);
|
|
25
|
+
return `${json.slice(0, maxLength - 3)}...`;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles = "\n.wand-launcher,\n.wand-panel,\n.wand-frame,\n.wand-toast {\n box-sizing: border-box;\n font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.wand-launcher {\n position: fixed;\n right: 18px;\n bottom: 18px;\n z-index: 2147483647;\n width: 42px;\n height: 42px;\n border: 1px solid rgba(17, 24, 39, 0.15);\n border-radius: 999px;\n background: rgba(17, 24, 39, 0.94);\n color: white;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);\n}\n.wand-launcher svg { width: 18px; height: 18px; }\n.wand-frame {\n position: fixed;\n z-index: 2147483645;\n pointer-events: none;\n border: 2px solid #3c7ce6;\n background: rgba(60, 124, 230, 0.08);\n box-shadow: 0 0 0 1px rgba(60, 124, 230, 0.28);\n transition: left 120ms ease-out, top 120ms ease-out, width 120ms ease-out, height 120ms ease-out;\n}\n.wand-panel {\n position: fixed;\n right: 18px;\n bottom: 72px;\n z-index: 2147483647;\n width: min(360px, calc(100vw - 36px));\n border: 1px solid rgba(17, 24, 39, 0.14);\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.98);\n color: #111827;\n box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18);\n overflow: hidden;\n}\n.wand-panel-header,\n.wand-panel-section {\n padding: 12px;\n}\n.wand-panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid rgba(17, 24, 39, 0.08);\n font-size: 13px;\n font-weight: 600;\n}\n.wand-panel textarea {\n box-sizing: border-box;\n width: 100%;\n max-width: 100%;\n min-height: 92px;\n resize: vertical;\n border: 1px solid rgba(17, 24, 39, 0.16);\n border-radius: 6px;\n padding: 9px 10px;\n color: #111827;\n background: white;\n font: inherit;\n}\n.wand-actions {\n display: flex;\n gap: 8px;\n margin-top: 10px;\n}\n.wand-agent-picker {\n display: grid;\n gap: 5px;\n margin-top: 10px;\n}\n.wand-agent-picker select {\n box-sizing: border-box;\n width: 100%;\n border: 1px solid rgba(17, 24, 39, 0.16);\n border-radius: 6px;\n padding: 8px 10px;\n color: #111827;\n background: white;\n font: inherit;\n}\n.wand-agent-picker select:disabled {\n color: rgba(17, 24, 39, 0.42);\n background: #f8fafc;\n}\n.wand-actions button,\n.wand-queue-item button {\n border: 0;\n border-radius: 6px;\n padding: 8px 10px;\n font: inherit;\n cursor: pointer;\n}\n.wand-actions button:disabled {\n cursor: wait;\n opacity: 0.72;\n}\n.wand-actions button:first-child {\n background: #111827;\n color: white;\n}\n.wand-actions button:last-child,\n.wand-queue-item button {\n background: #eef2f7;\n color: #111827;\n}\n.wand-queue {\n border-top: 1px solid rgba(17, 24, 39, 0.08);\n}\n.wand-queue-item {\n display: grid;\n gap: 8px;\n padding: 10px 12px;\n border-top: 1px solid rgba(17, 24, 39, 0.06);\n font-size: 12px;\n}\n.wand-queue-item:first-child { border-top: 0; }\n.wand-queue-controls { display: flex; gap: 8px; }\n.wand-agent-run {\n background: rgba(60, 124, 230, 0.08);\n}\n.wand-run-header {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.wand-run-title {\n min-width: 0;\n flex: 1;\n overflow-wrap: anywhere;\n}\n.wand-run-header button {\n flex: none;\n}\n.wand-run-message {\n overflow: hidden;\n margin: 0;\n color: rgba(17, 24, 39, 0.76);\n font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.wand-sending {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\n.wand-spinner {\n width: 12px;\n height: 12px;\n border: 2px solid rgba(255, 255, 255, 0.38);\n border-top-color: currentColor;\n border-radius: 999px;\n animation: wand-spin 700ms linear infinite;\n}\n@keyframes wand-spin {\n to { transform: rotate(360deg); }\n}\n@media (prefers-reduced-motion: reduce) {\n .wand-spinner { animation: none; }\n}\n.wand-muted {\n color: rgba(17, 24, 39, 0.58);\n font-size: 12px;\n}\n.wand-toast {\n position: fixed;\n right: 18px;\n bottom: 72px;\n z-index: 2147483647;\n max-width: min(320px, calc(100vw - 36px));\n border-radius: 6px;\n background: rgba(17, 24, 39, 0.94);\n color: white;\n padding: 9px 11px;\n font-size: 12px;\n box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);\n}\n";
|