@reticlehq/browser 3.6.0 → 3.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/action-report.d.ts +64 -0
- package/dist/actions/action-report.js +87 -0
- package/dist/actions/actions-dom.d.ts +5 -0
- package/dist/actions/actions-dom.js +32 -4
- package/dist/actions/actions-press.d.ts +1 -0
- package/dist/actions/actions-press.js +25 -1
- package/dist/actions/actions.d.ts +2 -37
- package/dist/actions/actions.js +56 -80
- package/dist/actions/appeared-text.js +5 -2
- package/dist/actions/click-target.d.ts +1 -0
- package/dist/actions/click-target.js +14 -0
- package/dist/actions/danger-context.d.ts +4 -1
- package/dist/actions/danger-context.js +45 -4
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/activate.d.ts +14 -0
- package/dist/activate.js +43 -0
- package/dist/busy.d.ts +3 -0
- package/dist/busy.js +42 -0
- package/dist/commands/commands.js +49 -17
- package/dist/connect-options.d.ts +12 -1
- package/dist/connect-options.js +18 -1
- package/dist/dom/a11y.d.ts +2 -0
- package/dist/dom/a11y.js +353 -80
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +8 -0
- package/dist/dom/dom-ignore.js +57 -2
- package/dist/dom/layout-audit.d.ts +15 -0
- package/dist/dom/layout-audit.js +208 -0
- package/dist/dom/paint-context.d.ts +15 -0
- package/dist/dom/paint-context.js +78 -0
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +163 -38
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/shadow-registry.d.ts +1 -0
- package/dist/dom/shadow-registry.js +27 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +135 -36
- package/dist/dom/stable-selector.d.ts +1 -0
- package/dist/dom/stable-selector.js +19 -0
- package/dist/dom/text-match.d.ts +12 -0
- package/dist/dom/text-match.js +115 -0
- package/dist/dom/theme.d.ts +1 -0
- package/dist/dom/theme.js +21 -9
- package/dist/dom/virtualized.js +5 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -0
- package/dist/observers/blind-spots.js +3 -2
- package/dist/observers/canvas.d.ts +18 -0
- package/dist/observers/canvas.js +76 -0
- package/dist/observers/dom.js +96 -32
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +116 -30
- package/dist/observers/install-all.js +4 -0
- package/dist/observers/media.d.ts +2 -0
- package/dist/observers/media.js +32 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +8 -1
- package/dist/observers/network.js +82 -13
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +39 -9
- package/dist/presenter/carousel/carousel.js +2 -7
- package/dist/presenter/carousel/panel-slides.d.ts +2 -4
- package/dist/presenter/carousel/panel-slides.js +17 -15
- package/dist/presenter/chrome/log-memory.d.ts +1 -0
- package/dist/presenter/chrome/presenter-log.d.ts +7 -0
- package/dist/presenter/chrome/presenter-log.js +48 -8
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
- package/dist/presenter/chrome/presenter-report-copy.js +12 -4
- package/dist/presenter/hud-telemetry.js +55 -4
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +27 -5
- package/dist/presenter/presenter-account.js +83 -6
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +25 -4
- package/dist/presenter/presenter-chat-views.js +339 -72
- package/dist/presenter/presenter-config.d.ts +3 -0
- package/dist/presenter/presenter-config.js +2 -0
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +13 -91
- package/dist/presenter/presenter-design-tokens.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +31 -24
- package/dist/presenter/presenter-harness-row.d.ts +56 -0
- package/dist/presenter/presenter-harness-row.js +170 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.js +25 -24
- package/dist/presenter/presenter-report-styles.js +23 -22
- package/dist/presenter/presenter-report.js +29 -13
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings-styles.js +8 -4
- package/dist/presenter/presenter-settings.d.ts +4 -4
- package/dist/presenter/presenter-settings.js +66 -28
- package/dist/presenter/presenter-shell-styles.js +12 -3
- package/dist/presenter/presenter-shell.d.ts +11 -3
- package/dist/presenter/presenter-shell.js +60 -22
- package/dist/presenter/presenter-styles.js +0 -2
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +3 -2
- package/dist/presenter/presenter.js +107 -25
- package/dist/registry/declared-platform.js +2 -0
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +771 -703
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +72 -8
- package/dist/security/page-location.d.ts +2 -0
- package/dist/security/page-location.js +7 -0
- package/dist/security/serialization.js +1 -1
- package/dist/storage-keys.d.ts +4 -2
- package/dist/storage-keys.js +8 -3
- package/dist/timers/native/native-timers.d.ts +1 -0
- package/dist/timers/native/native-timers.js +5 -0
- package/dist/transport/transport.d.ts +1 -0
- package/dist/transport/transport.js +23 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +6 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -56
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
1
|
+
import { BlindSpotKind, EventType, NEXT_ACTION_FIELD, NEXT_ACTION_HEADER, REQUEST_SHAPE_FIELD, REQUEST_SHAPE_NONE, RETICLE_WS_PATH, ResponseBodyReason, StreamTransport, StreamDirection, fnv1a, } from '@reticlehq/core';
|
|
2
2
|
import { captureMethod } from '../patching/capture-method.js';
|
|
3
3
|
import { observeSafely, observeValue } from './types.js';
|
|
4
|
-
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, withBodyDeadline, } from './net-detail/network-body.js';
|
|
4
|
+
import { isCapturableType, projectBody, projectErrorBody, shouldCaptureErrorBody, skippedBodyReason, withBodyDeadline, } from './net-detail/network-body.js';
|
|
5
5
|
import { redactUrl, netUrlFields } from './net-detail/network-redact.js';
|
|
6
6
|
import { watchStreamedBody } from './network-stream.js';
|
|
7
7
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
@@ -63,15 +63,64 @@ function jsonShape(text) {
|
|
|
63
63
|
return ShapeToken.OTHER;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
const MAX_DECODED_BODY_BYTES = 1 << 20;
|
|
67
|
+
const TAB = 0x09;
|
|
68
|
+
const LINE_FEED = 0x0a;
|
|
69
|
+
const CARRIAGE_RETURN = 0x0d;
|
|
70
|
+
const FIRST_PRINTABLE = 0x20;
|
|
71
|
+
const DELETE = 0x7f;
|
|
72
|
+
function hasControlCharacter(text) {
|
|
73
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
74
|
+
const code = text.charCodeAt(i);
|
|
75
|
+
if (code === DELETE)
|
|
76
|
+
return true;
|
|
77
|
+
if (code < FIRST_PRINTABLE && code !== TAB && code !== LINE_FEED && code !== CARRIAGE_RETURN) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function textOfBytes(body) {
|
|
84
|
+
if (!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)))
|
|
85
|
+
return undefined;
|
|
86
|
+
if (body.byteLength > MAX_DECODED_BODY_BYTES)
|
|
87
|
+
return undefined;
|
|
88
|
+
try {
|
|
89
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(body);
|
|
90
|
+
return hasControlCharacter(text) ? undefined : text;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function formShape(form) {
|
|
97
|
+
const fields = [];
|
|
98
|
+
let length = 0;
|
|
99
|
+
form.forEach((value, name) => {
|
|
100
|
+
if ('string' === typeof value)
|
|
101
|
+
length += value.length;
|
|
102
|
+
fields.push(JSON.stringify([name, 'string' === typeof value ? 0 : [value.type, value.size]]));
|
|
103
|
+
});
|
|
104
|
+
return 0 === fields.length ? undefined : fingerprintBody(fields.sort().join(), length);
|
|
105
|
+
}
|
|
66
106
|
function projectRequestBody(body, captureBodies) {
|
|
67
107
|
let text;
|
|
68
108
|
let contentType = 'application/json';
|
|
69
109
|
let shape = REQUEST_SHAPE_NONE;
|
|
70
|
-
|
|
71
|
-
|
|
110
|
+
const bytesText = textOfBytes(body);
|
|
111
|
+
const typeOf = () => body.constructor?.name ?? typeof body;
|
|
112
|
+
if ('string' === typeof body || bytesText !== undefined) {
|
|
113
|
+
text = bytesText ?? body;
|
|
72
114
|
if (text.length > 0)
|
|
73
115
|
shape = fingerprintBody(jsonShape(text), text.length);
|
|
74
116
|
}
|
|
117
|
+
else if (typeof FormData !== 'undefined' && body instanceof FormData) {
|
|
118
|
+
const form = formShape(body);
|
|
119
|
+
return {
|
|
120
|
+
...(form === undefined ? {} : { [REQUEST_SHAPE_FIELD]: form }),
|
|
121
|
+
...(captureBodies ? { requestBodyType: typeOf() } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
75
124
|
else if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams) {
|
|
76
125
|
text = body.toString();
|
|
77
126
|
contentType = 'application/x-www-form-urlencoded';
|
|
@@ -81,8 +130,7 @@ function projectRequestBody(body, captureBodies) {
|
|
|
81
130
|
}
|
|
82
131
|
}
|
|
83
132
|
else if (body !== undefined && body !== null) {
|
|
84
|
-
|
|
85
|
-
return captureBodies ? { requestBodyType: kind } : {};
|
|
133
|
+
return captureBodies ? { requestBodyType: typeOf() } : {};
|
|
86
134
|
}
|
|
87
135
|
const fields = { [REQUEST_SHAPE_FIELD]: shape };
|
|
88
136
|
if (!captureBodies || text === undefined || 0 === text.length)
|
|
@@ -182,7 +230,13 @@ function isNativeFetch(fn) {
|
|
|
182
230
|
}
|
|
183
231
|
}
|
|
184
232
|
const OURS = new WeakSet();
|
|
233
|
+
const PAGE_CAPTURE = Symbol.for('reticle.network');
|
|
234
|
+
const pageSlots = () => window;
|
|
185
235
|
export function installNetwork(emit, opts = {}) {
|
|
236
|
+
if (undefined !== pageSlots()[PAGE_CAPTURE])
|
|
237
|
+
return () => undefined;
|
|
238
|
+
const owner = {};
|
|
239
|
+
pageSlots()[PAGE_CAPTURE] = owner;
|
|
186
240
|
const captureBodies = true === opts.captureBodies;
|
|
187
241
|
const captureErrorBodies = false !== opts.captureErrorBodies;
|
|
188
242
|
const reinterpret = opts.reinterpret;
|
|
@@ -202,6 +256,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
202
256
|
const method = methodOf(input, init);
|
|
203
257
|
const nextAction = nextActionOf(input, init);
|
|
204
258
|
const nextActionFields = nextAction === undefined ? {} : { [NEXT_ACTION_FIELD]: nextAction };
|
|
259
|
+
const bodyFields = observeValue(() => projectRequestBody(init?.body, captureBodies)) ?? {};
|
|
205
260
|
const urlFields = netUrlFields(rawUrl);
|
|
206
261
|
const url = urlFields.url;
|
|
207
262
|
const initiatorStack = observeValue(() => initiatorFrame());
|
|
@@ -236,7 +291,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
236
291
|
...initiatorFields,
|
|
237
292
|
...resourceTiming(rawUrl),
|
|
238
293
|
...netResponseMeta(res.statusText, contentType, res.headers.get('content-length')),
|
|
239
|
-
...
|
|
294
|
+
...bodyFields,
|
|
240
295
|
...nextActionFields,
|
|
241
296
|
...responseBodyFields,
|
|
242
297
|
...(reinterpret?.(url, (name) => res.headers.get(name)) ?? {}),
|
|
@@ -252,7 +307,9 @@ export function installNetwork(emit, opts = {}) {
|
|
|
252
307
|
catch {
|
|
253
308
|
}
|
|
254
309
|
void (async () => {
|
|
255
|
-
let responseBodyFields =
|
|
310
|
+
let responseBodyFields = captureBodies
|
|
311
|
+
? { responseBodyReason: ResponseBodyReason.UNREADABLE }
|
|
312
|
+
: {};
|
|
256
313
|
if (clone !== undefined) {
|
|
257
314
|
try {
|
|
258
315
|
const text = await withBodyDeadline(clone.text());
|
|
@@ -263,7 +320,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
263
320
|
responseBodyFields = {
|
|
264
321
|
responseBody: body,
|
|
265
322
|
...(truncated ? { responseBodyTruncated: true } : {}),
|
|
266
|
-
...(captureBodies ? {} : { responseBodyReason:
|
|
323
|
+
...(captureBodies ? {} : { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
|
|
267
324
|
};
|
|
268
325
|
}
|
|
269
326
|
}
|
|
@@ -278,7 +335,8 @@ export function installNetwork(emit, opts = {}) {
|
|
|
278
335
|
})().catch(() => undefined);
|
|
279
336
|
}
|
|
280
337
|
else {
|
|
281
|
-
|
|
338
|
+
const skipped = captureBodies ? skippedBodyReason(contentType) : undefined;
|
|
339
|
+
emitRequest(skipped === undefined ? {} : { responseBodyReason: skipped });
|
|
282
340
|
}
|
|
283
341
|
return res;
|
|
284
342
|
}
|
|
@@ -326,7 +384,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
326
384
|
const m = meta.get(this);
|
|
327
385
|
if (m !== undefined) {
|
|
328
386
|
m.start = performance.now();
|
|
329
|
-
m.
|
|
387
|
+
m.reqFields = observeValue(() => projectRequestBody(body ?? null, captureBodies)) ?? {};
|
|
330
388
|
m.initiatorStack = initiatorFrame();
|
|
331
389
|
const initiatorFields = m.initiatorStack === undefined ? {} : { initiatorStack: m.initiatorStack };
|
|
332
390
|
emit(EventType.NET_PENDING, {
|
|
@@ -348,6 +406,13 @@ export function installNetwork(emit, opts = {}) {
|
|
|
348
406
|
let responseBodyFields = {};
|
|
349
407
|
const textReadable = '' === this.responseType || 'text' === this.responseType;
|
|
350
408
|
const wantXhrErrorBody = shouldCaptureErrorBody(this.status, captureErrorBodies);
|
|
409
|
+
if (captureBodies) {
|
|
410
|
+
const skipped = isCapturableType(xhrContentType)
|
|
411
|
+
? ResponseBodyReason.UNREADABLE
|
|
412
|
+
: skippedBodyReason(xhrContentType);
|
|
413
|
+
if (skipped !== undefined)
|
|
414
|
+
responseBodyFields = { responseBodyReason: skipped };
|
|
415
|
+
}
|
|
351
416
|
if ((captureBodies || wantXhrErrorBody) &&
|
|
352
417
|
textReadable &&
|
|
353
418
|
isCapturableType(xhrContentType)) {
|
|
@@ -358,7 +423,9 @@ export function installNetwork(emit, opts = {}) {
|
|
|
358
423
|
responseBodyFields = {
|
|
359
424
|
responseBody: rb,
|
|
360
425
|
...(truncated ? { responseBodyTruncated: true } : {}),
|
|
361
|
-
...(captureBodies
|
|
426
|
+
...(captureBodies
|
|
427
|
+
? {}
|
|
428
|
+
: { responseBodyReason: ResponseBodyReason.ERROR_STATUS }),
|
|
362
429
|
};
|
|
363
430
|
}
|
|
364
431
|
catch {
|
|
@@ -375,7 +442,7 @@ export function installNetwork(emit, opts = {}) {
|
|
|
375
442
|
...(cur.initiatorStack === undefined ? {} : { initiatorStack: cur.initiatorStack }),
|
|
376
443
|
...resourceTiming(cur.rawUrl),
|
|
377
444
|
...netResponseMeta(this.statusText, xhrContentType, this.getResponseHeader('content-length')),
|
|
378
|
-
...
|
|
445
|
+
...cur.reqFields,
|
|
379
446
|
...responseBodyFields,
|
|
380
447
|
});
|
|
381
448
|
});
|
|
@@ -530,6 +597,8 @@ export function installNetwork(emit, opts = {}) {
|
|
|
530
597
|
}
|
|
531
598
|
}
|
|
532
599
|
return () => {
|
|
600
|
+
if (pageSlots()[PAGE_CAPTURE] === owner)
|
|
601
|
+
delete pageSlots()[PAGE_CAPTURE];
|
|
533
602
|
subresourceObserver?.disconnect();
|
|
534
603
|
if (window.fetch === patchedFetch)
|
|
535
604
|
window.fetch = origFetch;
|
package/dist/observers/perf.js
CHANGED
|
@@ -1,4 +1,50 @@
|
|
|
1
1
|
import { EventType, PerfMetric } from '@reticlehq/core';
|
|
2
|
+
import { refs } from '../dom/addressing/refs.js';
|
|
3
|
+
import { getTestIdAttr, readTestId } from '../dom/addressing/testid-attr.js';
|
|
4
|
+
import { isReticleOverlay } from '../dom/dom-ignore.js';
|
|
5
|
+
import { isElement } from '../dom/realm.js';
|
|
6
|
+
const areaOf = (r) => r === undefined ? 0 : Math.max(0, r.width) * Math.max(0, r.height);
|
|
7
|
+
function cssIdent(value) {
|
|
8
|
+
const css = globalThis.CSS;
|
|
9
|
+
if ('function' === typeof css?.escape)
|
|
10
|
+
return css.escape(value);
|
|
11
|
+
let out = '';
|
|
12
|
+
for (const [i, ch] of [...value].entries()) {
|
|
13
|
+
const startsWithDigit = /\d/.test(ch) && (0 === i || (1 === i && value.startsWith('-')));
|
|
14
|
+
if (startsWithDigit)
|
|
15
|
+
out += `\\${ch.charCodeAt(0).toString(16)} `;
|
|
16
|
+
else if (/[\w-]/.test(ch))
|
|
17
|
+
out += ch;
|
|
18
|
+
else
|
|
19
|
+
out += `\\${ch}`;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function shortSelector(el) {
|
|
24
|
+
const testid = readTestId(el);
|
|
25
|
+
if (null !== testid)
|
|
26
|
+
return `[${getTestIdAttr()}="${testid.replace(/["\\]/g, '\\$&')}"]`;
|
|
27
|
+
const tag = el.tagName.toLowerCase();
|
|
28
|
+
if (0 < el.id.length)
|
|
29
|
+
return `${tag}#${cssIdent(el.id)}`;
|
|
30
|
+
const cls = el.classList.item(0);
|
|
31
|
+
return null === cls ? tag : `${tag}.${cssIdent(cls)}`;
|
|
32
|
+
}
|
|
33
|
+
function largestShifted(sources) {
|
|
34
|
+
let best;
|
|
35
|
+
for (const source of sources ?? []) {
|
|
36
|
+
const node = source.node ?? null;
|
|
37
|
+
const el = null === node ? null : isElement(node) ? node : node.parentElement;
|
|
38
|
+
if (null === el || isReticleOverlay(el))
|
|
39
|
+
continue;
|
|
40
|
+
const area = Math.max(areaOf(source.previousRect), areaOf(source.currentRect));
|
|
41
|
+
if (best === undefined || area > best.area)
|
|
42
|
+
best = { el, area };
|
|
43
|
+
}
|
|
44
|
+
return best === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: { ref: refs.refFor(best.el), selector: shortSelector(best.el) };
|
|
47
|
+
}
|
|
2
48
|
export function installPerf(emit) {
|
|
3
49
|
if (typeof PerformanceObserver !== 'function')
|
|
4
50
|
return () => undefined;
|
|
@@ -29,7 +75,13 @@ export function installPerf(emit) {
|
|
|
29
75
|
if (true === ls.hadRecentInput)
|
|
30
76
|
return;
|
|
31
77
|
cls += ls.value ?? 0;
|
|
32
|
-
|
|
78
|
+
const shifted = largestShifted(ls.sources);
|
|
79
|
+
emit(EventType.PERF, {
|
|
80
|
+
metric: PerfMetric.CLS,
|
|
81
|
+
value: cls,
|
|
82
|
+
at: Math.round(e.startTime),
|
|
83
|
+
...(shifted === undefined ? {} : { shifted }),
|
|
84
|
+
});
|
|
33
85
|
});
|
|
34
86
|
observe('longtask', (e) => {
|
|
35
87
|
emit(EventType.PERF, {
|
package/dist/observers/route.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { EventType } from '@reticlehq/core';
|
|
1
|
+
import { EventType, ROUTE_CHANGE_HOW_FIELD, RouteChangeHow } from '@reticlehq/core';
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
import { captureMethod } from '../patching/capture-method.js';
|
|
4
|
+
import { redactUrl } from '@reticlehq/core';
|
|
4
5
|
function snapshotLocation() {
|
|
5
6
|
return {
|
|
6
7
|
pathname: location.pathname,
|
|
@@ -9,6 +10,32 @@ function snapshotLocation() {
|
|
|
9
10
|
href: location.href,
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
let declared = [];
|
|
14
|
+
export function declareRoutes(patterns) {
|
|
15
|
+
declared = patterns.map((pattern) => ({ pattern, parts: pattern.split('/').filter(Boolean) }));
|
|
16
|
+
}
|
|
17
|
+
function nextPattern() {
|
|
18
|
+
const raw = globalThis.next?.router?.pathname;
|
|
19
|
+
if ('string' !== typeof raw || !raw.startsWith('/'))
|
|
20
|
+
return undefined;
|
|
21
|
+
return raw.replace(/\[\.\.\.[^\]]+\]/g, '*').replace(/\[([^\]]+)\]/g, ':$1');
|
|
22
|
+
}
|
|
23
|
+
export function patternOf(pathname) {
|
|
24
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
25
|
+
let best;
|
|
26
|
+
for (const { pattern, parts } of declared) {
|
|
27
|
+
const rest = '*' === parts.at(-1);
|
|
28
|
+
const fixed = rest ? parts.slice(0, -1) : parts;
|
|
29
|
+
if (rest ? segments.length < fixed.length : segments.length !== fixed.length)
|
|
30
|
+
continue;
|
|
31
|
+
if (!fixed.every((part, i) => part.startsWith(':') || part === segments[i]))
|
|
32
|
+
continue;
|
|
33
|
+
const params = parts.filter((part) => part.startsWith(':') || '*' === part).length;
|
|
34
|
+
if (best === undefined || params < best.params)
|
|
35
|
+
best = { pattern, params };
|
|
36
|
+
}
|
|
37
|
+
return best?.pattern ?? nextPattern();
|
|
38
|
+
}
|
|
12
39
|
export function installRoute(emit) {
|
|
13
40
|
const ac = new AbortController();
|
|
14
41
|
const origPush = captureMethod(history, 'pushState');
|
|
@@ -16,35 +43,38 @@ export function installRoute(emit) {
|
|
|
16
43
|
const callPush = origPush.bind(history);
|
|
17
44
|
const callReplace = origReplace.bind(history);
|
|
18
45
|
let lastHref = location.href;
|
|
19
|
-
const fire = (from) => {
|
|
46
|
+
const fire = (from, how) => {
|
|
20
47
|
observeSafely(() => {
|
|
21
48
|
const to = snapshotLocation();
|
|
22
49
|
if (to.href === from)
|
|
23
50
|
return;
|
|
24
51
|
lastHref = to.href;
|
|
52
|
+
const pattern = patternOf(to.pathname);
|
|
25
53
|
emit(EventType.ROUTE_CHANGE, {
|
|
26
|
-
from,
|
|
27
|
-
to: to.href,
|
|
54
|
+
from: redactUrl(from),
|
|
55
|
+
to: redactUrl(to.href),
|
|
28
56
|
pathname: to.pathname,
|
|
29
|
-
|
|
30
|
-
|
|
57
|
+
...(pattern === undefined ? {} : { pattern }),
|
|
58
|
+
search: redactUrl(to.search),
|
|
59
|
+
hash: redactUrl(to.hash),
|
|
60
|
+
[ROUTE_CHANGE_HOW_FIELD]: how,
|
|
31
61
|
});
|
|
32
62
|
});
|
|
33
63
|
};
|
|
34
64
|
const patchedPush = (data, unused, url) => {
|
|
35
65
|
const from = location.href;
|
|
36
66
|
callPush(data, unused, url ?? null);
|
|
37
|
-
fire(from);
|
|
67
|
+
fire(from, RouteChangeHow.PUSH);
|
|
38
68
|
};
|
|
39
69
|
const patchedReplace = (data, unused, url) => {
|
|
40
70
|
const from = location.href;
|
|
41
71
|
callReplace(data, unused, url ?? null);
|
|
42
|
-
fire(from);
|
|
72
|
+
fire(from, RouteChangeHow.REPLACE);
|
|
43
73
|
};
|
|
44
74
|
history.pushState = patchedPush;
|
|
45
75
|
history.replaceState = patchedReplace;
|
|
46
76
|
const onNav = () => {
|
|
47
|
-
fire(lastHref);
|
|
77
|
+
fire(lastHref, RouteChangeHow.POP);
|
|
48
78
|
};
|
|
49
79
|
window.addEventListener('popstate', onNav, { signal: ac.signal });
|
|
50
80
|
window.addEventListener('hashchange', onNav, { signal: ac.signal });
|
|
@@ -135,7 +135,7 @@ function startRotation(log, root, count) {
|
|
|
135
135
|
export const CAROUSEL_CSS = `
|
|
136
136
|
.reticle-carousel{position:relative;margin:8px 10px;padding:10px 12px 8px;border:1px solid var(--reticle-line,#2a2f3a);border-radius:10px;background:var(--reticle-surface-inset,rgba(255,255,255,.03));}
|
|
137
137
|
.reticle-carousel-track{height:100px;overflow:hidden;}
|
|
138
|
-
.reticle-carousel-slide .reticle-
|
|
138
|
+
.reticle-carousel-slide .reticle-talk{margin:0;padding:0;border:0;background:none;}
|
|
139
139
|
.reticle-carousel-close{position:absolute;top:4px;right:6px;width:20px;height:20px;padding:0;border:0;background:none;color:inherit;opacity:.55;cursor:pointer;font-size:14px;line-height:20px;}
|
|
140
140
|
.reticle-carousel-close:hover{opacity:1;}
|
|
141
141
|
.reticle-carousel-dots{display:flex;justify-content:center;gap:6px;margin-top:6px;}
|
|
@@ -144,10 +144,5 @@ export const CAROUSEL_CSS = `
|
|
|
144
144
|
.reticle-carousel-controls{display:flex;align-items:center;gap:8px;margin-top:6px;}
|
|
145
145
|
.reticle-carousel-arrow{width:24px;height:24px;flex:none;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--reticle-line);border-radius:6px;background:rgba(255,255,255,.04);color:var(--reticle-fg);font:inherit;font-size:18px;line-height:1;cursor:pointer;}
|
|
146
146
|
.reticle-carousel-arrow:hover{border-color:var(--reticle-accent);background:var(--reticle-accent-soft);}
|
|
147
|
-
.reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:
|
|
148
|
-
.reticle-carousel-slide .reticle-offer{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:3px 8px;margin:0;padding:0;border:0;background:none;}
|
|
149
|
-
.reticle-carousel-slide .reticle-offer-title{grid-column:1/-1;overflow:hidden;padding:0;font-size:11px;text-overflow:ellipsis;white-space:nowrap;}
|
|
150
|
-
.reticle-carousel-slide .reticle-offer-body{grid-column:1;grid-row:2;margin:0;overflow:hidden;font-size:10px;line-height:1.2;text-overflow:ellipsis;white-space:nowrap;}
|
|
151
|
-
.reticle-carousel-slide .reticle-offer-claim{grid-column:2;grid-row:2;padding:3px 7px;font-size:10px;white-space:nowrap;}
|
|
152
|
-
.reticle-carousel-slide .reticle-offer-claimed{overflow:hidden;margin:0;text-overflow:ellipsis;white-space:nowrap;}
|
|
147
|
+
.reticle-carousel-position{margin-left:auto;color:var(--reticle-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap;}
|
|
153
148
|
`;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { type OfferState } from './offer-card.js';
|
|
2
1
|
import type { Slide } from './carousel.js';
|
|
3
2
|
export declare const SLIDE_ID: {
|
|
4
|
-
readonly HARNESS: "harness-journeys";
|
|
5
3
|
readonly REPLAY: "replay-confidence";
|
|
6
|
-
readonly
|
|
4
|
+
readonly NOTES: "notes-for-agent";
|
|
7
5
|
};
|
|
8
|
-
export declare function panelSlides(
|
|
6
|
+
export declare function panelSlides(notices?: readonly unknown[], base?: string): Slide[];
|
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
import { offerHtml } from './offer-card.js';
|
|
2
1
|
import { HudNoticeSchema } from '@reticlehq/core/hud';
|
|
3
2
|
import { esc } from '../chrome/presenter-safe-html.js';
|
|
4
3
|
import { DEFAULT_PLATFORM_URL } from '@reticlehq/core';
|
|
5
4
|
const HARNESS_PATH = '/settings?group=billing';
|
|
6
5
|
export const SLIDE_ID = {
|
|
7
|
-
HARNESS: 'harness-journeys',
|
|
8
6
|
REPLAY: 'replay-confidence',
|
|
9
|
-
|
|
7
|
+
NOTES: 'notes-for-agent',
|
|
8
|
+
};
|
|
9
|
+
const SLIDE_TEXT = {
|
|
10
|
+
REPLAY_TITLE: 'Replay what your agent verified',
|
|
11
|
+
REPLAY_DETAIL: 'Saved flows re-run after each edit.',
|
|
12
|
+
REPLAY_LINK: 'See how →',
|
|
13
|
+
NOTES_TITLE: 'Point your agent at anything',
|
|
14
|
+
NOTES_DETAIL: 'Open Notes, then click an element.',
|
|
10
15
|
};
|
|
11
16
|
const valueSlides = (base) => [
|
|
12
17
|
{
|
|
13
|
-
id: SLIDE_ID.
|
|
14
|
-
html: `<div class="reticle-promo-copy"><
|
|
18
|
+
id: SLIDE_ID.REPLAY,
|
|
19
|
+
html: `<div class="reticle-promo-copy"><strong class="reticle-promo-title">${SLIDE_TEXT.REPLAY_TITLE}</strong><span class="reticle-promo-detail">${SLIDE_TEXT.REPLAY_DETAIL}</span><a data-reticle-promo-link class="reticle-promo-link" href="${base}${HARNESS_PATH}" target="_blank" rel="noopener noreferrer">${SLIDE_TEXT.REPLAY_LINK}</a></div>`,
|
|
15
20
|
},
|
|
16
21
|
{
|
|
17
|
-
id: SLIDE_ID.
|
|
18
|
-
html: `<div class="reticle-promo-copy"><
|
|
22
|
+
id: SLIDE_ID.NOTES,
|
|
23
|
+
html: `<div class="reticle-promo-copy"><strong class="reticle-promo-title">${SLIDE_TEXT.NOTES_TITLE}</strong><span class="reticle-promo-detail">${SLIDE_TEXT.NOTES_DETAIL}</span></div>`,
|
|
19
24
|
},
|
|
20
25
|
];
|
|
26
|
+
const HARNESS_NOTICE_PREFIX = 'harness';
|
|
21
27
|
const NOTICE_SLIDE_PREFIX = 'notice-';
|
|
22
28
|
function noticeSlide(notice) {
|
|
23
29
|
const kicker = notice.kicker === undefined
|
|
@@ -25,7 +31,7 @@ function noticeSlide(notice) {
|
|
|
25
31
|
: `<span class="reticle-promo-kicker">${esc(notice.kicker)}</span>`;
|
|
26
32
|
const detail = notice.detail === undefined
|
|
27
33
|
? ''
|
|
28
|
-
: `<span class="reticle-promo-detail">${esc(notice.detail)}</span>`;
|
|
34
|
+
: `<span class="reticle-promo-detail" title="${esc(notice.detail)}">${esc(notice.detail)}</span>`;
|
|
29
35
|
const link = notice.cta === undefined
|
|
30
36
|
? ''
|
|
31
37
|
: `<a data-reticle-promo-link class="reticle-promo-link" href="${esc(notice.cta.url)}" target="_blank" rel="noopener noreferrer">${esc(notice.cta.label)} →</a>`;
|
|
@@ -34,14 +40,10 @@ function noticeSlide(notice) {
|
|
|
34
40
|
html: `<div class="reticle-promo-copy">${kicker}<strong class="reticle-promo-title">${esc(notice.title)}</strong>${detail}${link}</div>`,
|
|
35
41
|
};
|
|
36
42
|
}
|
|
37
|
-
export function panelSlides(
|
|
38
|
-
const harnessOffer = offerHtml(offer, offerDeclined);
|
|
43
|
+
export function panelSlides(notices = [], base = DEFAULT_PLATFORM_URL) {
|
|
39
44
|
const valid = notices.flatMap((n) => {
|
|
40
45
|
const parsed = HudNoticeSchema.safeParse(n);
|
|
41
|
-
return parsed.success ? [parsed.data] : [];
|
|
46
|
+
return parsed.success && !parsed.data.id.startsWith(HARNESS_NOTICE_PREFIX) ? [parsed.data] : [];
|
|
42
47
|
});
|
|
43
|
-
return [
|
|
44
|
-
...('' === harnessOffer ? [] : [{ id: SLIDE_ID.OFFER, html: harnessOffer }]),
|
|
45
|
-
...(0 < valid.length ? valid.map(noticeSlide) : valueSlides(base)),
|
|
46
|
-
];
|
|
48
|
+
return 0 < valid.length ? valid.map(noticeSlide) : [...valueSlides(base)];
|
|
47
49
|
}
|
|
@@ -3,8 +3,15 @@ import { LOG_ACTOR, LOG_KIND, LOG_RESULT, type LogActor, type LogKind, type LogR
|
|
|
3
3
|
export { LOG_ACTOR, LOG_KIND, LOG_RESULT };
|
|
4
4
|
export type { LogActor, LogKind, LogResult };
|
|
5
5
|
export declare const CHIP_LABEL: Record<PresenterMode, string>;
|
|
6
|
+
export declare const LAST_ACTOR_ATTR = "data-reticle-log-actor";
|
|
6
7
|
export declare const DATA_RETICLE_LOG = "data-reticle-log";
|
|
7
8
|
export declare const LOG_TIME_ATTR = "data-reticle-log-time";
|
|
9
|
+
export declare const LOG_EMPTY_TEXT: {
|
|
10
|
+
readonly TITLE: "Reticle checks your app from the inside.";
|
|
11
|
+
readonly OFFLINE_TITLE: "Not connected yet.";
|
|
12
|
+
readonly OFFLINE_BODY: "Once Reticle is running, this page connects by itself and the steps show here.";
|
|
13
|
+
readonly BODY: "Ask your coding agent to verify a change with Reticle: each step shows here, live.\\AOr open Harness below: Reticle clicks through the app as the person you pick and reports what breaks.";
|
|
14
|
+
};
|
|
8
15
|
export declare const LOG_CSS: string;
|
|
9
16
|
export declare const LOG_SCROLLABLE_ATTR = "data-reticle-log-scrollable";
|
|
10
17
|
export declare function settleLogAtLatest(container: HTMLElement): void;
|
|
@@ -25,7 +25,7 @@ const HANDOVER_TEXT = {
|
|
|
25
25
|
agent: 'Your agent is driving',
|
|
26
26
|
harness: 'Reticle Harness is driving',
|
|
27
27
|
};
|
|
28
|
-
const LAST_ACTOR_ATTR = 'data-reticle-log-actor';
|
|
28
|
+
export const LAST_ACTOR_ATTR = 'data-reticle-log-actor';
|
|
29
29
|
const DATA_ACTOR = 'data-actor';
|
|
30
30
|
const DATA_STATE = 'data-state';
|
|
31
31
|
const HANDOVER_KIND = 'handover';
|
|
@@ -35,18 +35,29 @@ const DATA_RETICLE_LOG_ROW = 'data-reticle-log-row';
|
|
|
35
35
|
export const LOG_TIME_ATTR = 'data-reticle-log-time';
|
|
36
36
|
const DATA_KIND = 'data-kind';
|
|
37
37
|
const LOG_TEXT_CLASS = 'reticle-log-text';
|
|
38
|
+
const REPEAT_ATTR = 'data-reticle-repeat';
|
|
39
|
+
const LOG_REPEAT_CLASS = 'reticle-log-repeat';
|
|
38
40
|
const LOG_RES_CLASS = 'reticle-res';
|
|
39
41
|
const LOG_CHIP_CLASS = 'reticle-chip';
|
|
40
|
-
const
|
|
42
|
+
export const LOG_EMPTY_TEXT = {
|
|
43
|
+
TITLE: 'Reticle checks your app from the inside.',
|
|
44
|
+
OFFLINE_TITLE: 'Not connected yet.',
|
|
45
|
+
OFFLINE_BODY: 'Once Reticle is running, this page connects by itself and the steps show here.',
|
|
46
|
+
BODY: 'Ask your coding agent to verify a change with Reticle: each step shows here, live.\\AOr open Harness below: Reticle clicks through the app as the person you pick and reports what breaks.',
|
|
47
|
+
};
|
|
41
48
|
const LOG_SETTLE_MS = 160;
|
|
42
49
|
export const LOG_CSS = `
|
|
43
50
|
[data-reticle-log]{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;pointer-events:auto;touch-action:pan-y;
|
|
44
51
|
display:flex;flex-direction:column;
|
|
45
52
|
gap:4px;padding:8px 10px 10px;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.16) transparent;
|
|
46
53
|
background:transparent;}
|
|
47
|
-
[data-reticle-log]:empty{align-items:center;justify-content:center;}
|
|
48
|
-
[data-reticle-log]:empty::
|
|
49
|
-
color:var(--reticle-
|
|
54
|
+
[data-reticle-log]:empty{align-items:center;justify-content:center;gap:6px;padding:16px 18px;}
|
|
55
|
+
[data-reticle-log]:empty::before{content:"${LOG_EMPTY_TEXT.TITLE}";display:block;text-align:center;
|
|
56
|
+
color:var(--reticle-hud-text);font-size:var(--reticle-hud-size-base);font-weight:600;line-height:1.4;}
|
|
57
|
+
[data-reticle-log]:empty::after{content:"${LOG_EMPTY_TEXT.BODY}";display:block;max-width:260px;text-align:center;white-space:pre-line;
|
|
58
|
+
color:var(--reticle-hud-text-muted);font-size:var(--reticle-hud-size-sm);line-height:1.5;}
|
|
59
|
+
[data-reticle-state="unreachable"] [data-reticle-log]:empty::before{content:"${LOG_EMPTY_TEXT.OFFLINE_TITLE}";}
|
|
60
|
+
[data-reticle-state="unreachable"] [data-reticle-log]:empty::after{content:"${LOG_EMPTY_TEXT.OFFLINE_BODY}";}
|
|
50
61
|
[data-reticle-log]::-webkit-scrollbar{width:8px;}
|
|
51
62
|
[data-reticle-log]::-webkit-scrollbar-thumb{background:rgba(255,255,255,.14);border-radius:8px;border:2px solid transparent;background-clip:content-box;}
|
|
52
63
|
[data-reticle-log]::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.26);background-clip:content-box;}
|
|
@@ -69,8 +80,9 @@ export const LOG_CSS = `
|
|
|
69
80
|
[data-reticle-log] [data-reticle-log-row] .reticle-chip-label{
|
|
70
81
|
position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
|
|
71
82
|
[data-reticle-log] [data-reticle-log-row][data-kind="read"] .reticle-chip{opacity:.5;}
|
|
72
|
-
[${LOG_TIME_ATTR}]{flex:none;color:var(--reticle-faint);font-size:
|
|
83
|
+
[${LOG_TIME_ATTR}]{flex:none;color:var(--reticle-faint);font-size:11px;font-variant-numeric:tabular-nums;padding-top:2px;min-width:2em;opacity:.85;}
|
|
73
84
|
[data-reticle-log] .reticle-log-text{flex:1;min-width:0;color:var(--reticle-muted);overflow-wrap:anywhere;word-break:break-word;}
|
|
85
|
+
[data-reticle-log] .reticle-log-repeat{flex:none;color:var(--reticle-muted);font-variant-numeric:tabular-nums;opacity:.7;}
|
|
74
86
|
[data-reticle-log] .reticle-res{flex:none;font-size:7.5px;font-weight:600;letter-spacing:.05em;text-transform:uppercase;color:var(--reticle-bad);opacity:.75;padding-top:2px;}
|
|
75
87
|
[data-reticle-log] .reticle-res.reticle-pass{display:none;}
|
|
76
88
|
[data-reticle-log-row][data-kind="human"]{align-self:flex-end;max-width:78%;margin:6px 0 2px;
|
|
@@ -85,14 +97,14 @@ export const LOG_CSS = `
|
|
|
85
97
|
[data-reticle-log-row][data-state="running"] .reticle-res{display:inline-block;width:6px;height:6px;margin-top:5px;
|
|
86
98
|
border-radius:50%;background:var(--reticle-c-active);opacity:1;animation:reticle-row-pulse 1s ease-in-out infinite;}
|
|
87
99
|
@keyframes reticle-row-pulse{0%,100%{opacity:.35;transform:scale(.8)}50%{opacity:1;transform:scale(1)}}
|
|
88
|
-
[data-reticle-log] .reticle-res.reticle-pass{display:inline;color:#4ade80;font-size:
|
|
100
|
+
[data-reticle-log] .reticle-res.reticle-pass{display:inline;color:#4ade80;font-size:11px;opacity:.9;}
|
|
89
101
|
[data-reticle-log] .reticle-res.reticle-fail{color:#fca5a5;background:rgba(239,68,68,.16);border-radius:999px;padding:1px 6px;font-size:8px;opacity:1;}
|
|
90
102
|
/* The Harness's rows wear its own colour, so two drivers never read as one. */
|
|
91
103
|
[data-reticle-log-row][data-actor="harness"]{--reticle-row-accent:#a78bfa;}
|
|
92
104
|
[data-reticle-log-row][data-actor="harness"] .reticle-chip{color:#a78bfa;}
|
|
93
105
|
[data-reticle-log-row][data-actor="harness"]:not([data-kind="handover"]){box-shadow:inset 2px 0 0 color-mix(in srgb,#a78bfa 55%,transparent);padding-left:8px;}
|
|
94
106
|
[data-reticle-log-row][data-kind="handover"]{align-items:center;gap:8px;margin:8px 0 4px;padding:0;
|
|
95
|
-
font-size:
|
|
107
|
+
font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--reticle-row-accent,var(--reticle-c-active));}
|
|
96
108
|
[data-reticle-log-row][data-kind="handover"]::before,[data-reticle-log-row][data-kind="handover"]::after{
|
|
97
109
|
content:"";flex:1;height:1px;background:color-mix(in srgb,var(--reticle-row-accent,var(--reticle-c-active)) 40%,transparent);}
|
|
98
110
|
[data-reticle-log-row][data-kind="handover"] .reticle-log-text{flex:none;color:inherit;}
|
|
@@ -144,6 +156,23 @@ export function formatElapsed(ms) {
|
|
|
144
156
|
return humanDuration(ms);
|
|
145
157
|
}
|
|
146
158
|
export function appendLogRow(container, kind, text, ts, logMax, actor = LOG_ACTOR.AGENT) {
|
|
159
|
+
const repeated = kind === LOG_KIND.READ ? sameReadAsLast(container, text, actor) : undefined;
|
|
160
|
+
if (repeated !== undefined) {
|
|
161
|
+
const times = Number(repeated.getAttribute(REPEAT_ATTR) ?? '1') + 1;
|
|
162
|
+
repeated.setAttribute(REPEAT_ATTR, String(times));
|
|
163
|
+
let count = repeated.querySelector(`.${LOG_REPEAT_CLASS}`);
|
|
164
|
+
if (null === count) {
|
|
165
|
+
count = document.createElement('span');
|
|
166
|
+
count.className = LOG_REPEAT_CLASS;
|
|
167
|
+
repeated.querySelector(`.${LOG_TEXT_CLASS}`)?.after(count);
|
|
168
|
+
}
|
|
169
|
+
count.textContent = `×${String(times)}`;
|
|
170
|
+
const time = repeated.querySelector(`[${LOG_TIME_ATTR}]`);
|
|
171
|
+
if (null !== time)
|
|
172
|
+
time.textContent = ts;
|
|
173
|
+
const res = repeated.querySelector(`.${LOG_RES_CLASS}`);
|
|
174
|
+
return null === res ? { result: () => undefined } : rowHandle(repeated, res);
|
|
175
|
+
}
|
|
147
176
|
const driven = kind === LOG_KIND.ACT || kind === LOG_KIND.READ;
|
|
148
177
|
if (driven && (container.getAttribute(LAST_ACTOR_ATTR) ?? LOG_ACTOR.AGENT) !== actor) {
|
|
149
178
|
const divider = document.createElement('div');
|
|
@@ -200,6 +229,17 @@ export function appendLogRow(container, kind, text, ts, logMax, actor = LOG_ACTO
|
|
|
200
229
|
requestAnimationFrame(() => {
|
|
201
230
|
scrollLogToLatest(container);
|
|
202
231
|
});
|
|
232
|
+
return rowHandle(row, resEl);
|
|
233
|
+
}
|
|
234
|
+
function sameReadAsLast(container, text, actor) {
|
|
235
|
+
const last = container.lastElementChild;
|
|
236
|
+
if (!(last instanceof HTMLElement) || !last.hasAttribute(DATA_RETICLE_LOG_ROW))
|
|
237
|
+
return undefined;
|
|
238
|
+
if (last.getAttribute(DATA_KIND) !== LOG_KIND.READ || last.getAttribute(DATA_ACTOR) !== actor)
|
|
239
|
+
return undefined;
|
|
240
|
+
return last.querySelector(`.${LOG_TEXT_CLASS}`)?.textContent === text ? last : undefined;
|
|
241
|
+
}
|
|
242
|
+
function rowHandle(row, resEl) {
|
|
203
243
|
return {
|
|
204
244
|
result: (r) => {
|
|
205
245
|
row.setAttribute(DATA_STATE, r);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function morphHtml(target: Element, html: string): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function morphHtml(target, html) {
|
|
2
|
+
const next = document.createElement('template');
|
|
3
|
+
next.innerHTML = html;
|
|
4
|
+
morphChildren(target, next.content);
|
|
5
|
+
}
|
|
6
|
+
function morphChildren(from, to) {
|
|
7
|
+
const have = [...from.childNodes];
|
|
8
|
+
const want = [...to.childNodes];
|
|
9
|
+
want.forEach((node, i) => {
|
|
10
|
+
const old = have[i];
|
|
11
|
+
if (old === undefined)
|
|
12
|
+
from.appendChild(node);
|
|
13
|
+
else if (old.nodeType === node.nodeType && old.nodeName === node.nodeName)
|
|
14
|
+
morphNode(old, node);
|
|
15
|
+
else
|
|
16
|
+
from.replaceChild(node, old);
|
|
17
|
+
});
|
|
18
|
+
for (const extra of have.slice(want.length))
|
|
19
|
+
from.removeChild(extra);
|
|
20
|
+
}
|
|
21
|
+
function morphNode(old, node) {
|
|
22
|
+
if (!(old instanceof Element) || !(node instanceof Element)) {
|
|
23
|
+
if (old.nodeValue !== node.nodeValue)
|
|
24
|
+
old.nodeValue = node.nodeValue;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
for (const { name } of [...old.attributes])
|
|
28
|
+
if (!node.hasAttribute(name))
|
|
29
|
+
old.removeAttribute(name);
|
|
30
|
+
for (const { name, value } of [...node.attributes])
|
|
31
|
+
if (old.getAttribute(name) !== value)
|
|
32
|
+
old.setAttribute(name, value);
|
|
33
|
+
morphChildren(old, node);
|
|
34
|
+
}
|