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.
@@ -1,778 +1,4 @@
1
- /**
2
- * A small, self-contained on-page indicator for a live AutoSession -
3
- * "this browser is being driven by Waygraph," plus an expandable panel
4
- * showing the session's own current live menu. Real, direct user request:
5
- * a headful Pilot session looks like an ordinary browser tab with no
6
- * indication anything is driving it, which matters once a human might be
7
- * co-driving the same visible session (see `AutoSession.resync()`'s own
8
- * doc comment for the related state-desync problem that scenario causes).
9
- *
10
- * Deliberately NOT built on `src/cli.ts`'s own demo-panel/banner overlay
11
- * system, for the same reason `pilot-v1`'s narrate-mode ring renderer
12
- * wasn't either (see the git history at tag
13
- * `waygraph-pilot-v1-logs-prettified`): `cli.ts` runs `main().catch(...)`
14
- * unconditionally at module load with no `import.meta.url` guard, so
15
- * importing anything from it here would trigger the whole CLI's argument
16
- * dispatch as a side effect of loading a library module - unsafe. This
17
- * ships its own small, purpose-built badge + panel instead.
18
- */
19
- import { pilotFxRingCss } from "./ui/components/ring.js";
20
- import { SURFACE } from "./ui/tokens.js";
21
- import { WAYGRAPH_RING_CSS, normalizeHighlightTone, normalizeHighlightSize, normalizeHighlightWeight, formatHighlightCaption, resolveDeviceState, resolveTodoDockUi, } from "./highlights.js";
22
- // Real, direct user request: this overlay's own colors were an improvised
23
- // purple/white scheme (#a78bfa / #1a1033) that didn't match anything else -
24
- // "our design here seems like barely shares with the ui on the stepper
25
- // design... looks a bit different and out of sync". These are the SAME
26
- // colors `cli.ts`'s own demo panel/ring already use (#7C3AED primary
27
- // purple, the dark rgba(20,10,40,.94) panel background + white text,
28
- // #c9a6ff for a secondary purple accent on that dark background - all
29
- // copied verbatim from cli.ts's own `#wg-panel`/`RING_CSS`, not
30
- // re-invented) - genuinely the same visual language, not just the ring CSS
31
- // this file already shared via WAYGRAPH_RING_CSS.
32
- const OVERLAY_CSS = `
33
- #wg-pilot-overlay {
34
- position: fixed; z-index: 2147483000; bottom: 12px; right: 12px;
35
- font: 12px/1.4 ui-monospace, "SF Mono", Consolas, monospace;
36
- color: #fff; pointer-events: auto;
37
- }
38
- #wg-pilot-badge {
39
- background: #7C3AED; color: #fff; padding: 6px 10px; border-radius: 8px;
40
- box-shadow: 0 2px 8px rgba(0,0,0,0.25); cursor: pointer; white-space: nowrap;
41
- font-weight: 600; user-select: none;
42
- }
43
- #wg-pilot-panel {
44
- display: none; margin-top: 6px; max-width: 420px; max-height: 320px;
45
- overflow-y: auto; background: ${SURFACE}; color: #fff; border-radius: 8px;
46
- box-shadow: 0 2px 8px rgba(0,0,0,0.25); padding: 8px 10px;
47
- }
48
- /* The graph tab renders a real Mermaid flowchart, which needs more room than
49
- the flat-list "Here" tab and its own scroll - real, direct user request:
50
- "a 2d version. left to right stuff. maybe mermaid?" replacing the original
51
- flat indented-text tree. Widened+its own scrollbox rather than growing
52
- the shared panel unconditionally, so "Here" stays compact. */
53
- #wg-pilot-panel:has(#wg-pilot-panel-graph:not([style*="display: none"])) {
54
- max-width: 640px; max-height: 460px;
55
- }
56
- #wg-pilot-panel-graph { overflow: auto; }
57
- #wg-pilot-panel-graph svg { max-width: none; }
58
- #wg-pilot-panel-graph .wg-pilot-graph-note { color: #999; font-style: italic; font-size: 11px; padding: 4px 2px 0; }
59
- #wg-pilot-panel.wg-pilot-open { display: block; }
60
- #wg-pilot-panel-toggle { display: flex; gap: 6px; margin-bottom: 8px; }
61
- #wg-pilot-panel-toggle button {
62
- background: transparent; color: #bbb; border: 1px solid rgba(255,255,255,.2);
63
- border-radius: 6px; padding: 3px 8px; font: inherit; cursor: pointer;
64
- }
65
- #wg-pilot-panel-toggle button.wg-pilot-tab-active { background: #7C3AED; color: #fff; border-color: #7C3AED; }
66
- #wg-pilot-panel .wg-pilot-section { font-weight: 700; margin: 6px 0 2px; color: #c9a6ff; }
67
- #wg-pilot-panel .wg-pilot-edge { padding: 2px 0; border-bottom: 1px solid rgba(255,255,255,.12); }
68
- #wg-pilot-panel .wg-pilot-kind { color: #c9a6ff; font-weight: 600; }
69
- #wg-pilot-panel .wg-pilot-desc { color: #bbb; }
70
- #wg-pilot-panel .wg-pilot-empty { color: #999; font-style: italic; }
71
- #wg-pilot-panel-graph .wg-pilot-graph-node { margin: 6px 0; }
72
- #wg-pilot-panel-graph .wg-pilot-graph-checkpoint { color: #c9a6ff; font-weight: 700; }
73
- #wg-pilot-panel-graph .wg-pilot-graph-edge { padding: 2px 0 2px 14px; border-left: 2px solid rgba(255,255,255,.15); font-size: 11px; }
74
- #wg-pilot-panel-graph .wg-pilot-graph-edge .wg-pilot-kind { color: #c9a6ff; }
75
- #wg-pilot-activity {
76
- position: fixed; z-index: 2147483000; bottom: 12px; left: 12px;
77
- font: 12px/1.4 ui-monospace, "SF Mono", Consolas, monospace;
78
- background: ${SURFACE}; color: #fff; padding: 6px 10px; border-radius: 8px;
79
- box-shadow: 0 2px 8px rgba(0,0,0,0.25); white-space: nowrap; pointer-events: none;
80
- opacity: 0; transform: translateY(4px); transition: opacity 0.15s, transform 0.15s;
81
- }
82
- #wg-pilot-activity.wg-pilot-toast-show { opacity: 1; transform: translateY(0); }
83
- /* Agent-sent fixture rings: generated from ui/tokens (tones shared with #wg-ring). */
84
- ${pilotFxRingCss()}
85
- #wg-pilot-fx-todos{
86
- position:fixed;z-index:2147483645;top:72px;max-width:280px;max-height:calc(100vh - 100px);overflow:auto;
87
- font:12px/1.4 ui-sans-serif,system-ui,sans-serif;color:#fff;
88
- background:${SURFACE};border-radius:10px;padding:10px 12px;
89
- box-shadow:0 2px 10px rgba(0,0,0,.3);pointer-events:auto;
90
- transition:opacity .25s ease;
91
- }
92
- #wg-pilot-fx-todos.wg-todo-behind{z-index:2147483643;opacity:.42;pointer-events:none;}
93
- #wg-pilot-fx-todos[data-pos=left]{left:14px;right:auto;}
94
- #wg-pilot-fx-todos[data-pos=right]{right:14px;left:auto;}
95
- #wg-pilot-fx-todos .wg-pilot-fx-todo-title{font-weight:700;color:#c9a6ff;margin-bottom:6px;}
96
- #wg-pilot-fx-todos ol{margin:0;padding-left:18px;}
97
- #wg-pilot-fx-todos li{margin:3px 0;color:#ddd;}
98
- #wg-pilot-fx-todos li.wg-pilot-fx-todo-done{color:#888;text-decoration:line-through;}
99
- #wg-pilot-fx-todos li.wg-pilot-fx-todo-now{color:#fff;font-weight:700;}
100
- #wg-pilot-fx-todos[data-compact="1"]:not(:hover) li.wg-todo-fold{display:none;}
101
- #wg-pilot-fx-todos .wg-todo-more{margin-top:6px;padding:4px 8px;border-radius:8px;
102
- background:rgba(124,58,237,.22);font:700 11px/1.2 system-ui,sans-serif;color:#e8dcff;}
103
- /* Spotlight (demo focus:true) - dim page, cutout around target. */
104
- #wg-pilot-fx-focus{position:fixed;z-index:2147483644;pointer-events:none;
105
- border-radius:12px;box-shadow:0 0 0 9999px rgba(8,4,20,.62);
106
- opacity:0;transition:opacity .3s ease,left .3s ease,top .3s ease,width .3s ease,height .3s ease;}
107
- #wg-pilot-fx-focus.wg-in{opacity:1;}
108
- /* Zoom HUD (demo parity - badge only; no CSS scale of the page). */
109
- #wg-pilot-fx-zoom{position:fixed;z-index:2147483646;bottom:14px;right:14px;
110
- display:flex;align-items:center;gap:7px;padding:6px 11px 6px 8px;
111
- border-radius:999px;background:${SURFACE};color:#f0e8ff;
112
- border:1px solid rgba(250,204,21,.6);box-shadow:0 6px 18px rgba(0,0,0,.4);
113
- font:700 12px/1.2 system-ui,sans-serif;pointer-events:none;}
114
- #wg-pilot-fx-zoom .wg-pilot-fx-zoom-ico{width:18px;height:18px;display:flex;align-items:center;justify-content:center;
115
- border-radius:6px;background:rgba(250,204,21,.22);}
116
- #wg-pilot-fx-zoom .wg-pilot-fx-zoom-ico svg{width:14px;height:14px;display:block;}
117
- #wg-pilot-fx-zoom .wg-pilot-fx-zoom-val{color:#fde68a;font-variant-numeric:tabular-nums;font-weight:800;min-width:3.2em;}
118
- #wg-pilot-fx-zoom[data-zoomed="1"]{border-color:#fbbf24;}
119
- #wg-pilot-fx-device{position:fixed;z-index:2147483646;top:14px;right:14px;
120
- padding:8px 12px;border-radius:10px;background:${SURFACE};color:#f0e8ff;
121
- border:1px solid rgba(124,58,237,.55);font:700 12px/1.2 system-ui,sans-serif;
122
- pointer-events:none;box-shadow:0 6px 18px rgba(0,0,0,.35);}
123
- #wg-pilot-fx-device[data-preset=mobile]{border-color:#3B82F6;}
124
- #wg-pilot-fx-device[data-preset=tablet]{border-color:#22C55E;}
125
- #wg-pilot-fx-device[data-preset=desktop]{border-color:#9CA3AF;}
126
- ` + WAYGRAPH_RING_CSS;
127
- /** Browser-side shell install — runs on every document via context.addInitScript. */
128
- function installPilotOverlayShell(css) {
129
- if (document.getElementById("wg-pilot-overlay"))
130
- return;
131
- const style = document.createElement("style");
132
- style.id = "wg-pilot-overlay-css";
133
- style.textContent = css;
134
- document.documentElement.appendChild(style);
135
- const root = document.createElement("div");
136
- root.id = "wg-pilot-overlay";
137
- root.innerHTML =
138
- '<div id="wg-pilot-badge">Waygraph Pilot</div>' +
139
- '<div id="wg-pilot-panel">' +
140
- '<div id="wg-pilot-panel-toggle"><button id="wg-pilot-view-current" class="wg-pilot-tab-active">Here</button>' +
141
- '<button id="wg-pilot-view-graph">All nodes</button></div>' +
142
- '<div id="wg-pilot-panel-current"></div>' +
143
- '<div id="wg-pilot-panel-graph" style="display:none"></div>' +
144
- "</div>";
145
- document.documentElement.appendChild(root);
146
- document.getElementById("wg-pilot-badge").addEventListener("click", () => {
147
- document.getElementById("wg-pilot-panel").classList.toggle("wg-pilot-open");
148
- });
149
- document.getElementById("wg-pilot-view-current").addEventListener("click", () => {
150
- document.getElementById("wg-pilot-view-current").classList.add("wg-pilot-tab-active");
151
- document.getElementById("wg-pilot-view-graph").classList.remove("wg-pilot-tab-active");
152
- document.getElementById("wg-pilot-panel-current").style.display = "";
153
- document.getElementById("wg-pilot-panel-graph").style.display = "none";
154
- });
155
- document.getElementById("wg-pilot-view-graph").addEventListener("click", () => {
156
- document.getElementById("wg-pilot-view-graph").classList.add("wg-pilot-tab-active");
157
- document.getElementById("wg-pilot-view-current").classList.remove("wg-pilot-tab-active");
158
- document.getElementById("wg-pilot-panel-graph").style.display = "";
159
- document.getElementById("wg-pilot-panel-current").style.display = "none";
160
- });
161
- if (!document.getElementById("wg-pilot-activity")) {
162
- const toast = document.createElement("div");
163
- toast.id = "wg-pilot-activity";
164
- document.documentElement.appendChild(toast);
165
- }
166
- if (!document.getElementById("wg-ring")) {
167
- const ring = document.createElement("div");
168
- ring.id = "wg-ring";
169
- document.documentElement.appendChild(ring);
170
- }
171
- if (!document.getElementById("wg-ring-label")) {
172
- const ringLabel = document.createElement("div");
173
- ringLabel.id = "wg-ring-label";
174
- document.documentElement.appendChild(ringLabel);
175
- }
176
- }
177
- /**
178
- * Re-install the overlay shell on every navigation (about:blank, goto, etc.)
179
- * so the badge/panel persist outside the app's own DOM — not lost when the
180
- * page is blank or replaced.
181
- */
182
- export async function installPersistentPilotOverlay(context) {
183
- await context.addInitScript(installPilotOverlayShell, OVERLAY_CSS);
184
- }
185
- /** Idempotent - safe to call before every update, matching narrate mode's own precedent. */
186
- async function ensureInstalled(page) {
187
- await page.evaluate(installPilotOverlayShell, OVERLAY_CSS).catch(() => { });
188
- }
189
- /**
190
- * Bottom-left transient toast, separate from the persistent bottom-right
191
- * badge - real, direct user request: something that visibly flags "a
192
- * command is running" / "the map just changed" distinct from the
193
- * always-on Checkpoint badge. Self-clearing (CSS fade after a timeout),
194
- * not tied to the next `currentMenu()` snapshot the way the badge is,
195
- * since some callers (rawClick/rawType/rawGoto/reloadLibrary/applyPick)
196
- * only call `currentMenu()` again once the whole operation - including
197
- * this toast's own lifetime - should already be over.
198
- */
199
- export async function showPilotActivity(page, text) {
200
- await ensureInstalled(page);
201
- await page
202
- .evaluate((text) => {
203
- const toast = document.getElementById("wg-pilot-activity");
204
- if (!toast)
205
- return;
206
- toast.textContent = text;
207
- toast.classList.add("wg-pilot-toast-show");
208
- const w = window;
209
- if (w.__wgPilotToastTimer)
210
- clearTimeout(w.__wgPilotToastTimer);
211
- w.__wgPilotToastTimer = setTimeout(() => {
212
- toast.classList.remove("wg-pilot-toast-show");
213
- }, 2500);
214
- }, text)
215
- .catch(() => { });
216
- }
217
- /**
218
- * "browser-use"-style vision box: a ring drawn around the exact element
219
- * `auto dom --selector <sel>` just inspected, so a human watching a headful
220
- * Pilot session can see what the agent is actually looking at, not just a
221
- * JSON blob in a terminal. Real, direct user request.
222
- *
223
- * Reuses the real `#wg-ring`/`#wg-ring-label` elements + `WAYGRAPH_RING_CSS`
224
- * (see `highlights.ts`) - the exact same visual language `cli.ts`'s own demo
225
- * stepper paints rings with, not a separate look-alike. Still a purpose-built
226
- * renderer here (position/show/hide only, via plain `getBoundingClientRect` -
227
- * no camera zoom/spotlight/todo-dock/pacing), since that whole engine still
228
- * lives in `cli.ts`, which this file continues to avoid importing (see this
229
- * file's own header). Auto-fades a few real seconds after showing (or
230
- * sooner if explicitly replaced/hidden first) - tuned live twice: an
231
- * original 2.5s fade read as gone before it could be seen, a since-tried
232
- * "stay up indefinitely" alternative read as lingering clutter instead.
233
- *
234
- * `tone` defaults to "auto" (gray) - real, direct user request: "gray is
235
- * waygraph playwright run. orange is dom check" - a real Block run or a
236
- * raw click/type/upload IS a real Playwright-driven action (`auto` was
237
- * always documented as meaning exactly that), while a pure `auto dom` read
238
- * touches nothing, so it gets its own "orange" tone instead.
239
- *
240
- * For agent-authored multi-ring / todo narration, use {@link showPilotFixtures}
241
- * (`auto highlight`) instead - this vision ring is one-shot inspect feedback.
242
- */
243
- export async function showPilotVision(page, selector, label, tone = "auto") {
244
- await ensureInstalled(page);
245
- await page
246
- .evaluate(({ selector, label, tone }) => {
247
- const ring = document.getElementById("wg-ring");
248
- const tag = document.getElementById("wg-ring-label");
249
- if (!ring || !tag)
250
- return;
251
- const w = window;
252
- if (w.__wgVisionHideTimer)
253
- clearTimeout(w.__wgVisionHideTimer);
254
- const el = document.querySelector(selector);
255
- if (!el) {
256
- ring.style.opacity = "0";
257
- tag.style.opacity = "0";
258
- return;
259
- }
260
- const rect = el.getBoundingClientRect();
261
- if (rect.width === 0 && rect.height === 0) {
262
- ring.style.opacity = "0";
263
- tag.style.opacity = "0";
264
- return;
265
- }
266
- ring.dataset.tone = tone;
267
- ring.dataset.size = "md";
268
- tag.dataset.tone = tone;
269
- tag.dataset.size = "md";
270
- tag.dataset.weight = "normal";
271
- ring.style.left = `${rect.left - 3}px`;
272
- ring.style.top = `${rect.top - 3}px`;
273
- ring.style.width = `${rect.width + 6}px`;
274
- ring.style.height = `${rect.height + 6}px`;
275
- tag.textContent = label;
276
- tag.style.left = `${Math.max(0, rect.left)}px`;
277
- tag.style.top = `${Math.max(0, rect.top - 24)}px`;
278
- ring.style.opacity = "1";
279
- tag.style.opacity = "1";
280
- // Real, direct user correction: this used to be a 30s "long
281
- // fallback" (stay up until explicitly replaced) - after actually
282
- // watching it, the user wants it to auto-clear after a few real
283
- // seconds instead, not linger. Short enough to still read as "just
284
- // happened", not so short it's gone before you look (that was the
285
- // ORIGINAL complaint this session, at 2.5s - this is deliberately
286
- // in between).
287
- w.__wgVisionHideTimer = setTimeout(() => {
288
- ring.style.opacity = "0";
289
- tag.style.opacity = "0";
290
- }, 4_000);
291
- }, { selector, label, tone })
292
- .catch(() => { });
293
- }
294
- /**
295
- * Paint (or clear) agent-authored highlight fixtures on the live page.
296
- * Multi-ring + optional todo strip + zoom/focus/device - the Pilot equivalent
297
- * of Block `stubBefore` narration in `waygraph demo`.
298
- * Does not import `cli.ts` (see file header).
299
- */
300
- export async function showPilotFixtures(page, fixtures) {
301
- await ensureInstalled(page);
302
- const holdMs = fixtures.clear === true
303
- ? 0
304
- : fixtures.holdMs === undefined
305
- ? 30_000
306
- : Math.max(0, fixtures.holdMs);
307
- const rings = (fixtures.clear ? [] : (fixtures.rings ?? [])).map((r) => {
308
- const tone = normalizeHighlightTone(r.tone);
309
- const size = normalizeHighlightSize(r.size);
310
- const weight = normalizeHighlightWeight(r.weight);
311
- const caption = { label: r.label, tone };
312
- if (r.detail)
313
- caption.detail = r.detail;
314
- if (r.tag)
315
- caption.tag = r.tag;
316
- const label = formatHighlightCaption(caption);
317
- return {
318
- selector: r.selector,
319
- label,
320
- tone,
321
- size,
322
- weight,
323
- color: typeof r.color === "string" && r.color.trim() ? r.color.trim() : "",
324
- zoom: typeof r.zoom === "number" && Number.isFinite(r.zoom) && r.zoom > 0
325
- ? r.zoom
326
- : undefined,
327
- zoomOut: r.zoomOut,
328
- focus: r.focus === true,
329
- };
330
- });
331
- const todos = fixtures.clear ? [] : (fixtures.todos ?? []);
332
- const todoIndex = fixtures.todoIndex ?? 0;
333
- const todoTitle = fixtures.todoTitle ?? "Plan";
334
- const todoPos = fixtures.todoPos === "left" ? "left" : "right";
335
- const todoUi = resolveTodoDockUi(fixtures.todoUi ?? null);
336
- // Effective zoom: phase zoom, else last ring that authored zoom.
337
- let zoom = fixtures.clear === true
338
- ? 1
339
- : typeof fixtures.zoom === "number" && Number.isFinite(fixtures.zoom)
340
- ? fixtures.zoom
341
- : 1;
342
- let zoomSelector = fixtures.zoomSelector ??
343
- (rings.length > 0 ? rings[rings.length - 1].selector : undefined);
344
- let zoomOut = fixtures.zoomOut !== false;
345
- if (!fixtures.clear && zoom <= 1.001) {
346
- for (let i = rings.length - 1; i >= 0; i--) {
347
- const r = rings[i];
348
- if (r.zoom !== undefined && r.zoom > 1.001) {
349
- zoom = r.zoom;
350
- zoomSelector = r.selector;
351
- if (r.zoomOut !== undefined)
352
- zoomOut = r.zoomOut !== false;
353
- break;
354
- }
355
- }
356
- }
357
- else if (!fixtures.clear) {
358
- // Phase zoomOut can still be overridden by the targeted ring.
359
- const hit = rings.find((r) => r.selector === zoomSelector);
360
- if (hit?.zoomOut !== undefined)
361
- zoomOut = hit.zoomOut !== false;
362
- }
363
- // Device viewport (Node-side) before paint so rings land on the new size.
364
- if (fixtures.clear === true) {
365
- // Leave viewport alone on clear - only drop overlays.
366
- }
367
- else if (fixtures.device !== undefined) {
368
- const d = resolveDeviceState(fixtures.device);
369
- if (d) {
370
- await page
371
- .setViewportSize({
372
- width: Math.max(200, Math.floor(d.viewport.width)),
373
- height: Math.max(200, Math.floor(d.viewport.height)),
374
- })
375
- .catch(() => { });
376
- }
377
- }
378
- const deviceLabel = fixtures.clear === true
379
- ? ""
380
- : fixtures.device === undefined
381
- ? ""
382
- : typeof fixtures.device === "string"
383
- ? fixtures.device
384
- : fixtures.device.preset || "device";
385
- return page
386
- .evaluate(({ rings, todos, todoIndex, todoTitle, todoPos, holdMs, zoom, zoomSelector, zoomOut, clearAll, deviceLabel, todoUi, }) => {
387
- const w = window;
388
- if (w.__wgPilotFxHideTimer)
389
- clearTimeout(w.__wgPilotFxHideTimer);
390
- const clearFocus = () => {
391
- const veil = document.getElementById("wg-pilot-fx-focus");
392
- if (!veil)
393
- return;
394
- veil.classList.remove("wg-in");
395
- setTimeout(() => {
396
- const v = document.getElementById("wg-pilot-fx-focus");
397
- if (v && !v.classList.contains("wg-in"))
398
- v.remove();
399
- }, 320);
400
- };
401
- const applyFocus = (box) => {
402
- let veil = document.getElementById("wg-pilot-fx-focus");
403
- if (!veil) {
404
- veil = document.createElement("div");
405
- veil.id = "wg-pilot-fx-focus";
406
- document.documentElement.appendChild(veil);
407
- }
408
- const pad = 10;
409
- veil.style.left = `${Math.max(0, box.x - pad)}px`;
410
- veil.style.top = `${Math.max(0, box.y - pad)}px`;
411
- veil.style.width = `${Math.max(8, box.width + pad * 2)}px`;
412
- veil.style.height = `${Math.max(8, box.height + pad * 2)}px`;
413
- void veil.offsetWidth;
414
- veil.classList.add("wg-in");
415
- };
416
- const setZoomBadge = (scale) => {
417
- const level = Number.isFinite(scale) && scale > 0 ? scale : 1;
418
- let badge = document.getElementById("wg-pilot-fx-zoom");
419
- if (!badge) {
420
- badge = document.createElement("div");
421
- badge.id = "wg-pilot-fx-zoom";
422
- document.documentElement.appendChild(badge);
423
- }
424
- const zoomed = level > 1.001 || level < 0.999;
425
- badge.dataset.zoomed = zoomed ? "1" : "0";
426
- badge.innerHTML =
427
- '<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>' +
428
- `<span class="wg-pilot-fx-zoom-val">${level.toFixed(2)}\u00d7</span>`;
429
- };
430
- const clearZoom = () => {
431
- // Undo any leftover CSS-scale from older Pilot zoom path.
432
- const root = document.documentElement;
433
- root.style.removeProperty("transform");
434
- root.style.removeProperty("transform-origin");
435
- root.style.removeProperty("transition");
436
- document.getElementById("wg-pilot-fx-zoom")?.remove();
437
- };
438
- const setDeviceChip = (label) => {
439
- document.getElementById("wg-pilot-fx-device")?.remove();
440
- if (!label)
441
- return;
442
- const chip = document.createElement("div");
443
- chip.id = "wg-pilot-fx-device";
444
- chip.dataset.preset = label;
445
- chip.textContent = `device \u00b7 ${label}`;
446
- document.documentElement.appendChild(chip);
447
- };
448
- w.__wgPilotFxZoomOutOnHide = zoomOut;
449
- const clearFx = () => {
450
- document.querySelectorAll(".wg-pilot-fx-ring, .wg-pilot-fx-label").forEach((el) => el.remove());
451
- document.getElementById("wg-pilot-fx-todos")?.remove();
452
- clearFocus();
453
- if (w.__wgPilotFxZoomOutOnHide !== false || clearAll)
454
- clearZoom();
455
- if (clearAll) {
456
- document.getElementById("wg-pilot-fx-device")?.remove();
457
- clearZoom();
458
- }
459
- };
460
- w.__wgPilotFxClear = clearFx;
461
- clearFx();
462
- if (clearAll) {
463
- return { painted: 0, missing: [] };
464
- }
465
- const missing = [];
466
- let painted = 0;
467
- let focusBox = null;
468
- for (const ring of rings) {
469
- const el = document.querySelector(ring.selector);
470
- if (!el) {
471
- missing.push(ring.selector);
472
- continue;
473
- }
474
- const rect = el.getBoundingClientRect();
475
- if (rect.width === 0 && rect.height === 0) {
476
- missing.push(ring.selector);
477
- continue;
478
- }
479
- const pad = ring.size === "sm" ? 3 : ring.size === "lg" ? 10 : 6;
480
- const ringEl = document.createElement("div");
481
- ringEl.className = "wg-pilot-fx-ring";
482
- ringEl.dataset.tone = ring.tone;
483
- ringEl.dataset.size = ring.size;
484
- ringEl.style.left = `${rect.left - pad}px`;
485
- ringEl.style.top = `${rect.top - pad}px`;
486
- ringEl.style.width = `${Math.max(4, rect.width + pad * 2)}px`;
487
- ringEl.style.height = `${Math.max(4, rect.height + pad * 2)}px`;
488
- ringEl.style.opacity = "1";
489
- const labelEl = document.createElement("div");
490
- labelEl.className = "wg-pilot-fx-label";
491
- labelEl.dataset.tone = ring.tone;
492
- labelEl.dataset.size = ring.size;
493
- labelEl.dataset.weight = ring.weight;
494
- labelEl.textContent = ring.label;
495
- labelEl.style.opacity = "1";
496
- if (ring.color) {
497
- labelEl.style.color = ring.color;
498
- }
499
- document.documentElement.appendChild(ringEl);
500
- document.documentElement.appendChild(labelEl);
501
- const lw = labelEl.offsetWidth;
502
- const lh = labelEl.offsetHeight;
503
- let labelLeft = rect.left - pad;
504
- let labelTop = rect.top - pad + rect.height + pad * 2 + 8;
505
- if (labelTop + lh > window.innerHeight - 6)
506
- labelTop = rect.top - pad - lh - 8;
507
- if (labelTop < 6)
508
- labelTop = 6;
509
- if (labelLeft + lw > window.innerWidth - 6) {
510
- labelLeft = Math.max(6, window.innerWidth - 6 - lw);
511
- }
512
- if (labelLeft < 6)
513
- labelLeft = 6;
514
- labelEl.style.left = `${labelLeft}px`;
515
- labelEl.style.top = `${labelTop}px`;
516
- painted += 1;
517
- if (ring.focus) {
518
- focusBox = { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
519
- }
520
- }
521
- if (todos.length > 0) {
522
- const dock = document.createElement("div");
523
- dock.id = "wg-pilot-fx-todos";
524
- dock.dataset.pos = todoPos;
525
- const title = document.createElement("div");
526
- title.className = "wg-pilot-fx-todo-title";
527
- title.textContent = todoTitle;
528
- dock.appendChild(title);
529
- const WINDOW = todoUi.cap > 0 ? todoUi.cap : 5;
530
- const compact = todoUi.compact !== false && todos.length > WINDOW;
531
- if (compact)
532
- dock.dataset.compact = "1";
533
- let start = 0;
534
- let end = todos.length;
535
- if (compact) {
536
- start = Math.max(0, todoIndex - Math.floor((WINDOW - 1) / 2));
537
- end = Math.min(todos.length, start + WINDOW);
538
- start = Math.max(0, end - WINDOW);
539
- }
540
- const ol = document.createElement("ol");
541
- todos.forEach((text, i) => {
542
- const li = document.createElement("li");
543
- li.textContent = text;
544
- if (i < todoIndex)
545
- li.className = "wg-pilot-fx-todo-done";
546
- else if (i === todoIndex)
547
- li.className = "wg-pilot-fx-todo-now";
548
- if (compact && (i < start || i >= end))
549
- li.classList.add("wg-todo-fold");
550
- ol.appendChild(li);
551
- });
552
- dock.appendChild(ol);
553
- if (compact && todos.length - (end - start) > 0) {
554
- const more = document.createElement("div");
555
- more.className = "wg-todo-more";
556
- more.textContent = "+" + (todos.length - (end - start)) + " more";
557
- dock.appendChild(more);
558
- }
559
- document.documentElement.appendChild(dock);
560
- }
561
- if (focusBox)
562
- applyFocus(focusBox);
563
- // Dim todos under rings / focus so captions stay readable.
564
- const dockEl = document.getElementById("wg-pilot-fx-todos");
565
- if (dockEl && todoUi.behindRing !== false && (painted > 0 || focusBox)) {
566
- dockEl.classList.add("wg-todo-behind");
567
- }
568
- // Collision flip: if a painted ring intersects the dock, flip side once.
569
- if (dockEl && todoUi.collision !== false && painted > 0) {
570
- const firstRing = document.querySelector(".wg-pilot-fx-ring");
571
- if (firstRing) {
572
- const rr = firstRing.getBoundingClientRect();
573
- const dr = dockEl.getBoundingClientRect();
574
- const hit = !(rr.right < dr.left || rr.left > dr.right || rr.bottom < dr.top || rr.top > dr.bottom);
575
- if (hit) {
576
- dockEl.dataset.pos = dockEl.dataset.pos === "right" ? "left" : "right";
577
- }
578
- }
579
- }
580
- if (zoom > 1.001 && zoomSelector) {
581
- const target = document.querySelector(zoomSelector);
582
- if (target) {
583
- target.scrollIntoView({
584
- block: "center",
585
- inline: "center",
586
- behavior: "instant",
587
- });
588
- // Re-apply focus after scroll so the cutout tracks the new rect.
589
- if (focusBox) {
590
- const r = target.getBoundingClientRect();
591
- applyFocus({ x: r.left, y: r.top, width: r.width, height: r.height });
592
- }
593
- setZoomBadge(zoom);
594
- }
595
- else {
596
- missing.push(zoomSelector);
597
- }
598
- }
599
- else if (zoom > 1.001) {
600
- setZoomBadge(zoom);
601
- }
602
- if (deviceLabel)
603
- setDeviceChip(deviceLabel);
604
- if (holdMs > 0 &&
605
- (painted > 0 || todos.length > 0 || zoom > 1.001 || !!deviceLabel)) {
606
- w.__wgPilotFxHideTimer = setTimeout(() => clearFx(), holdMs);
607
- }
608
- return { painted, missing };
609
- }, {
610
- rings,
611
- todos,
612
- todoIndex,
613
- todoTitle,
614
- todoPos,
615
- holdMs,
616
- zoom,
617
- zoomSelector: zoomSelector ?? "",
618
- zoomOut,
619
- clearAll: fixtures.clear === true,
620
- deviceLabel,
621
- todoUi,
622
- })
623
- .catch(() => ({ painted: 0, missing: rings.map((r) => r.selector) }));
624
- }
625
- /**
626
- * Refreshes the badge text and the (possibly-collapsed) panel's two tabs
627
- * ("Here" - current live menu; "All nodes" - whole project graph as a
628
- * tree). Never throws - a page mid-navigation when this fires is a real,
629
- * expected timing case, not an error worth failing the caller's own real
630
- * operation over.
631
- */
632
- export async function updatePilotOverlay(page, info) {
633
- await ensureInstalled(page);
634
- await page
635
- .evaluate(async ({ sessionId, snapshot, graph }) => {
636
- const badge = document.getElementById("wg-pilot-badge");
637
- const current = document.getElementById("wg-pilot-panel-current");
638
- const graphPanel = document.getElementById("wg-pilot-panel-graph");
639
- if (!badge || !current || !graphPanel)
640
- return;
641
- const idPart = sessionId ? `session ${sessionId}` : "no session id";
642
- const onBlank = location.href === "about:blank" || location.protocol === "about:";
643
- const hereLabel = snapshot.here ?? (onBlank ? "(blank page)" : "(unknown)");
644
- badge.textContent = `Waygraph Pilot - ${idPart} - ${hereLabel}`;
645
- const totalEdges = snapshot.sections.reduce((n, s) => n + s.edges.length, 0);
646
- if (totalEdges === 0) {
647
- current.innerHTML = onBlank
648
- ? '<div class="wg-pilot-empty">Blank page — no Block moves yet. Use auto goto or a nav Block when ready.</div>'
649
- : '<div class="wg-pilot-empty">No Block moves from here right now.</div>';
650
- }
651
- else {
652
- current.innerHTML = snapshot.sections
653
- .map((section) => {
654
- const rows = section.edges
655
- .map((edge) => {
656
- const desc = edge.description ? ` - ${edge.description}` : "";
657
- const label = edge.label ? ` (${edge.label})` : "";
658
- return `<div class="wg-pilot-edge">[${edge.index}] <span class="wg-pilot-kind">${edge.kind}</span> ${edge.block}${label} -&gt; ${edge.to}${desc}</div>`;
659
- })
660
- .join("");
661
- return `<div class="wg-pilot-section">${section.title}</div>${rows}`;
662
- })
663
- .join("");
664
- }
665
- if (!graph || graph.nodes.length === 0) {
666
- graphPanel.innerHTML = '<div class="wg-pilot-empty">No graph data yet.</div>';
667
- return;
668
- }
669
- // Flat indented-text tree - the ORIGINAL "All nodes" rendering.
670
- // Real, direct user correction: "i also dont like the all node
671
- // stuff. ther should be a 2d version" - kept only as a fallback for
672
- // when Mermaid genuinely can't load (e.g. offline), not the primary
673
- // rendering anymore.
674
- const renderTextTree = () => {
675
- graphPanel.innerHTML = graph.nodes
676
- .map((node) => {
677
- const outgoing = graph.edges.filter((e) => e.from === node.checkpoint || e.from === "*");
678
- const rows = outgoing.length
679
- ? outgoing
680
- .map((e) => `<div class="wg-pilot-graph-edge"><span class="wg-pilot-kind">${e.kind}</span> ${e.block} -&gt; ${e.to}</div>`)
681
- .join("")
682
- : '<div class="wg-pilot-graph-edge wg-pilot-empty">(no outgoing edges)</div>';
683
- const here = node.checkpoint === snapshot.here ? " (here)" : "";
684
- return `<div class="wg-pilot-graph-node"><div class="wg-pilot-graph-checkpoint">${node.checkpoint}${here}</div>${rows}</div>`;
685
- })
686
- .join("");
687
- };
688
- try {
689
- const w = window;
690
- if (!w.mermaid) {
691
- if (!w.__wgMermaidLoadPromise) {
692
- w.__wgMermaidLoadPromise = new Promise((resolve, reject) => {
693
- const script = document.createElement("script");
694
- script.src = "https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js";
695
- script.onload = () => resolve(undefined);
696
- script.onerror = () => reject(new Error("mermaid script failed to load"));
697
- document.head.appendChild(script);
698
- });
699
- }
700
- await w.__wgMermaidLoadPromise;
701
- }
702
- if (!w.mermaid)
703
- throw new Error("mermaid did not attach to window after load");
704
- // Real, direct user request: "left to right stuff. maybe mermaid?
705
- // with square ish arrows???" - LR layout, linear (non-curvy) edges.
706
- w.mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "loose", flowchart: { curve: "linear" } });
707
- const sanitize = (s) => s.replace(/[^A-Za-z0-9_]/g, "_");
708
- // Every Nav Block gets a `from:"*"` wildcard edge for pathfinding
709
- // (see auto-explore.ts's own isUrlNav comment). Real, screenshot-
710
- // confirmed finding, TWICE over:
711
- // 1. Collapsing all of these into one shared "Anywhere" hub node
712
- // still broke the layout - ~9 of 10 Checkpoints have their own
713
- // nav edge, so nearly every node ends up one hop from that hub
714
- // and dagre stacks them in a single rank/column ("it dones't
715
- // look left to right").
716
- // 2. Dropping them entirely (no edges, just a footnote) fixed the
717
- // layout but then hid real connectivity - a node like Settings
718
- // that has ONLY a nav edge looked like a disconnected orphan
719
- // ("the all nodes doesn't even show the navs block... whre is
720
- // settings there").
721
- // Neither extreme is right: a wildcard nav edge isn't really a
722
- // flow TRANSITION (it doesn't come from any one specific screen,
723
- // it's global infrastructure - the persistent sidebar), so it
724
- // doesn't belong in the layout-driving edge set at all, but the
725
- // fact that a node is directly nav-reachable is still real
726
- // information worth showing. Marked on the node's own label
727
- // instead of as a graph edge - visible on every affected node,
728
- // adds zero layout-warping connectivity.
729
- const nonWildcard = graph.edges.filter((e) => e.from !== "*");
730
- const navReachable = new Set(graph.edges.filter((e) => e.from === "*").map((e) => e.to));
731
- // Real, direct user finding (screenshot-confirmed): a self-loop
732
- // edge (an action that stays on the same Checkpoint, e.g.
733
- // "fill-email -> SignIn") renders as a spiral squiggle Mermaid
734
- // draws back into the same node - with several per node (every
735
- // fill-* / select-* action), these dominated the diagram and
736
- // broke the left-to-right layout dagre otherwise produces cleanly
737
- // for a real DAG of inter-Checkpoint transitions. Self-loops add
738
- // no flow information anyway (the "Here" tab already lists every
739
- // action from wherever you are) - excluded from the graph itself,
740
- // kept only as a "(+N)" count on the node label so the count
741
- // isn't silently lost.
742
- const selfLoopCounts = new Map();
743
- for (const e of nonWildcard) {
744
- if (e.from === e.to)
745
- selfLoopCounts.set(e.from, (selfLoopCounts.get(e.from) ?? 0) + 1);
746
- }
747
- const specific = nonWildcard.filter((e) => e.from !== e.to);
748
- const lines = ["graph LR"];
749
- for (const node of graph.nodes) {
750
- const id = sanitize(node.checkpoint);
751
- const here = node.checkpoint === snapshot.here;
752
- const loops = selfLoopCounts.get(node.checkpoint);
753
- const nav = navReachable.has(node.checkpoint) ? " [nav]" : "";
754
- const suffix = `${here ? " (here)" : ""}${loops ? ` (+${loops})` : ""}${nav}`;
755
- lines.push(` ${id}["${node.checkpoint}${suffix}"]`);
756
- if (here)
757
- lines.push(` style ${id} stroke:#7C3AED,stroke-width:3px`);
758
- }
759
- for (const e of specific) {
760
- lines.push(` ${sanitize(e.from)} -->|${e.block}| ${sanitize(e.to)}`);
761
- }
762
- const { svg } = await w.mermaid.render(`wg-mermaid-${Date.now()}`, lines.join("\n"));
763
- graphPanel.innerHTML =
764
- 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>`;
765
- // Real, direct user request: "i hope it is zoomed to the current
766
- // node" - the diagram is usually bigger than the panel, so scroll
767
- // (not zoom - the SVG itself stays real size, legible) the "here"
768
- // node into view instead of leaving it wherever dagre placed it.
769
- const hereId = snapshot.here ? sanitize(snapshot.here) : null;
770
- const hereEl = hereId ? graphPanel.querySelector(`[id^="flowchart-${hereId}-"]`) : null;
771
- hereEl?.scrollIntoView({ block: "center", inline: "center" });
772
- }
773
- catch {
774
- renderTextTree();
775
- }
776
- }, { sessionId: info.sessionId, snapshot: info.snapshot, graph: info.graph ?? null })
777
- .catch(() => { });
778
- }
1
+ // Barrel: pilot-overlay lives in src/pilot-overlay/ (see src/ARCHITECTURE.md). Kept so existing imports of "./pilot-overlay.js" don't change.
2
+ export * from "./pilot-overlay/shell.js";
3
+ export * from "./pilot-overlay/fixtures.js";
4
+ export * from "./pilot-overlay/panel.js";