waygraph 0.15.28 → 0.15.30
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/dist/pilot-overlay/fixtures.d.ts +81 -0
- package/dist/pilot-overlay/fixtures.js +334 -0
- package/dist/pilot-overlay/panel.d.ts +34 -0
- package/dist/pilot-overlay/panel.js +155 -0
- package/dist/pilot-overlay/shell.d.ts +48 -0
- package/dist/pilot-overlay/shell.js +276 -0
- package/dist/pilot-overlay.d.ts +3 -158
- package/dist/pilot-overlay.js +4 -778
- package/dist/runner/inpage/banner.js +68 -0
- package/dist/runner/inpage/core.js +267 -0
- package/dist/runner/inpage/device.js +211 -0
- package/dist/runner/inpage/panel-chrome.js +152 -0
- package/dist/runner/inpage/stamp.js +46 -0
- package/dist/runner/inpage/swipe-zoom.js +220 -0
- package/dist/runner/inpage/todos.js +332 -0
- package/dist/runner/overlay-install.js +15 -1269
- package/examples/saucedemo/.waygraph-auto/3f088138.log +0 -0
- package/examples/saucedemo/.waygraph-auto/94221b2b.log +0 -0
- package/examples/saucedemo/.waygraph-auto/bb0e0e46.log +0 -0
- package/examples/saucedemo/.waygraph-auto/e230f24e.log +0 -0
- package/package.json +1 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { DevicePreset, DeviceState, HighlightTone, TodoDockUiOpts } from "../highlights.js";
|
|
2
|
+
import type { Page } from "@playwright/test";
|
|
3
|
+
/** One ring the Pilot agent wants painted (same fields as demo stub rings). */
|
|
4
|
+
export type PilotFixtureRing = {
|
|
5
|
+
selector: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** Extra caption line (demo `detail`). */
|
|
8
|
+
detail?: string;
|
|
9
|
+
/** Short badge, e.g. AC / BUG / GATE. */
|
|
10
|
+
tag?: string;
|
|
11
|
+
/** Any {@link HighlightTone} or alias (`error`/`blue`/…). Normalized on paint. */
|
|
12
|
+
tone?: HighlightTone | string;
|
|
13
|
+
size?: "sm" | "md" | "lg" | string;
|
|
14
|
+
weight?: "normal" | "bold" | string;
|
|
15
|
+
/** Caption text color (CSS). Overrides tone label color when set. */
|
|
16
|
+
color?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Camera zoom while this ring is up (demo parity: scroll + zoom badge;
|
|
19
|
+
* does not CSS-scale the page). Typical `1.25`..`2`.
|
|
20
|
+
*/
|
|
21
|
+
zoom?: number;
|
|
22
|
+
/** When false, keep zoom after this paint / holdMs. Default true. */
|
|
23
|
+
zoomOut?: boolean;
|
|
24
|
+
/** Spotlight: dim the rest of the page around this target. */
|
|
25
|
+
focus?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Agent-sent highlight fixtures for a live Pilot / auto session.
|
|
29
|
+
* Painted by {@link showPilotFixtures}; driven via IPC `op: "highlight"`.
|
|
30
|
+
* Mirrors demo stub surface: rings, todos, zoom, focus, device.
|
|
31
|
+
*/
|
|
32
|
+
export type PilotHighlightFixtures = {
|
|
33
|
+
/** Replace current fixture rings with these (empty + no todos = clear). */
|
|
34
|
+
rings?: readonly PilotFixtureRing[];
|
|
35
|
+
/** Optional floating todo list (demo-dock language, simplified). */
|
|
36
|
+
todos?: readonly string[];
|
|
37
|
+
/** 0-based index of the "current" todo row (others before it = done). */
|
|
38
|
+
todoIndex?: number;
|
|
39
|
+
/** Optional title above the todo list. */
|
|
40
|
+
todoTitle?: string;
|
|
41
|
+
/** Todo dock side. Default `right`. */
|
|
42
|
+
todoPos?: "left" | "right";
|
|
43
|
+
/**
|
|
44
|
+
* Todo-dock UX (compact / collision / behind-ring). Defaults smart-on;
|
|
45
|
+
* pass false fields to opt out. See {@link TodoDockUiOpts}.
|
|
46
|
+
*/
|
|
47
|
+
todoUi?: TodoDockUiOpts;
|
|
48
|
+
/**
|
|
49
|
+
* How long fixtures stay visible (ms). `0` = until the next highlight /
|
|
50
|
+
* clear. Default `30000` (30s) unless specified.
|
|
51
|
+
*/
|
|
52
|
+
holdMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Camera zoom toward a selector (demo parity: scrollIntoView + zoom badge).
|
|
55
|
+
* Defaults the zoom target to {@link zoomSelector}, else the last ring's
|
|
56
|
+
* selector (or a ring that sets its own `zoom`). Cleared on `clear: true`
|
|
57
|
+
* or when holdMs expires (if zoomOut).
|
|
58
|
+
*/
|
|
59
|
+
zoom?: number;
|
|
60
|
+
/** Element to zoom toward when {@link zoom} is set (defaults to last ring). */
|
|
61
|
+
zoomSelector?: string;
|
|
62
|
+
/** When false, keep zoom after fixtures clear/expire. Default true. */
|
|
63
|
+
zoomOut?: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Viewport / device fixture (`mobile` | `tablet` | `desktop`, or a full
|
|
66
|
+
* {@link DeviceState}). Applied via Playwright `setViewportSize`.
|
|
67
|
+
*/
|
|
68
|
+
device?: DevicePreset | DeviceState;
|
|
69
|
+
/** Drop every agent fixture ring/todo/focus/zoom immediately. */
|
|
70
|
+
clear?: boolean;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Paint (or clear) agent-authored highlight fixtures on the live page.
|
|
74
|
+
* Multi-ring + optional todo strip + zoom/focus/device - the Pilot equivalent
|
|
75
|
+
* of Block `stubBefore` narration in `waygraph demo`.
|
|
76
|
+
* Does not import `cli.ts` (see file header).
|
|
77
|
+
*/
|
|
78
|
+
export declare function showPilotFixtures(page: Page, fixtures: PilotHighlightFixtures): Promise<{
|
|
79
|
+
painted: number;
|
|
80
|
+
missing: string[];
|
|
81
|
+
}>;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// Split out of the former 950-line pilot-overlay.ts (see src/ARCHITECTURE.md). Behavior unchanged.
|
|
2
|
+
import { formatHighlightCaption, normalizeHighlightSize, normalizeHighlightTone, normalizeHighlightWeight, resolveDeviceState, resolveTodoDockUi } from "../highlights.js";
|
|
3
|
+
import { ensureInstalled } from "./shell.js";
|
|
4
|
+
/**
|
|
5
|
+
* Paint (or clear) agent-authored highlight fixtures on the live page.
|
|
6
|
+
* Multi-ring + optional todo strip + zoom/focus/device - the Pilot equivalent
|
|
7
|
+
* of Block `stubBefore` narration in `waygraph demo`.
|
|
8
|
+
* Does not import `cli.ts` (see file header).
|
|
9
|
+
*/
|
|
10
|
+
export async function showPilotFixtures(page, fixtures) {
|
|
11
|
+
await ensureInstalled(page);
|
|
12
|
+
const holdMs = fixtures.clear === true
|
|
13
|
+
? 0
|
|
14
|
+
: fixtures.holdMs === undefined
|
|
15
|
+
? 30_000
|
|
16
|
+
: Math.max(0, fixtures.holdMs);
|
|
17
|
+
const rings = (fixtures.clear ? [] : (fixtures.rings ?? [])).map((r) => {
|
|
18
|
+
const tone = normalizeHighlightTone(r.tone);
|
|
19
|
+
const size = normalizeHighlightSize(r.size);
|
|
20
|
+
const weight = normalizeHighlightWeight(r.weight);
|
|
21
|
+
const caption = { label: r.label, tone };
|
|
22
|
+
if (r.detail)
|
|
23
|
+
caption.detail = r.detail;
|
|
24
|
+
if (r.tag)
|
|
25
|
+
caption.tag = r.tag;
|
|
26
|
+
const label = formatHighlightCaption(caption);
|
|
27
|
+
return {
|
|
28
|
+
selector: r.selector,
|
|
29
|
+
label,
|
|
30
|
+
tone,
|
|
31
|
+
size,
|
|
32
|
+
weight,
|
|
33
|
+
color: typeof r.color === "string" && r.color.trim() ? r.color.trim() : "",
|
|
34
|
+
zoom: typeof r.zoom === "number" && Number.isFinite(r.zoom) && r.zoom > 0
|
|
35
|
+
? r.zoom
|
|
36
|
+
: undefined,
|
|
37
|
+
zoomOut: r.zoomOut,
|
|
38
|
+
focus: r.focus === true,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const todos = fixtures.clear ? [] : (fixtures.todos ?? []);
|
|
42
|
+
const todoIndex = fixtures.todoIndex ?? 0;
|
|
43
|
+
const todoTitle = fixtures.todoTitle ?? "Plan";
|
|
44
|
+
const todoPos = fixtures.todoPos === "left" ? "left" : "right";
|
|
45
|
+
const todoUi = resolveTodoDockUi(fixtures.todoUi ?? null);
|
|
46
|
+
// Effective zoom: phase zoom, else last ring that authored zoom.
|
|
47
|
+
let zoom = fixtures.clear === true
|
|
48
|
+
? 1
|
|
49
|
+
: typeof fixtures.zoom === "number" && Number.isFinite(fixtures.zoom)
|
|
50
|
+
? fixtures.zoom
|
|
51
|
+
: 1;
|
|
52
|
+
let zoomSelector = fixtures.zoomSelector ??
|
|
53
|
+
(rings.length > 0 ? rings[rings.length - 1].selector : undefined);
|
|
54
|
+
let zoomOut = fixtures.zoomOut !== false;
|
|
55
|
+
if (!fixtures.clear && zoom <= 1.001) {
|
|
56
|
+
for (let i = rings.length - 1; i >= 0; i--) {
|
|
57
|
+
const r = rings[i];
|
|
58
|
+
if (r.zoom !== undefined && r.zoom > 1.001) {
|
|
59
|
+
zoom = r.zoom;
|
|
60
|
+
zoomSelector = r.selector;
|
|
61
|
+
if (r.zoomOut !== undefined)
|
|
62
|
+
zoomOut = r.zoomOut !== false;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (!fixtures.clear) {
|
|
68
|
+
// Phase zoomOut can still be overridden by the targeted ring.
|
|
69
|
+
const hit = rings.find((r) => r.selector === zoomSelector);
|
|
70
|
+
if (hit?.zoomOut !== undefined)
|
|
71
|
+
zoomOut = hit.zoomOut !== false;
|
|
72
|
+
}
|
|
73
|
+
// Device viewport (Node-side) before paint so rings land on the new size.
|
|
74
|
+
if (fixtures.clear === true) {
|
|
75
|
+
// Leave viewport alone on clear - only drop overlays.
|
|
76
|
+
}
|
|
77
|
+
else if (fixtures.device !== undefined) {
|
|
78
|
+
const d = resolveDeviceState(fixtures.device);
|
|
79
|
+
if (d) {
|
|
80
|
+
await page
|
|
81
|
+
.setViewportSize({
|
|
82
|
+
width: Math.max(200, Math.floor(d.viewport.width)),
|
|
83
|
+
height: Math.max(200, Math.floor(d.viewport.height)),
|
|
84
|
+
})
|
|
85
|
+
.catch(() => { });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const deviceLabel = fixtures.clear === true
|
|
89
|
+
? ""
|
|
90
|
+
: fixtures.device === undefined
|
|
91
|
+
? ""
|
|
92
|
+
: typeof fixtures.device === "string"
|
|
93
|
+
? fixtures.device
|
|
94
|
+
: fixtures.device.preset || "device";
|
|
95
|
+
return page
|
|
96
|
+
.evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, todoUi, }) => {
|
|
97
|
+
const w = window;
|
|
98
|
+
if (w.__wgPilotFxHideTimer)
|
|
99
|
+
clearTimeout(w.__wgPilotFxHideTimer);
|
|
100
|
+
const clearFocus = () => {
|
|
101
|
+
const veil = document.getElementById("wg-pilot-fx-focus");
|
|
102
|
+
if (!veil)
|
|
103
|
+
return;
|
|
104
|
+
veil.classList.remove("wg-in");
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
const v = document.getElementById("wg-pilot-fx-focus");
|
|
107
|
+
if (v && !v.classList.contains("wg-in"))
|
|
108
|
+
v.remove();
|
|
109
|
+
}, 320);
|
|
110
|
+
};
|
|
111
|
+
const applyFocus = (box) => {
|
|
112
|
+
let veil = document.getElementById("wg-pilot-fx-focus");
|
|
113
|
+
if (!veil) {
|
|
114
|
+
veil = document.createElement("div");
|
|
115
|
+
veil.id = "wg-pilot-fx-focus";
|
|
116
|
+
document.documentElement.appendChild(veil);
|
|
117
|
+
}
|
|
118
|
+
const pad = 10;
|
|
119
|
+
veil.style.left = `${Math.max(0, box.x - pad)}px`;
|
|
120
|
+
veil.style.top = `${Math.max(0, box.y - pad)}px`;
|
|
121
|
+
veil.style.width = `${Math.max(8, box.width + pad * 2)}px`;
|
|
122
|
+
veil.style.height = `${Math.max(8, box.height + pad * 2)}px`;
|
|
123
|
+
void veil.offsetWidth;
|
|
124
|
+
veil.classList.add("wg-in");
|
|
125
|
+
};
|
|
126
|
+
const setZoomBadge = (scale) => {
|
|
127
|
+
const level = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
128
|
+
let badge = document.getElementById("wg-pilot-fx-zoom");
|
|
129
|
+
if (!badge) {
|
|
130
|
+
badge = document.createElement("div");
|
|
131
|
+
badge.id = "wg-pilot-fx-zoom";
|
|
132
|
+
document.documentElement.appendChild(badge);
|
|
133
|
+
}
|
|
134
|
+
const zoomed = level > 1.001 || level < 0.999;
|
|
135
|
+
badge.dataset.zoomed = zoomed ? "1" : "0";
|
|
136
|
+
badge.innerHTML =
|
|
137
|
+
'<span class="wg-pilot-fx-zoom-ico"><svg viewBox="0 0 24 24" fill="none" stroke="#fde68a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-3.5-3.5"/></svg></span>' +
|
|
138
|
+
`<span class="wg-pilot-fx-zoom-val">${level.toFixed(2)}\u00d7</span>`;
|
|
139
|
+
};
|
|
140
|
+
const clearZoom = () => {
|
|
141
|
+
// Undo any leftover CSS-scale from older Pilot zoom path.
|
|
142
|
+
const root = document.documentElement;
|
|
143
|
+
root.style.removeProperty("transform");
|
|
144
|
+
root.style.removeProperty("transform-origin");
|
|
145
|
+
root.style.removeProperty("transition");
|
|
146
|
+
document.getElementById("wg-pilot-fx-zoom")?.remove();
|
|
147
|
+
};
|
|
148
|
+
const setDeviceChip = (label) => {
|
|
149
|
+
document.getElementById("wg-pilot-fx-device")?.remove();
|
|
150
|
+
if (!label)
|
|
151
|
+
return;
|
|
152
|
+
const chip = document.createElement("div");
|
|
153
|
+
chip.id = "wg-pilot-fx-device";
|
|
154
|
+
chip.dataset.preset = label;
|
|
155
|
+
chip.textContent = `device \u00b7 ${label}`;
|
|
156
|
+
document.documentElement.appendChild(chip);
|
|
157
|
+
};
|
|
158
|
+
w.__wgPilotFxZoomOutOnHide = zoomOut;
|
|
159
|
+
const clearFx = () => {
|
|
160
|
+
document.querySelectorAll(".wg-pilot-fx-ring, .wg-pilot-fx-label").forEach((el) => el.remove());
|
|
161
|
+
document.getElementById("wg-pilot-fx-todos")?.remove();
|
|
162
|
+
clearFocus();
|
|
163
|
+
if (w.__wgPilotFxZoomOutOnHide !== false || clearAll)
|
|
164
|
+
clearZoom();
|
|
165
|
+
if (clearAll) {
|
|
166
|
+
document.getElementById("wg-pilot-fx-device")?.remove();
|
|
167
|
+
clearZoom();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
w.__wgPilotFxClear = clearFx;
|
|
171
|
+
clearFx();
|
|
172
|
+
if (clearAll) {
|
|
173
|
+
return { painted: 0, missing: [] };
|
|
174
|
+
}
|
|
175
|
+
const missing = [];
|
|
176
|
+
let painted = 0;
|
|
177
|
+
let focusBox = null;
|
|
178
|
+
for (const ring of rings) {
|
|
179
|
+
const el = document.querySelector(ring.selector);
|
|
180
|
+
if (!el) {
|
|
181
|
+
missing.push(ring.selector);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const rect = el.getBoundingClientRect();
|
|
185
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
186
|
+
missing.push(ring.selector);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const pad = ring.size === "sm" ? 3 : ring.size === "lg" ? 10 : 6;
|
|
190
|
+
const ringEl = document.createElement("div");
|
|
191
|
+
ringEl.className = "wg-pilot-fx-ring";
|
|
192
|
+
ringEl.dataset.tone = ring.tone;
|
|
193
|
+
ringEl.dataset.size = ring.size;
|
|
194
|
+
ringEl.style.left = `${rect.left - pad}px`;
|
|
195
|
+
ringEl.style.top = `${rect.top - pad}px`;
|
|
196
|
+
ringEl.style.width = `${Math.max(4, rect.width + pad * 2)}px`;
|
|
197
|
+
ringEl.style.height = `${Math.max(4, rect.height + pad * 2)}px`;
|
|
198
|
+
ringEl.style.opacity = "1";
|
|
199
|
+
const labelEl = document.createElement("div");
|
|
200
|
+
labelEl.className = "wg-pilot-fx-label";
|
|
201
|
+
labelEl.dataset.tone = ring.tone;
|
|
202
|
+
labelEl.dataset.size = ring.size;
|
|
203
|
+
labelEl.dataset.weight = ring.weight;
|
|
204
|
+
labelEl.textContent = ring.label;
|
|
205
|
+
labelEl.style.opacity = "1";
|
|
206
|
+
if (ring.color) {
|
|
207
|
+
labelEl.style.color = ring.color;
|
|
208
|
+
}
|
|
209
|
+
document.documentElement.appendChild(ringEl);
|
|
210
|
+
document.documentElement.appendChild(labelEl);
|
|
211
|
+
const lw = labelEl.offsetWidth;
|
|
212
|
+
const lh = labelEl.offsetHeight;
|
|
213
|
+
let labelLeft = rect.left - pad;
|
|
214
|
+
let labelTop = rect.top - pad + rect.height + pad * 2 + 8;
|
|
215
|
+
if (labelTop + lh > window.innerHeight - 6)
|
|
216
|
+
labelTop = rect.top - pad - lh - 8;
|
|
217
|
+
if (labelTop < 6)
|
|
218
|
+
labelTop = 6;
|
|
219
|
+
if (labelLeft + lw > window.innerWidth - 6) {
|
|
220
|
+
labelLeft = Math.max(6, window.innerWidth - 6 - lw);
|
|
221
|
+
}
|
|
222
|
+
if (labelLeft < 6)
|
|
223
|
+
labelLeft = 6;
|
|
224
|
+
labelEl.style.left = `${labelLeft}px`;
|
|
225
|
+
labelEl.style.top = `${labelTop}px`;
|
|
226
|
+
painted += 1;
|
|
227
|
+
if (ring.focus) {
|
|
228
|
+
focusBox = { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (todos.length > 0) {
|
|
232
|
+
const dock = document.createElement("div");
|
|
233
|
+
dock.id = "wg-pilot-fx-todos";
|
|
234
|
+
dock.dataset.pos = todoPos;
|
|
235
|
+
const title = document.createElement("div");
|
|
236
|
+
title.className = "wg-pilot-fx-todo-title";
|
|
237
|
+
title.textContent = todoTitle;
|
|
238
|
+
dock.appendChild(title);
|
|
239
|
+
const WINDOW = todoUi.cap > 0 ? todoUi.cap : 5;
|
|
240
|
+
const compact = todoUi.compact !== false && todos.length > WINDOW;
|
|
241
|
+
if (compact)
|
|
242
|
+
dock.dataset.compact = "1";
|
|
243
|
+
let start = 0;
|
|
244
|
+
let end = todos.length;
|
|
245
|
+
if (compact) {
|
|
246
|
+
start = Math.max(0, todoIndex - Math.floor((WINDOW - 1) / 2));
|
|
247
|
+
end = Math.min(todos.length, start + WINDOW);
|
|
248
|
+
start = Math.max(0, end - WINDOW);
|
|
249
|
+
}
|
|
250
|
+
const ol = document.createElement("ol");
|
|
251
|
+
todos.forEach((text, i) => {
|
|
252
|
+
const li = document.createElement("li");
|
|
253
|
+
li.textContent = text;
|
|
254
|
+
if (i < todoIndex)
|
|
255
|
+
li.className = "wg-pilot-fx-todo-done";
|
|
256
|
+
else if (i === todoIndex)
|
|
257
|
+
li.className = "wg-pilot-fx-todo-now";
|
|
258
|
+
if (compact && (i < start || i >= end))
|
|
259
|
+
li.classList.add("wg-todo-fold");
|
|
260
|
+
ol.appendChild(li);
|
|
261
|
+
});
|
|
262
|
+
dock.appendChild(ol);
|
|
263
|
+
if (compact && todos.length - (end - start) > 0) {
|
|
264
|
+
const more = document.createElement("div");
|
|
265
|
+
more.className = "wg-todo-more";
|
|
266
|
+
more.textContent = "+" + (todos.length - (end - start)) + " more";
|
|
267
|
+
dock.appendChild(more);
|
|
268
|
+
}
|
|
269
|
+
document.documentElement.appendChild(dock);
|
|
270
|
+
}
|
|
271
|
+
if (focusBox)
|
|
272
|
+
applyFocus(focusBox);
|
|
273
|
+
// Dim todos under rings / focus so captions stay readable.
|
|
274
|
+
const dockEl = document.getElementById("wg-pilot-fx-todos");
|
|
275
|
+
if (dockEl && todoUi.behindRing !== false && (painted > 0 || focusBox)) {
|
|
276
|
+
dockEl.classList.add("wg-todo-behind");
|
|
277
|
+
}
|
|
278
|
+
// Collision flip: if a painted ring intersects the dock, flip side once.
|
|
279
|
+
if (dockEl && todoUi.collision !== false && painted > 0) {
|
|
280
|
+
const firstRing = document.querySelector(".wg-pilot-fx-ring");
|
|
281
|
+
if (firstRing) {
|
|
282
|
+
const rr = firstRing.getBoundingClientRect();
|
|
283
|
+
const dr = dockEl.getBoundingClientRect();
|
|
284
|
+
const hit = !(rr.right < dr.left || rr.left > dr.right || rr.bottom < dr.top || rr.top > dr.bottom);
|
|
285
|
+
if (hit) {
|
|
286
|
+
dockEl.dataset.pos = dockEl.dataset.pos === "right" ? "left" : "right";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (zoom > 1.001 && zoomSelector) {
|
|
291
|
+
const target = document.querySelector(zoomSelector);
|
|
292
|
+
if (target) {
|
|
293
|
+
target.scrollIntoView({
|
|
294
|
+
block: "center",
|
|
295
|
+
inline: "center",
|
|
296
|
+
behavior: "instant",
|
|
297
|
+
});
|
|
298
|
+
// Re-apply focus after scroll so the cutout tracks the new rect.
|
|
299
|
+
if (focusBox) {
|
|
300
|
+
const r = target.getBoundingClientRect();
|
|
301
|
+
applyFocus({ x: r.left, y: r.top, width: r.width, height: r.height });
|
|
302
|
+
}
|
|
303
|
+
setZoomBadge(zoom);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
missing.push(zoomSelector);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else if (zoom > 1.001) {
|
|
310
|
+
setZoomBadge(zoom);
|
|
311
|
+
}
|
|
312
|
+
if (deviceLabel)
|
|
313
|
+
setDeviceChip(deviceLabel);
|
|
314
|
+
if (holdMs > 0 &&
|
|
315
|
+
(painted > 0 || todos.length > 0 || zoom > 1.001 || !!deviceLabel)) {
|
|
316
|
+
w.__wgPilotFxHideTimer = setTimeout(() => clearFx(), holdMs);
|
|
317
|
+
}
|
|
318
|
+
return { painted, missing };
|
|
319
|
+
}, {
|
|
320
|
+
rings,
|
|
321
|
+
todos,
|
|
322
|
+
todoIndex,
|
|
323
|
+
todoTitle,
|
|
324
|
+
todoPos,
|
|
325
|
+
holdMs,
|
|
326
|
+
zoom,
|
|
327
|
+
zoomSelector: zoomSelector ?? "",
|
|
328
|
+
zoomOut,
|
|
329
|
+
clearAll: fixtures.clear === true,
|
|
330
|
+
deviceLabel,
|
|
331
|
+
todoUi,
|
|
332
|
+
})
|
|
333
|
+
.catch(() => ({ painted: 0, missing: rings.map((r) => r.selector) }));
|
|
334
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SessionSnapshot } from "../auto-session.js";
|
|
2
|
+
import type { Page } from "@playwright/test";
|
|
3
|
+
/** Minimal shape needed to render the graph tree - matches WaygraphGraph. */
|
|
4
|
+
export interface PilotOverlayGraph {
|
|
5
|
+
nodes: readonly {
|
|
6
|
+
checkpoint: string;
|
|
7
|
+
}[];
|
|
8
|
+
edges: readonly {
|
|
9
|
+
block: string;
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
kind: "nav" | "action";
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export interface PilotOverlayInfo {
|
|
16
|
+
sessionId: string | undefined;
|
|
17
|
+
snapshot: SessionSnapshot;
|
|
18
|
+
/**
|
|
19
|
+
* The WHOLE project's graph, not just what's reachable from the current
|
|
20
|
+
* page - real, direct user request: "i have a button where it shows all
|
|
21
|
+
* the nodes like a node tree stuff". Optional so a caller that doesn't
|
|
22
|
+
* have it handy (or doesn't want the extra payload every call) can omit
|
|
23
|
+
* it; the "All nodes" tab just shows nothing until one arrives.
|
|
24
|
+
*/
|
|
25
|
+
graph?: PilotOverlayGraph;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Refreshes the badge text and the (possibly-collapsed) panel's two tabs
|
|
29
|
+
* ("Here" - current live menu; "All nodes" - whole project graph as a
|
|
30
|
+
* tree). Never throws - a page mid-navigation when this fires is a real,
|
|
31
|
+
* expected timing case, not an error worth failing the caller's own real
|
|
32
|
+
* operation over.
|
|
33
|
+
*/
|
|
34
|
+
export declare function updatePilotOverlay(page: Page, info: PilotOverlayInfo): Promise<void>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { ensureInstalled } from "./shell.js";
|
|
2
|
+
/**
|
|
3
|
+
* Refreshes the badge text and the (possibly-collapsed) panel's two tabs
|
|
4
|
+
* ("Here" - current live menu; "All nodes" - whole project graph as a
|
|
5
|
+
* tree). Never throws - a page mid-navigation when this fires is a real,
|
|
6
|
+
* expected timing case, not an error worth failing the caller's own real
|
|
7
|
+
* operation over.
|
|
8
|
+
*/
|
|
9
|
+
export async function updatePilotOverlay(page, info) {
|
|
10
|
+
await ensureInstalled(page);
|
|
11
|
+
await page
|
|
12
|
+
.evaluate(async ({ sessionId, snapshot, graph }) => {
|
|
13
|
+
const badge = document.getElementById("wg-pilot-badge");
|
|
14
|
+
const current = document.getElementById("wg-pilot-panel-current");
|
|
15
|
+
const graphPanel = document.getElementById("wg-pilot-panel-graph");
|
|
16
|
+
if (!badge || !current || !graphPanel)
|
|
17
|
+
return;
|
|
18
|
+
const idPart = sessionId ? `session ${sessionId}` : "no session id";
|
|
19
|
+
const onBlank = location.href === "about:blank" || location.protocol === "about:";
|
|
20
|
+
const hereLabel = snapshot.here ?? (onBlank ? "(blank page)" : "(unknown)");
|
|
21
|
+
badge.textContent = `Waygraph Pilot - ${idPart} - ${hereLabel}`;
|
|
22
|
+
const totalEdges = snapshot.sections.reduce((n, s) => n + s.edges.length, 0);
|
|
23
|
+
if (totalEdges === 0) {
|
|
24
|
+
current.innerHTML = onBlank
|
|
25
|
+
? '<div class="wg-pilot-empty">Blank page — no Block moves yet. Use auto goto or a nav Block when ready.</div>'
|
|
26
|
+
: '<div class="wg-pilot-empty">No Block moves from here right now.</div>';
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
current.innerHTML = snapshot.sections
|
|
30
|
+
.map((section) => {
|
|
31
|
+
const rows = section.edges
|
|
32
|
+
.map((edge) => {
|
|
33
|
+
const desc = edge.description ? ` - ${edge.description}` : "";
|
|
34
|
+
const label = edge.label ? ` (${edge.label})` : "";
|
|
35
|
+
return `<div class="wg-pilot-edge">[${edge.index}] <span class="wg-pilot-kind">${edge.kind}</span> ${edge.block}${label} -> ${edge.to}${desc}</div>`;
|
|
36
|
+
})
|
|
37
|
+
.join("");
|
|
38
|
+
return `<div class="wg-pilot-section">${section.title}</div>${rows}`;
|
|
39
|
+
})
|
|
40
|
+
.join("");
|
|
41
|
+
}
|
|
42
|
+
if (!graph || graph.nodes.length === 0) {
|
|
43
|
+
graphPanel.innerHTML = '<div class="wg-pilot-empty">No graph data yet.</div>';
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Flat indented-text tree - the ORIGINAL "All nodes" rendering.
|
|
47
|
+
// Real, direct user correction: "i also dont like the all node
|
|
48
|
+
// stuff. ther should be a 2d version" - kept only as a fallback for
|
|
49
|
+
// when Mermaid genuinely can't load (e.g. offline), not the primary
|
|
50
|
+
// rendering anymore.
|
|
51
|
+
const renderTextTree = () => {
|
|
52
|
+
graphPanel.innerHTML = graph.nodes
|
|
53
|
+
.map((node) => {
|
|
54
|
+
const outgoing = graph.edges.filter((e) => e.from === node.checkpoint || e.from === "*");
|
|
55
|
+
const rows = outgoing.length
|
|
56
|
+
? outgoing
|
|
57
|
+
.map((e) => `<div class="wg-pilot-graph-edge"><span class="wg-pilot-kind">${e.kind}</span> ${e.block} -> ${e.to}</div>`)
|
|
58
|
+
.join("")
|
|
59
|
+
: '<div class="wg-pilot-graph-edge wg-pilot-empty">(no outgoing edges)</div>';
|
|
60
|
+
const here = node.checkpoint === snapshot.here ? " (here)" : "";
|
|
61
|
+
return `<div class="wg-pilot-graph-node"><div class="wg-pilot-graph-checkpoint">${node.checkpoint}${here}</div>${rows}</div>`;
|
|
62
|
+
})
|
|
63
|
+
.join("");
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
const w = window;
|
|
67
|
+
if (!w.mermaid) {
|
|
68
|
+
if (!w.__wgMermaidLoadPromise) {
|
|
69
|
+
w.__wgMermaidLoadPromise = new Promise((resolve, reject) => {
|
|
70
|
+
const script = document.createElement("script");
|
|
71
|
+
script.src = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js";
|
|
72
|
+
script.onload = () => resolve(undefined);
|
|
73
|
+
script.onerror = () => reject(new Error("mermaid script failed to load"));
|
|
74
|
+
document.head.appendChild(script);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
await w.__wgMermaidLoadPromise;
|
|
78
|
+
}
|
|
79
|
+
if (!w.mermaid)
|
|
80
|
+
throw new Error("mermaid did not attach to window after load");
|
|
81
|
+
// Real, direct user request: "left to right stuff. maybe mermaid?
|
|
82
|
+
// with square ish arrows???" - LR layout, linear (non-curvy) edges.
|
|
83
|
+
w.mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "loose", flowchart: { curve: "linear" } });
|
|
84
|
+
const sanitize = (s) => s.replace(/[^A-Za-z0-9_]/g, "_");
|
|
85
|
+
// Every Nav Block gets a `from:"*"` wildcard edge for pathfinding
|
|
86
|
+
// (see auto-explore.ts's own isUrlNav comment). Real, screenshot-
|
|
87
|
+
// confirmed finding, TWICE over:
|
|
88
|
+
// 1. Collapsing all of these into one shared "Anywhere" hub node
|
|
89
|
+
// still broke the layout - ~9 of 10 Checkpoints have their own
|
|
90
|
+
// nav edge, so nearly every node ends up one hop from that hub
|
|
91
|
+
// and dagre stacks them in a single rank/column ("it dones't
|
|
92
|
+
// look left to right").
|
|
93
|
+
// 2. Dropping them entirely (no edges, just a footnote) fixed the
|
|
94
|
+
// layout but then hid real connectivity - a node like Settings
|
|
95
|
+
// that has ONLY a nav edge looked like a disconnected orphan
|
|
96
|
+
// ("the all nodes doesn't even show the navs block... whre is
|
|
97
|
+
// settings there").
|
|
98
|
+
// Neither extreme is right: a wildcard nav edge isn't really a
|
|
99
|
+
// flow TRANSITION (it doesn't come from any one specific screen,
|
|
100
|
+
// it's global infrastructure - the persistent sidebar), so it
|
|
101
|
+
// doesn't belong in the layout-driving edge set at all, but the
|
|
102
|
+
// fact that a node is directly nav-reachable is still real
|
|
103
|
+
// information worth showing. Marked on the node's own label
|
|
104
|
+
// instead of as a graph edge - visible on every affected node,
|
|
105
|
+
// adds zero layout-warping connectivity.
|
|
106
|
+
const nonWildcard = graph.edges.filter((e) => e.from !== "*");
|
|
107
|
+
const navReachable = new Set(graph.edges.filter((e) => e.from === "*").map((e) => e.to));
|
|
108
|
+
// Real, direct user finding (screenshot-confirmed): a self-loop
|
|
109
|
+
// edge (an action that stays on the same Checkpoint, e.g.
|
|
110
|
+
// "fill-email -> SignIn") renders as a spiral squiggle Mermaid
|
|
111
|
+
// draws back into the same node - with several per node (every
|
|
112
|
+
// fill-* / select-* action), these dominated the diagram and
|
|
113
|
+
// broke the left-to-right layout dagre otherwise produces cleanly
|
|
114
|
+
// for a real DAG of inter-Checkpoint transitions. Self-loops add
|
|
115
|
+
// no flow information anyway (the "Here" tab already lists every
|
|
116
|
+
// action from wherever you are) - excluded from the graph itself,
|
|
117
|
+
// kept only as a "(+N)" count on the node label so the count
|
|
118
|
+
// isn't silently lost.
|
|
119
|
+
const selfLoopCounts = new Map();
|
|
120
|
+
for (const e of nonWildcard) {
|
|
121
|
+
if (e.from === e.to)
|
|
122
|
+
selfLoopCounts.set(e.from, (selfLoopCounts.get(e.from) ?? 0) + 1);
|
|
123
|
+
}
|
|
124
|
+
const specific = nonWildcard.filter((e) => e.from !== e.to);
|
|
125
|
+
const lines = ["graph LR"];
|
|
126
|
+
for (const node of graph.nodes) {
|
|
127
|
+
const id = sanitize(node.checkpoint);
|
|
128
|
+
const here = node.checkpoint === snapshot.here;
|
|
129
|
+
const loops = selfLoopCounts.get(node.checkpoint);
|
|
130
|
+
const nav = navReachable.has(node.checkpoint) ? " [nav]" : "";
|
|
131
|
+
const suffix = `${here ? " (here)" : ""}${loops ? ` (+${loops})` : ""}${nav}`;
|
|
132
|
+
lines.push(` ${id}["${node.checkpoint}${suffix}"]`);
|
|
133
|
+
if (here)
|
|
134
|
+
lines.push(` style ${id} stroke:#7C3AED,stroke-width:3px`);
|
|
135
|
+
}
|
|
136
|
+
for (const e of specific) {
|
|
137
|
+
lines.push(` ${sanitize(e.from)} -->|${e.block}| ${sanitize(e.to)}`);
|
|
138
|
+
}
|
|
139
|
+
const { svg } = await w.mermaid.render(`wg-mermaid-${Date.now()}`, lines.join("\n"));
|
|
140
|
+
graphPanel.innerHTML =
|
|
141
|
+
svg + `<div class="wg-pilot-graph-note">[nav] = also directly reachable via sidebar/URL from anywhere (not drawn as an edge - see the "Here" tab for what's live on this page).</div>`;
|
|
142
|
+
// Real, direct user request: "i hope it is zoomed to the current
|
|
143
|
+
// node" - the diagram is usually bigger than the panel, so scroll
|
|
144
|
+
// (not zoom - the SVG itself stays real size, legible) the "here"
|
|
145
|
+
// node into view instead of leaving it wherever dagre placed it.
|
|
146
|
+
const hereId = snapshot.here ? sanitize(snapshot.here) : null;
|
|
147
|
+
const hereEl = hereId ? graphPanel.querySelector(`[id^="flowchart-${hereId}-"]`) : null;
|
|
148
|
+
hereEl?.scrollIntoView({ block: "center", inline: "center" });
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
renderTextTree();
|
|
152
|
+
}
|
|
153
|
+
}, { sessionId: info.sessionId, snapshot: info.snapshot, graph: info.graph ?? null })
|
|
154
|
+
.catch(() => { });
|
|
155
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { HighlightTone } from "../highlights.js";
|
|
2
|
+
import type { BrowserContext, Page } from "@playwright/test";
|
|
3
|
+
/**
|
|
4
|
+
* Re-install the overlay shell on every navigation (about:blank, goto, etc.)
|
|
5
|
+
* so the badge/panel persist outside the app's own DOM — not lost when the
|
|
6
|
+
* page is blank or replaced.
|
|
7
|
+
*/
|
|
8
|
+
export declare function installPersistentPilotOverlay(context: BrowserContext): Promise<void>;
|
|
9
|
+
/** Idempotent - safe to call before every update, matching narrate mode's own precedent. */
|
|
10
|
+
export declare function ensureInstalled(page: Page): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Bottom-left transient toast, separate from the persistent bottom-right
|
|
13
|
+
* badge - real, direct user request: something that visibly flags "a
|
|
14
|
+
* command is running" / "the map just changed" distinct from the
|
|
15
|
+
* always-on Checkpoint badge. Self-clearing (CSS fade after a timeout),
|
|
16
|
+
* not tied to the next `currentMenu()` snapshot the way the badge is,
|
|
17
|
+
* since some callers (rawClick/rawType/rawGoto/reloadLibrary/applyPick)
|
|
18
|
+
* only call `currentMenu()` again once the whole operation - including
|
|
19
|
+
* this toast's own lifetime - should already be over.
|
|
20
|
+
*/
|
|
21
|
+
export declare function showPilotActivity(page: Page, text: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* "browser-use"-style vision box: a ring drawn around the exact element
|
|
24
|
+
* `auto dom --selector <sel>` just inspected, so a human watching a headful
|
|
25
|
+
* Pilot session can see what the agent is actually looking at, not just a
|
|
26
|
+
* JSON blob in a terminal. Real, direct user request.
|
|
27
|
+
*
|
|
28
|
+
* Reuses the real `#wg-ring`/`#wg-ring-label` elements + `WAYGRAPH_RING_CSS`
|
|
29
|
+
* (see `highlights.ts`) - the exact same visual language `cli.ts`'s own demo
|
|
30
|
+
* stepper paints rings with, not a separate look-alike. Still a purpose-built
|
|
31
|
+
* renderer here (position/show/hide only, via plain `getBoundingClientRect` -
|
|
32
|
+
* no camera zoom/spotlight/todo-dock/pacing), since that whole engine still
|
|
33
|
+
* lives in `cli.ts`, which this file continues to avoid importing (see this
|
|
34
|
+
* file's own header). Auto-fades a few real seconds after showing (or
|
|
35
|
+
* sooner if explicitly replaced/hidden first) - tuned live twice: an
|
|
36
|
+
* original 2.5s fade read as gone before it could be seen, a since-tried
|
|
37
|
+
* "stay up indefinitely" alternative read as lingering clutter instead.
|
|
38
|
+
*
|
|
39
|
+
* `tone` defaults to "auto" (gray) - real, direct user request: "gray is
|
|
40
|
+
* waygraph playwright run. orange is dom check" - a real Block run or a
|
|
41
|
+
* raw click/type/upload IS a real Playwright-driven action (`auto` was
|
|
42
|
+
* always documented as meaning exactly that), while a pure `auto dom` read
|
|
43
|
+
* touches nothing, so it gets its own "orange" tone instead.
|
|
44
|
+
*
|
|
45
|
+
* For agent-authored multi-ring / todo narration, use {@link showPilotFixtures}
|
|
46
|
+
* (`auto highlight`) instead - this vision ring is one-shot inspect feedback.
|
|
47
|
+
*/
|
|
48
|
+
export declare function showPilotVision(page: Page, selector: string, label: string, tone?: HighlightTone): Promise<void>;
|