@lightworkai.official/debug-capture 0.19.7 → 0.19.9
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/README.md +26 -9
- package/dist/debug-capture.js +16 -16
- package/dist/index.mjs +23 -23
- package/dist/replayPack.d.ts +56 -3
- package/dist/replayPictures.d.ts +33 -0
- package/dist/types.d.ts +10 -2
- package/dist/ui/bubblePlace.d.ts +29 -29
- package/package.json +1 -1
- package/src/replayPack.ts +369 -44
- package/src/replayPictures.ts +200 -0
- package/src/submit.ts +1 -1
- package/src/types.ts +14 -2
- package/src/ui/bubblePlace.ts +76 -35
- package/src/ui/chatBubble.ts +34 -28
package/src/submit.ts
CHANGED
|
@@ -110,7 +110,7 @@ export async function submitBundle(title: string, bundle: DebugBundle): Promise<
|
|
|
110
110
|
let replay: ReplaySummary | undefined;
|
|
111
111
|
const recording = replayOptions();
|
|
112
112
|
if (recording) {
|
|
113
|
-
const packed = await packReplay(trimmed.replay, recording.maxBytes);
|
|
113
|
+
const packed = await packReplay(trimmed.replay, recording.maxBytes, { quality: recording.canvas?.quality });
|
|
114
114
|
replay = packed.summary;
|
|
115
115
|
if ("dataUri" in packed) attachments.push({ dataUri: packed.dataUri, filename: packed.filename });
|
|
116
116
|
}
|
package/src/types.ts
CHANGED
|
@@ -188,7 +188,7 @@ export interface ReplayCapture {
|
|
|
188
188
|
* attachment loads — or say why there is none.
|
|
189
189
|
*/
|
|
190
190
|
export interface ReplaySummary {
|
|
191
|
-
/** Whole seconds covered,
|
|
191
|
+
/** Whole seconds covered by what was attached (or, with `omitted`, by what was recorded). */
|
|
192
192
|
seconds?: number;
|
|
193
193
|
events?: number;
|
|
194
194
|
startedAt?: string;
|
|
@@ -196,8 +196,20 @@ export interface ReplaySummary {
|
|
|
196
196
|
/** Size of the attachment as sent, and whether it is gzip. */
|
|
197
197
|
bytes?: number;
|
|
198
198
|
compressed?: boolean;
|
|
199
|
-
|
|
199
|
+
/*
|
|
200
|
+
* What was given up to fit under the size cap (replayPack), each present
|
|
201
|
+
* only when it happened.
|
|
202
|
+
*/
|
|
203
|
+
/** Canvas pictures left out: the canvases play blank. */
|
|
200
204
|
canvasDropped?: boolean;
|
|
205
|
+
/** Large pictures embedded in the page (a `data:` image, an inlined font) left out. */
|
|
206
|
+
picturesDropped?: boolean;
|
|
207
|
+
/** Long text and styles shortened — only when not even one picture of the page fitted otherwise. */
|
|
208
|
+
simplified?: boolean;
|
|
209
|
+
/** Only part of the recording went: how long the whole was. `seconds` is what was sent. */
|
|
210
|
+
recordedSeconds?: number;
|
|
211
|
+
/** The part sent stops this many seconds before the report was made. */
|
|
212
|
+
endsEarlySeconds?: number;
|
|
201
213
|
/** Present when the host records but this report carries no replay. */
|
|
202
214
|
omitted?: "too-large" | "empty";
|
|
203
215
|
}
|
package/src/ui/bubblePlace.ts
CHANGED
|
@@ -14,13 +14,14 @@
|
|
|
14
14
|
* Content that scrolls under is not: a floating button over content is what a floating button is. A host may name
|
|
15
15
|
* more (`chat.avoid: "<selector>"`) or turn it off (`false`).
|
|
16
16
|
*
|
|
17
|
-
* THE NEAREST FREE PLACE.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
17
|
+
* THE NEAREST FREE PLACE (`placeSpot`). What a place costs is the climb plus the move in from its edge, and
|
|
18
|
+
* SWITCH_COST more on the other side. Up or down its own column; or a column further in, just past what stands in
|
|
19
|
+
* the way; or the other corner. A FAB lifts it above. A toolbar column as tall as the window puts it just inside the
|
|
20
|
+
* toolbar, at the same height. 0.19.7 only knew the two corner columns, so on a short window, with the ERP's tray
|
|
21
|
+
* filling the right edge and its sidebar the left, it found no free place and stayed on the tray (the owner's
|
|
22
|
+
* screenshot, 11-02). When a nearer place frees up it goes back: the tray shut, it is home again. A shift of less than
|
|
23
|
+
* STAY_MARGIN does not move it, so it does not jitter as the page settles.
|
|
24
|
+
|
|
24
25
|
* A PAGE'S OWN PLACE. The desk's rules (ปุ่มแชท › ตำแหน่งปุ่ม): pages whose path starts so — hidden there, or
|
|
25
26
|
* started in a corner at a height. The most specific rule wins; a person who moved it keeps their place.
|
|
26
27
|
*
|
|
@@ -76,37 +77,58 @@ export function nearestFreeY(want: number, ranges: readonly [number, number][],
|
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const here = nearestFreeY(want.y, free[want.side], min, max);
|
|
86
|
-
const there = nearestFreeY(want.y, free[other], min, max);
|
|
87
|
-
if (here === null && there === null) return want;
|
|
88
|
-
const costHere = here === null ? Infinity : Math.abs(here - want.y);
|
|
89
|
-
const costThere = there === null ? Infinity : SWITCH_COST + Math.abs(there - want.y);
|
|
90
|
-
return costHere <= costThere ? { side: want.side, y: here! } : { side: other, y: there! };
|
|
80
|
+
export interface Spot {
|
|
81
|
+
side: Side;
|
|
82
|
+
/** From its side's edge, px. */
|
|
83
|
+
x: number;
|
|
84
|
+
/** Up from the bottom, px. */
|
|
85
|
+
y: number;
|
|
91
86
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
/** The page's boxes across the bubble's column at `x` from `side`'s edge — read off the page, or given in the tests. */
|
|
88
|
+
export type ReadColumn = (side: Side, x: number) => readonly Rect[];
|
|
89
|
+
/** How many columns further in it looks on a side, past what stands in the way. */
|
|
90
|
+
export const MAX_SHIFTS = 3;
|
|
95
91
|
|
|
96
92
|
/**
|
|
97
|
-
* Where
|
|
98
|
-
*
|
|
93
|
+
* Where the bubble stands: `want` when it is free there; else the cheapest free place — up or down its column, a column
|
|
94
|
+
* further in just past what stands in the way (a toolbar column as tall as the window: just left of it, at the same
|
|
95
|
+
* height), or the other side's (SWITCH_COST more). What a place costs is the climb plus the move in. A place it already
|
|
96
|
+
* stands in (`prev`), still free and within STAY_MARGIN of the best, is kept — no jitter as the page settles. Columns
|
|
97
|
+
* are read only as needed (`read`), and never past the middle of the window. Nothing free anywhere: `want`. Pure.
|
|
99
98
|
*/
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
export function placeSpot(want: Spot, read: ReadColumn, view: { w: number; h: number }, size: number, min: number, max: number, prev: Spot | null = null): Spot {
|
|
100
|
+
const boxes = new Map<string, readonly Rect[]>();
|
|
101
|
+
const at = (side: Side, x: number) => {
|
|
102
|
+
const k = `${side}:${x}`;
|
|
103
|
+
if (!boxes.has(k)) boxes.set(k, read(side, x));
|
|
104
|
+
return boxes.get(k)!;
|
|
105
|
+
};
|
|
106
|
+
const col = (side: Side, x: number) => (side === "right" ? { left: view.w - x - size, right: view.w - x } : { left: x, right: x + size });
|
|
107
|
+
const ranges = (side: Side, x: number) => blockedRanges(at(side, x), col(side, x), view.h, size);
|
|
108
|
+
const cost = (s: Spot) => Math.abs(s.y - want.y) + Math.abs(s.x - want.x) + (s.side === want.side ? 0 : SWITCH_COST);
|
|
109
|
+
const freeAt = (s: Spot) => nearestFreeY(s.y, ranges(s.side, s.x), min, max) === s.y;
|
|
110
|
+
const keep = (best: Spot) => (prev && (prev.side !== best.side || prev.x !== best.x || prev.y !== best.y) && cost(prev) <= cost(best) + STAY_MARGIN && freeAt(prev) ? prev : best);
|
|
111
|
+
if (freeAt(want)) return keep(want);
|
|
112
|
+
let best: Spot | null = null;
|
|
113
|
+
const other: Side = want.side === "right" ? "left" : "right";
|
|
114
|
+
for (const side of [want.side, other]) {
|
|
115
|
+
const away = side === want.side ? 0 : SWITCH_COST;
|
|
116
|
+
if (best && away >= cost(best)) break;
|
|
117
|
+
let x = want.x;
|
|
118
|
+
for (let i = 0; i <= MAX_SHIFTS; i++) {
|
|
119
|
+
const y = nearestFreeY(want.y, ranges(side, x), min, max);
|
|
120
|
+
if (y !== null && (!best || cost({ side, x, y }) < cost(best))) best = { side, x, y };
|
|
121
|
+
// What stands where it wants to be, at this column: the next column in is just past it.
|
|
122
|
+
const c = col(side, x);
|
|
123
|
+
const band = { top: view.h - want.y - size - CLEAR, bottom: view.h - want.y + CLEAR };
|
|
124
|
+
const inWay = at(side, x).filter((r) => r.top < band.bottom && r.bottom > band.top && r.left < c.right && r.right > c.left);
|
|
125
|
+
if (!inWay.length) break;
|
|
126
|
+
const next = Math.round(side === "right" ? Math.max(...inWay.map((r) => view.w - r.left + CLEAR)) : Math.max(...inWay.map((r) => r.right + CLEAR)));
|
|
127
|
+
if (next <= x || next + size > view.w / 2 || (best && away + next - want.x >= cost(best))) break;
|
|
128
|
+
x = next;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return best ? keep(best) : want;
|
|
110
132
|
}
|
|
111
133
|
|
|
112
134
|
/* ── A page's own place: the desk's rules ───────────────────────────────── */
|
|
@@ -153,6 +175,8 @@ const OURS = /^LW-/;
|
|
|
153
175
|
const CONTROL =
|
|
154
176
|
'a[href],button,input,select,textarea,summary,label,[role="button"],[role="link"],[role="tab"],[role="menuitem"],[role="checkbox"],[role="switch"],[role="radio"],[role="option"],[role="slider"],[role="combobox"],[role="textbox"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
|
|
155
177
|
const SCROLLS = /(auto|scroll|overlay)/;
|
|
178
|
+
/** A control that takes no press now — no hit test finds it (`disabled:pointer-events-none`), yet it shows, and takes one soon. */
|
|
179
|
+
const DISABLED = 'button:disabled,input:disabled,select:disabled,textarea:disabled,[aria-disabled="true"]';
|
|
156
180
|
|
|
157
181
|
/**
|
|
158
182
|
* The page's pieces the bubble must not cover, for each column — sampled every `step` px down it at three points
|
|
@@ -160,7 +184,9 @@ const SCROLLS = /(auto|scroll|overlay)/;
|
|
|
160
184
|
* - a host's selector (`extra`): what it names, whole, wherever it is;
|
|
161
185
|
* - in a small floating box (its nearest `fixed`, or `sticky` stuck at its edge): what shows there, by its own box —
|
|
162
186
|
* the control it is part of, else itself;
|
|
163
|
-
* - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller
|
|
187
|
+
* - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller — a disabled one
|
|
188
|
+
* too, found by its own selector, as no hit test reaches it (the ERP's ผลการจำแนก panel on a phone, 11-02: its
|
|
189
|
+
* greyed มอบหมายผู้ดำเนินการ was under the bubble);
|
|
164
190
|
* - anything else scrolls with the page, and is not.
|
|
165
191
|
*/
|
|
166
192
|
export function scanColumns(cols: readonly { left: number; right: number }[], view: { w: number; h: number }, opts: { step?: number; extra?: string | null } = {}): Rect[][] {
|
|
@@ -240,6 +266,8 @@ export function scanColumns(cols: readonly { left: number; right: number }[], vi
|
|
|
240
266
|
right: r.right,
|
|
241
267
|
});
|
|
242
268
|
};
|
|
269
|
+
/** The frames as big as the window met down this column (null: a page that does not scroll) — their disabled controls are looked for below. */
|
|
270
|
+
const frames = new Set<Element | null>();
|
|
243
271
|
const xs = [col.left + 6, (col.left + col.right) / 2, col.right - 6];
|
|
244
272
|
for (let y = step / 2; y < view.h; y += step) {
|
|
245
273
|
for (const x of xs) {
|
|
@@ -268,10 +296,23 @@ export function scanColumns(cols: readonly { left: number; right: number }[], vi
|
|
|
268
296
|
continue;
|
|
269
297
|
}
|
|
270
298
|
// The page itself — a scrolling page, or what is in a frame as big as the window: only a control that stays put.
|
|
299
|
+
if (!scrolled && (float || !pageScrolls)) frames.add(float);
|
|
271
300
|
if (scrolled || !own || big(own.getBoundingClientRect())) continue;
|
|
272
301
|
keep(own);
|
|
273
302
|
}
|
|
274
303
|
}
|
|
304
|
+
for (const frame of frames) {
|
|
305
|
+
for (const el of (frame ?? body)?.querySelectorAll(DISABLED) ?? []) {
|
|
306
|
+
const r = el.getBoundingClientRect();
|
|
307
|
+
if (!r.width || !r.height || r.right <= col.left || r.left >= col.right || big(r) || style(el).visibility === "hidden") continue;
|
|
308
|
+
const pin = pins.get(el) ?? (pins.set(el, pinOf(el)), pins.get(el)!);
|
|
309
|
+
if (pin.float !== frame || pin.scrolled || pin.opacity < 0.05) continue;
|
|
310
|
+
// Not under another layer: what is on top at its middle (in this column) is it, or the frame's piece around it.
|
|
311
|
+
const on = topAt(Math.min(Math.max((r.left + r.right) / 2, col.left + 6), col.right - 6), (r.top + r.bottom) / 2);
|
|
312
|
+
if (on && on !== el && !el.contains(on) && !on.contains(el)) continue;
|
|
313
|
+
keep(el);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
275
316
|
return out;
|
|
276
317
|
});
|
|
277
318
|
}
|
package/src/ui/chatBubble.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { chatPlace, getConfig } from "../config";
|
|
|
18
18
|
import { hostToken, onLiveFrame } from "../live";
|
|
19
19
|
import { openSupportPortal } from "../portal";
|
|
20
20
|
import { launch } from "../reporter";
|
|
21
|
-
import { blockedRanges,
|
|
21
|
+
import { blockedRanges, placeFor, placeSpot, scanColumns, type PlaceRule, type Rect, type Side } from "./bubblePlace";
|
|
22
22
|
import { followHostFont, OWN_FONTS } from "./hostFont";
|
|
23
23
|
import { strings } from "./strings";
|
|
24
24
|
|
|
@@ -354,13 +354,27 @@ function savePlace(): void {
|
|
|
354
354
|
const pagePath = () => window.location?.pathname ?? "/";
|
|
355
355
|
/** Where it stands now — `spot` (or the page's rule), moved clear of the page's floating controls. */
|
|
356
356
|
let shown: BubbleSpot | null = null;
|
|
357
|
-
/** What `shown` was chosen for — the page and the wish; while they hold, a free `shown` is kept (
|
|
357
|
+
/** What `shown` was chosen for — the page and the wish; while they hold, a free `shown` is kept (placeSpot's prev). */
|
|
358
358
|
let shownFor = "";
|
|
359
359
|
/**
|
|
360
|
-
* The
|
|
361
|
-
*
|
|
360
|
+
* The page's boxes across each column read since the last look, for the window size they were read at — null: not
|
|
361
|
+
* looked at yet, or not stepping aside. `placeSpot` asks for columns as it needs them; one not read is read then.
|
|
362
362
|
*/
|
|
363
|
-
let
|
|
363
|
+
let columns: { w: number; h: number; boxes: Map<string, Rect[]> } | null = null;
|
|
364
|
+
/** What the last placing saw at home and chose — a look that changes neither is "nothing new" (queueRescan). */
|
|
365
|
+
let placedSig = "";
|
|
366
|
+
function readColumn(side: Side, x: number): Rect[] {
|
|
367
|
+
const v = view();
|
|
368
|
+
if (!columns || columns.w !== v.w || columns.h !== v.h) columns = { w: v.w, h: v.h, boxes: new Map() };
|
|
369
|
+
const k = `${side}:${x}`;
|
|
370
|
+
let boxes = columns.boxes.get(k);
|
|
371
|
+
if (!boxes) {
|
|
372
|
+
const col = side === "right" ? { left: v.w - x - SIZE, right: v.w - x } : { left: x, right: x + SIZE };
|
|
373
|
+
boxes = scanColumns([col], v, { extra: avoidSetting().extra })[0] ?? [];
|
|
374
|
+
columns.boxes.set(k, boxes);
|
|
375
|
+
}
|
|
376
|
+
return boxes;
|
|
377
|
+
}
|
|
364
378
|
/** Hidden on this page by the desk's rule (ซ่อนปุ่มแชทในหน้านี้). */
|
|
365
379
|
let ruleHidden = false;
|
|
366
380
|
|
|
@@ -377,29 +391,19 @@ function wanted(): BubbleSpot {
|
|
|
377
391
|
if (movedByHand || !rule || rule.show === "hide") return spot;
|
|
378
392
|
return { side: rule.show, x: spot.x, y: rule.y };
|
|
379
393
|
}
|
|
380
|
-
/**
|
|
394
|
+
/** Look at the page afresh — its columns read again as placing asks for them — and place again. */
|
|
381
395
|
function rescan(): void {
|
|
382
396
|
if (!bubble) return;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
blocked = null;
|
|
397
|
+
if (!avoidSetting().on) {
|
|
398
|
+
columns = null;
|
|
386
399
|
return layout();
|
|
387
400
|
}
|
|
401
|
+
const before = placedSig;
|
|
388
402
|
const v = view();
|
|
389
|
-
|
|
390
|
-
const cols: Record<Side, { left: number; right: number }> = { right: { left: v.w - x - SIZE, right: v.w - x }, left: { left: x, right: x + SIZE } };
|
|
391
|
-
const read = (side: Side) => blockedRanges(scanColumns([cols[side]], v, { extra })[0] ?? [], cols[side], v.h, SIZE);
|
|
392
|
-
// Its own side first; the other corner is read only when the place it wants there is taken (half the hit tests).
|
|
393
|
-
const want = wanted();
|
|
394
|
-
const wantY = clampBubbleY(want.y, v.h);
|
|
395
|
-
const other: Side = want.side === "right" ? "left" : "right";
|
|
396
|
-
const here = read(want.side);
|
|
397
|
-
const there = nearestFreeY(wantY, here, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN)) === wantY ? null : read(other);
|
|
398
|
-
const next = { [want.side]: here, [other]: there } as Record<Side, [number, number][] | null>;
|
|
399
|
-
// Nothing new where it could stand: the page is looked at less often while that holds (queueRescan).
|
|
400
|
-
calm = JSON.stringify([next.right, next.left]) === JSON.stringify(blocked && [blocked.right, blocked.left]) ? Math.min(calm + 1, CALM_STEPS) : 0;
|
|
401
|
-
blocked = next;
|
|
403
|
+
columns = { w: v.w, h: v.h, boxes: new Map() };
|
|
402
404
|
layout();
|
|
405
|
+
// Nothing new where it could stand: the page is looked at less often while that holds (queueRescan).
|
|
406
|
+
calm = placedSig === before ? Math.min(calm + 1, CALM_STEPS) : 0;
|
|
403
407
|
}
|
|
404
408
|
/*
|
|
405
409
|
* How often the page is read (a read is a column or two of hit tests). A page that keeps changing — a clock, a
|
|
@@ -474,7 +478,7 @@ function watchPage(on: boolean): void {
|
|
|
474
478
|
});
|
|
475
479
|
if (theirs) queueRescan();
|
|
476
480
|
});
|
|
477
|
-
watcher.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-expanded"] });
|
|
481
|
+
watcher.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style", "hidden", "open", "aria-expanded", "disabled", "aria-disabled"] });
|
|
478
482
|
}
|
|
479
483
|
for (const t of ACTED) window.addEventListener(t, onPageActed, { capture: true, passive: true });
|
|
480
484
|
// A toolbar's own opening ends after the press; a sticky bar sticks as the page scrolls.
|
|
@@ -500,11 +504,12 @@ function layout(): void {
|
|
|
500
504
|
// While the panel is open it stays where it was opened from — never moved under the person's eyes.
|
|
501
505
|
const wish = `${pagePath()}|${want.side}|${want.x}|${wantY}`;
|
|
502
506
|
const prev = shown && shownFor === wish ? shown : null;
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
const free = !isOpen || !shown ? (
|
|
506
|
-
shown = { side: free.side, x:
|
|
507
|
+
const home = { side: want.side, x: want.x, y: wantY };
|
|
508
|
+
const placing = !!columns && avoidSetting().on;
|
|
509
|
+
const free = !isOpen || !shown ? (placing ? placeSpot(home, readColumn, v, SIZE, MARGIN, Math.max(MARGIN, v.h - SIZE - MARGIN), prev) : home) : shown;
|
|
510
|
+
shown = { side: free.side, x: free.x, y: free.y };
|
|
507
511
|
shownFor = wish;
|
|
512
|
+
if (placing) placedSig = JSON.stringify([blockedRanges(readColumn(home.side, home.x), home.side === "right" ? { left: v.w - home.x - SIZE, right: v.w - home.x } : { left: home.x, right: home.x + SIZE }, v.h, SIZE), shown]);
|
|
508
513
|
const at = shown;
|
|
509
514
|
const y = at.y;
|
|
510
515
|
const other = at.side === "right" ? "left" : "right";
|
|
@@ -779,7 +784,8 @@ export function unmountSupportChat(): void {
|
|
|
779
784
|
calm = 0;
|
|
780
785
|
shown = null;
|
|
781
786
|
shownFor = "";
|
|
782
|
-
|
|
787
|
+
columns = null;
|
|
788
|
+
placedSig = "";
|
|
783
789
|
ruleHidden = false;
|
|
784
790
|
followHere(false);
|
|
785
791
|
root?.remove();
|