browsertrack 0.2.1 → 0.2.3
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/AGENTS.md +9 -6
- package/README.md +2 -0
- package/dist/{chunk-464D4U2U.js → chunk-5NR5K3ER.js} +299 -44
- package/dist/chunk-5NR5K3ER.js.map +1 -0
- package/dist/{chunk-3HOXPTM2.js → chunk-G5CIZSQM.js} +808 -53
- package/dist/chunk-G5CIZSQM.js.map +1 -0
- package/dist/{chunk-6VA7GBAO.js → chunk-ONPW7AYL.js} +144 -12
- package/dist/chunk-ONPW7AYL.js.map +1 -0
- package/dist/{chunk-INXDWPJW.js → chunk-PG4JJDCV.js} +353 -119
- package/dist/chunk-PG4JJDCV.js.map +1 -0
- package/dist/cli/index.js +1058 -546
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +449 -128
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -2
- package/dist/client.iife.js +121 -15
- package/dist/core/index.d.ts +32 -2
- package/dist/core/index.js +11 -1
- package/dist/daemon/index.d.ts +2 -2
- package/dist/daemon/index.js +6 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -7
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +7 -4
- package/dist/{server-DiVmTrIR.d.ts → server-BRG-RQQP.d.ts} +10 -1
- package/docs/cli.md +4 -1
- package/docs/getting-started.md +60 -6
- package/docs/mcp-reference.md +42 -0
- package/package.json +1 -1
- package/packages/cli/src/index.ts +247 -151
- package/packages/client/src/interceptors/navigation.ts +38 -26
- package/packages/client/src/interceptors/network.ts +22 -17
- package/packages/client/src/notes/inspector.ts +289 -65
- package/packages/client/src/source/resolver.ts +9 -3
- package/packages/client/src/transport/websocket.ts +23 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/safety.ts +86 -0
- package/packages/core/src/selector.ts +110 -15
- package/packages/daemon/src/server/daemon.ts +7 -1
- package/packages/daemon/src/server/http.ts +125 -5
- package/packages/daemon/src/server/ws.ts +33 -29
- package/packages/daemon/src/storage/db.ts +57 -35
- package/packages/mcp/src/handlers.ts +121 -45
- package/packages/mcp/src/server.ts +221 -3
- package/test/core/safety.test.ts +106 -0
- package/test/core/selector.test.ts +133 -0
- package/test/daemon/storage.test.ts +36 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
- package/test/mcp/auto-start.test.ts +87 -0
- package/dist/chunk-3HOXPTM2.js.map +0 -1
- package/dist/chunk-464D4U2U.js.map +0 -1
- package/dist/chunk-6VA7GBAO.js.map +0 -1
- package/dist/chunk-7OCOQGDN.js +0 -635
- package/dist/chunk-7OCOQGDN.js.map +0 -1
- package/dist/chunk-INXDWPJW.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ElementContext, NoteTarget, RegionContext, VisualNote } from '../../../core/src/index.js';
|
|
2
|
-
import { getSemanticSelector, truncate } from '../../../core/src/index.js';
|
|
2
|
+
import { getSemanticSelector, resolveMeaningfulTarget, truncate } from '../../../core/src/index.js';
|
|
3
3
|
import type { ScreenshotDriver } from '../screenshot/driver.js';
|
|
4
4
|
import type { WebSocketTransport } from '../transport/websocket.js';
|
|
5
5
|
import { shouldHideUIFromUrl } from '../config.js';
|
|
@@ -65,6 +65,7 @@ export class NoteInspector {
|
|
|
65
65
|
private savedNotes: VisualNote[] = [];
|
|
66
66
|
private showMarkers = true;
|
|
67
67
|
private isHidden = false;
|
|
68
|
+
private isToolbarCollapsed = false;
|
|
68
69
|
private cleanups: (() => void)[] = [];
|
|
69
70
|
|
|
70
71
|
constructor(transport: WebSocketTransport, screenshotDriver: ScreenshotDriver, options: InspectorOptions = {}) {
|
|
@@ -73,6 +74,11 @@ export class NoteInspector {
|
|
|
73
74
|
const hiddenByQuery = shouldHideUIFromUrl(options.hideQueryParam);
|
|
74
75
|
this.isHidden = options.hidden === true || hiddenByQuery;
|
|
75
76
|
this.showMarkers = options.showBadges !== false && !this.isHidden;
|
|
77
|
+
try {
|
|
78
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
79
|
+
this.isToolbarCollapsed = window.sessionStorage.getItem('bt_toolbar_collapsed') === 'true';
|
|
80
|
+
}
|
|
81
|
+
} catch {}
|
|
76
82
|
this.options = {
|
|
77
83
|
shortcut: 'Alt+Click',
|
|
78
84
|
maskSelectors: ['input[type="password"]', '[data-sensitive]'],
|
|
@@ -224,11 +230,28 @@ export class NoteInspector {
|
|
|
224
230
|
// Styles for highlight overlay, region drag box, markers, floating toolbar and modals
|
|
225
231
|
const style = document.createElement('style');
|
|
226
232
|
style.textContent = `
|
|
227
|
-
:
|
|
228
|
-
|
|
229
|
-
|
|
233
|
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
|
234
|
+
|
|
235
|
+
:host, :host *, :host *::before, :host *::after {
|
|
236
|
+
box-sizing: border-box;
|
|
230
237
|
-webkit-font-smoothing: antialiased;
|
|
231
238
|
-moz-osx-font-smoothing: grayscale;
|
|
239
|
+
text-rendering: optimizeLegibility;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
:host {
|
|
243
|
+
font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
244
|
+
color-scheme: dark;
|
|
245
|
+
letter-spacing: -0.012em;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
button, input, textarea, select {
|
|
249
|
+
font-family: inherit;
|
|
250
|
+
letter-spacing: inherit;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
code, pre, kbd, .bt-mono, .bt-badge, .bt-region-badge, .bt-target-pill, .bt-component-pill {
|
|
254
|
+
font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
232
255
|
}
|
|
233
256
|
|
|
234
257
|
/* 1. Element Highlight Overlay */
|
|
@@ -462,7 +485,7 @@ export class NoteInspector {
|
|
|
462
485
|
pointer-events: auto;
|
|
463
486
|
z-index: 2147483646;
|
|
464
487
|
user-select: none;
|
|
465
|
-
transition: all 0.
|
|
488
|
+
transition: all 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
|
466
489
|
}
|
|
467
490
|
|
|
468
491
|
.bt-toolbar-btn {
|
|
@@ -523,6 +546,86 @@ export class NoteInspector {
|
|
|
523
546
|
margin: 0 2px;
|
|
524
547
|
}
|
|
525
548
|
|
|
549
|
+
.bt-toolbar-collapse-btn {
|
|
550
|
+
background: transparent;
|
|
551
|
+
border: none;
|
|
552
|
+
color: #64748b;
|
|
553
|
+
font-size: 11px;
|
|
554
|
+
font-weight: 700;
|
|
555
|
+
padding: 6px 8px;
|
|
556
|
+
border-radius: 20px;
|
|
557
|
+
cursor: pointer;
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
justify-content: center;
|
|
561
|
+
transition: all 0.15s ease;
|
|
562
|
+
font-family: inherit;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
.bt-toolbar-collapse-btn:hover {
|
|
566
|
+
background: #1e293b;
|
|
567
|
+
color: #f8fafc;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* Collapsed Toolbar State */
|
|
571
|
+
.bt-toolbar.bt-toolbar-collapsed {
|
|
572
|
+
padding: 0;
|
|
573
|
+
background: transparent;
|
|
574
|
+
border: none;
|
|
575
|
+
box-shadow: none;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-btn,
|
|
579
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-divider,
|
|
580
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-collapse-btn {
|
|
581
|
+
display: none !important;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.bt-toolbar-trigger {
|
|
585
|
+
display: none;
|
|
586
|
+
background: #0f172a;
|
|
587
|
+
border: 1px solid #334155;
|
|
588
|
+
border-radius: 30px;
|
|
589
|
+
padding: 6px 14px;
|
|
590
|
+
color: #38bdf8;
|
|
591
|
+
font-size: 12px;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
cursor: pointer;
|
|
594
|
+
align-items: center;
|
|
595
|
+
gap: 8px;
|
|
596
|
+
box-shadow: 0 10px 25px -3px rgba(0,0,0,0.6), 0 4px 6px -4px rgba(0,0,0,0.4);
|
|
597
|
+
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
598
|
+
font-family: inherit;
|
|
599
|
+
user-select: none;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.bt-toolbar.bt-toolbar-collapsed .bt-toolbar-trigger {
|
|
603
|
+
display: inline-flex;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
.bt-toolbar-trigger:hover {
|
|
607
|
+
background: #1e293b;
|
|
608
|
+
color: #ffffff;
|
|
609
|
+
transform: translateY(-2px);
|
|
610
|
+
box-shadow: 0 12px 28px -3px rgba(0,0,0,0.7);
|
|
611
|
+
border-color: #38bdf8;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.bt-pulse-dot {
|
|
615
|
+
width: 7px;
|
|
616
|
+
height: 7px;
|
|
617
|
+
border-radius: 50%;
|
|
618
|
+
background: #38bdf8;
|
|
619
|
+
box-shadow: 0 0 8px #38bdf8;
|
|
620
|
+
display: inline-block;
|
|
621
|
+
animation: bt-dot-pulse 2s infinite;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
@keyframes bt-dot-pulse {
|
|
625
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
626
|
+
50% { opacity: 0.4; transform: scale(0.85); }
|
|
627
|
+
}
|
|
628
|
+
|
|
526
629
|
/* 5. Modals & Popover Card */
|
|
527
630
|
.bt-modal-backdrop {
|
|
528
631
|
position: fixed;
|
|
@@ -946,8 +1049,13 @@ export class NoteInspector {
|
|
|
946
1049
|
if (!this.shadowRoot || this.toolbarElement) return;
|
|
947
1050
|
|
|
948
1051
|
this.toolbarElement = document.createElement('div');
|
|
949
|
-
this.toolbarElement.className = 'bt-toolbar'
|
|
1052
|
+
this.toolbarElement.className = `bt-toolbar ${this.isToolbarCollapsed ? 'bt-toolbar-collapsed' : ''}`;
|
|
950
1053
|
this.toolbarElement.innerHTML = `
|
|
1054
|
+
<button class="bt-toolbar-trigger" id="bt-toolbar-expand" title="Expand BrowserTrack toolbar (Alt+T)">
|
|
1055
|
+
<span class="bt-pulse-dot"></span>
|
|
1056
|
+
<span>⚡ BrowserTrack</span>
|
|
1057
|
+
<span class="bt-count-pill" id="bt-notes-count-collapsed">0</span>
|
|
1058
|
+
</button>
|
|
951
1059
|
<button class="bt-toolbar-btn" id="bt-mode-element" title="Inspect element (or hold Alt+Click)">
|
|
952
1060
|
<span>🎯</span> Element
|
|
953
1061
|
</button>
|
|
@@ -965,14 +1073,30 @@ export class NoteInspector {
|
|
|
965
1073
|
<button class="bt-toolbar-btn active" id="bt-toggle-notes" title="Toggle visible note markers on screen">
|
|
966
1074
|
<span>📌</span> Notes <span class="bt-count-pill" id="bt-notes-count">0</span>
|
|
967
1075
|
</button>
|
|
1076
|
+
<div class="bt-toolbar-divider"></div>
|
|
1077
|
+
<button class="bt-toolbar-collapse-btn" id="bt-toolbar-collapse" title="Collapse toolbar (Alt+T)">
|
|
1078
|
+
<span>❯</span>
|
|
1079
|
+
</button>
|
|
968
1080
|
`;
|
|
969
1081
|
|
|
1082
|
+
const btnExpand = this.toolbarElement.querySelector('#bt-toolbar-expand') as HTMLButtonElement;
|
|
1083
|
+
const btnCollapse = this.toolbarElement.querySelector('#bt-toolbar-collapse') as HTMLButtonElement;
|
|
970
1084
|
const btnElement = this.toolbarElement.querySelector('#bt-mode-element') as HTMLButtonElement;
|
|
971
1085
|
const btnRegion = this.toolbarElement.querySelector('#bt-mode-region') as HTMLButtonElement;
|
|
972
1086
|
const btnPage = this.toolbarElement.querySelector('#bt-mode-page') as HTMLButtonElement;
|
|
973
1087
|
const btnFlow = this.toolbarElement.querySelector('#bt-mode-flow') as HTMLButtonElement;
|
|
974
1088
|
const btnToggleNotes = this.toolbarElement.querySelector('#bt-toggle-notes') as HTMLButtonElement;
|
|
975
1089
|
|
|
1090
|
+
btnExpand.onclick = (e: MouseEvent) => {
|
|
1091
|
+
e.stopPropagation();
|
|
1092
|
+
this.toggleToolbarCollapse(false);
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
btnCollapse.onclick = (e: MouseEvent) => {
|
|
1096
|
+
e.stopPropagation();
|
|
1097
|
+
this.toggleToolbarCollapse(true);
|
|
1098
|
+
};
|
|
1099
|
+
|
|
976
1100
|
btnElement.onclick = (e: MouseEvent) => {
|
|
977
1101
|
e.stopPropagation();
|
|
978
1102
|
this.setMode(this.activeMode === 'element' ? 'idle' : 'element');
|
|
@@ -1007,6 +1131,17 @@ export class NoteInspector {
|
|
|
1007
1131
|
this.shadowRoot.appendChild(this.toolbarElement);
|
|
1008
1132
|
}
|
|
1009
1133
|
|
|
1134
|
+
public toggleToolbarCollapse(force?: boolean): void {
|
|
1135
|
+
if (!this.toolbarElement) return;
|
|
1136
|
+
this.isToolbarCollapsed = typeof force === 'boolean' ? force : !this.isToolbarCollapsed;
|
|
1137
|
+
this.toolbarElement.classList.toggle('bt-toolbar-collapsed', this.isToolbarCollapsed);
|
|
1138
|
+
try {
|
|
1139
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
1140
|
+
window.sessionStorage.setItem('bt_toolbar_collapsed', String(this.isToolbarCollapsed));
|
|
1141
|
+
}
|
|
1142
|
+
} catch {}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1010
1145
|
private updateToolbarState(): void {
|
|
1011
1146
|
if (!this.toolbarElement) return;
|
|
1012
1147
|
|
|
@@ -1034,11 +1169,15 @@ export class NoteInspector {
|
|
|
1034
1169
|
|
|
1035
1170
|
private updateToolbarCount(): void {
|
|
1036
1171
|
if (!this.toolbarElement) return;
|
|
1172
|
+
const openCount = this.savedNotes.filter((n) => n.status === 'OPEN').length;
|
|
1037
1173
|
const countEl = this.toolbarElement.querySelector('#bt-notes-count');
|
|
1038
1174
|
if (countEl) {
|
|
1039
|
-
const openCount = this.savedNotes.filter((n) => n.status === 'OPEN').length;
|
|
1040
1175
|
countEl.textContent = String(openCount);
|
|
1041
1176
|
}
|
|
1177
|
+
const countCollapsedEl = this.toolbarElement.querySelector('#bt-notes-count-collapsed');
|
|
1178
|
+
if (countCollapsedEl) {
|
|
1179
|
+
countCollapsedEl.textContent = String(openCount);
|
|
1180
|
+
}
|
|
1042
1181
|
}
|
|
1043
1182
|
|
|
1044
1183
|
public renderMarkers(): void {
|
|
@@ -1050,9 +1189,16 @@ export class NoteInspector {
|
|
|
1050
1189
|
|
|
1051
1190
|
const currentPath = window.location.pathname;
|
|
1052
1191
|
// Filter open notes on this route or global
|
|
1053
|
-
const activeNotes = this.savedNotes.filter(
|
|
1054
|
-
(n
|
|
1055
|
-
|
|
1192
|
+
const activeNotes = this.savedNotes.filter((n) => {
|
|
1193
|
+
if (n.status !== 'OPEN') return false;
|
|
1194
|
+
if (!n.route) return true;
|
|
1195
|
+
if (n.route === currentPath) return true;
|
|
1196
|
+
// Support hash or query routes (e.g. /#modal or /?tab=settings)
|
|
1197
|
+
if (n.route.includes('#') || n.route.includes('?')) {
|
|
1198
|
+
return window.location.href.includes(n.route);
|
|
1199
|
+
}
|
|
1200
|
+
return false;
|
|
1201
|
+
});
|
|
1056
1202
|
|
|
1057
1203
|
const pageNotes: VisualNote[] = [];
|
|
1058
1204
|
|
|
@@ -1101,6 +1247,14 @@ export class NoteInspector {
|
|
|
1101
1247
|
|
|
1102
1248
|
const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
|
|
1103
1249
|
if (rect) {
|
|
1250
|
+
// If element is completely scrolled out of view, do not display or pin it to corner
|
|
1251
|
+
const isOffscreen =
|
|
1252
|
+
targetEl &&
|
|
1253
|
+
(rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth);
|
|
1254
|
+
if (isOffscreen) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1104
1258
|
const pin = document.createElement('div');
|
|
1105
1259
|
pin.className = markerClass;
|
|
1106
1260
|
pin.setAttribute('data-note-id', note.id);
|
|
@@ -1343,81 +1497,111 @@ export class NoteInspector {
|
|
|
1343
1497
|
private setupListeners(): void {
|
|
1344
1498
|
// 1. Mouse move: hover highlight in element mode or with Alt key
|
|
1345
1499
|
const onMouseMove = (e: MouseEvent) => {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
// Don't highlight elements if mouse is over our own inspector UI
|
|
1349
|
-
const path = e.composedPath ? e.composedPath() : [];
|
|
1350
|
-
if (this.container && path.includes(this.container)) {
|
|
1351
|
-
this.hideHighlight();
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1500
|
+
try {
|
|
1501
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
|
|
1354
1502
|
|
|
1355
|
-
|
|
1356
|
-
const
|
|
1357
|
-
if (
|
|
1358
|
-
this.
|
|
1359
|
-
this.updateHighlight(target);
|
|
1503
|
+
// Don't highlight elements if mouse is over our own inspector UI
|
|
1504
|
+
const path = e.composedPath ? e.composedPath() : [];
|
|
1505
|
+
if (this.container && path.includes(this.container)) {
|
|
1506
|
+
this.hideHighlight();
|
|
1360
1507
|
return;
|
|
1361
1508
|
}
|
|
1362
|
-
}
|
|
1363
1509
|
|
|
1364
|
-
|
|
1365
|
-
|
|
1510
|
+
if (e.altKey || this.activeMode === 'element') {
|
|
1511
|
+
const rawTarget = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
|
|
1512
|
+
const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
|
|
1513
|
+
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
1514
|
+
this.hoveredElement = target;
|
|
1515
|
+
this.updateHighlight(target);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
if (!e.altKey && this.activeMode !== 'element') {
|
|
1521
|
+
this.hideHighlight();
|
|
1522
|
+
}
|
|
1523
|
+
} catch {
|
|
1524
|
+
// Defensive: never let inspector hover crash host app
|
|
1366
1525
|
}
|
|
1367
1526
|
};
|
|
1368
1527
|
|
|
1369
1528
|
// 2. Click: Alt+Click or Element mode click selects element and opens editor
|
|
1370
1529
|
const onClick = (e: MouseEvent) => {
|
|
1371
|
-
|
|
1530
|
+
try {
|
|
1531
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
|
|
1372
1532
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1533
|
+
// If clicking inside inspector toolbar or container, do NOT intercept
|
|
1534
|
+
const path = e.composedPath ? e.composedPath() : [];
|
|
1535
|
+
if (this.container && path.includes(this.container)) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1378
1538
|
|
|
1379
|
-
|
|
1539
|
+
if (this.activeMode === 'region') return;
|
|
1380
1540
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1541
|
+
if (e.altKey || this.activeMode === 'element') {
|
|
1542
|
+
e.preventDefault();
|
|
1543
|
+
e.stopPropagation();
|
|
1384
1544
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
this.
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
this.
|
|
1545
|
+
const rawTarget = (this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY)) as HTMLElement | null;
|
|
1546
|
+
const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
|
|
1547
|
+
if (target && target !== this.container && !this.container?.contains(target)) {
|
|
1548
|
+
this.selectedElement = target;
|
|
1549
|
+
this.openNoteEditor(target, 'element');
|
|
1550
|
+
if (this.activeMode === 'element' && !this.activeScenario) {
|
|
1551
|
+
this.setMode('idle');
|
|
1552
|
+
}
|
|
1391
1553
|
}
|
|
1392
1554
|
}
|
|
1555
|
+
} catch {
|
|
1556
|
+
// Defensive
|
|
1393
1557
|
}
|
|
1394
1558
|
};
|
|
1395
1559
|
|
|
1396
|
-
// 3. Key handling: Escape cancels active modes, Alt release hides highlight
|
|
1560
|
+
// 3. Key handling: Escape cancels active modes, Alt release hides highlight, Alt+T toggles toolbar
|
|
1397
1561
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
1398
|
-
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1401
|
-
this.
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1562
|
+
try {
|
|
1563
|
+
if (e.altKey && (e.key === 't' || e.key === 'T')) {
|
|
1564
|
+
e.preventDefault();
|
|
1565
|
+
this.toggleToolbarCollapse();
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
if (e.key === 'Escape') {
|
|
1570
|
+
if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
|
|
1571
|
+
this.shadowRoot.removeChild(this.cardOverlay);
|
|
1572
|
+
this.cardOverlay = null;
|
|
1573
|
+
} else if (this.activeMode === 'region' || this.activeMode === 'element') {
|
|
1574
|
+
if (!this.activeScenario) {
|
|
1575
|
+
this.setMode('idle');
|
|
1576
|
+
}
|
|
1405
1577
|
}
|
|
1406
1578
|
}
|
|
1579
|
+
} catch {
|
|
1580
|
+
// Defensive
|
|
1407
1581
|
}
|
|
1408
1582
|
};
|
|
1409
1583
|
|
|
1410
1584
|
const onKeyUp = (e: KeyboardEvent) => {
|
|
1411
|
-
|
|
1412
|
-
this.
|
|
1585
|
+
try {
|
|
1586
|
+
if (e.key === 'Alt' && !this.modalOverlay && !this.cardOverlay && this.activeMode !== 'element') {
|
|
1587
|
+
this.hideHighlight();
|
|
1588
|
+
}
|
|
1589
|
+
} catch {
|
|
1590
|
+
// Defensive
|
|
1413
1591
|
}
|
|
1414
1592
|
};
|
|
1415
1593
|
|
|
1416
1594
|
// 4. Scroll & resize: reposition markers accurately
|
|
1417
1595
|
const onScrollOrResize = () => {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1596
|
+
try {
|
|
1597
|
+
requestAnimationFrame(() => {
|
|
1598
|
+
try {
|
|
1599
|
+
this.updateMarkerPositions();
|
|
1600
|
+
} catch {}
|
|
1601
|
+
});
|
|
1602
|
+
} catch {
|
|
1603
|
+
// Defensive
|
|
1604
|
+
}
|
|
1421
1605
|
};
|
|
1422
1606
|
|
|
1423
1607
|
window.addEventListener('mousemove', onMouseMove, { capture: true, passive: true });
|
|
@@ -1513,6 +1697,11 @@ export class NoteInspector {
|
|
|
1513
1697
|
this.regionBox.appendChild(badge);
|
|
1514
1698
|
}
|
|
1515
1699
|
badge.textContent = `Region: ${Math.round(width)} × ${Math.round(height)} px`;
|
|
1700
|
+
if (top < 32) {
|
|
1701
|
+
badge.style.top = '4px';
|
|
1702
|
+
} else {
|
|
1703
|
+
badge.style.top = '-26px';
|
|
1704
|
+
}
|
|
1516
1705
|
};
|
|
1517
1706
|
|
|
1518
1707
|
this.regionOverlay.onmouseup = (e: MouseEvent) => {
|
|
@@ -1581,6 +1770,22 @@ export class NoteInspector {
|
|
|
1581
1770
|
const selector = getSemanticSelector(el);
|
|
1582
1771
|
const label = this.activeScenario ? `🎬 Step ${this.activeScenario.stepNumber} · ${selector}` : selector;
|
|
1583
1772
|
badge.textContent = `${label} (${Math.round(rect.width)} × ${Math.round(rect.height)} px)`;
|
|
1773
|
+
|
|
1774
|
+
// Viewport-aware badge clamping: flip below element if near top of window
|
|
1775
|
+
if (rect.top < 32) {
|
|
1776
|
+
badge.style.top = `${rect.height + 4}px`;
|
|
1777
|
+
} else {
|
|
1778
|
+
badge.style.top = '-26px';
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// Align to right edge if overflowing the right viewport boundary
|
|
1782
|
+
if (rect.left + 260 > window.innerWidth) {
|
|
1783
|
+
badge.style.left = 'auto';
|
|
1784
|
+
badge.style.right = '0';
|
|
1785
|
+
} else {
|
|
1786
|
+
badge.style.left = '0';
|
|
1787
|
+
badge.style.right = 'auto';
|
|
1788
|
+
}
|
|
1584
1789
|
}
|
|
1585
1790
|
|
|
1586
1791
|
private hideHighlight(): void {
|
|
@@ -1768,10 +1973,13 @@ export class NoteInspector {
|
|
|
1768
1973
|
if (btnSave) btnSave.disabled = true;
|
|
1769
1974
|
if (btnFinishFlow) btnFinishFlow.disabled = true;
|
|
1770
1975
|
|
|
1976
|
+
// Close modal immediately for instant, snappy user experience
|
|
1977
|
+
closeModal();
|
|
1978
|
+
|
|
1771
1979
|
try {
|
|
1772
1980
|
await options.onSave(message, action);
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1981
|
+
} catch {
|
|
1982
|
+
this.showToast('Failed to save note', '⚠️');
|
|
1775
1983
|
}
|
|
1776
1984
|
};
|
|
1777
1985
|
|
|
@@ -1806,11 +2014,15 @@ export class NoteInspector {
|
|
|
1806
2014
|
const rect = targetEl.getBoundingClientRect();
|
|
1807
2015
|
const selector = noteType === 'page' ? 'body' : getSemanticSelector(targetEl);
|
|
1808
2016
|
|
|
1809
|
-
// 1. Capture screenshot via screenshotDriver
|
|
2017
|
+
// 1. Capture screenshot via screenshotDriver with fast timeout protection
|
|
1810
2018
|
let screenshotDataUrl: string | undefined;
|
|
1811
2019
|
try {
|
|
1812
|
-
const
|
|
1813
|
-
|
|
2020
|
+
const snapPromise = this.screenshotDriver.captureElement(targetEl);
|
|
2021
|
+
const timeoutPromise = new Promise<{ ok: boolean; dataUrl?: string }>((resolve) =>
|
|
2022
|
+
setTimeout(() => resolve({ ok: false }), 800)
|
|
2023
|
+
);
|
|
2024
|
+
const snap = await Promise.race([snapPromise, timeoutPromise]);
|
|
2025
|
+
if (snap && snap.ok && snap.dataUrl) {
|
|
1814
2026
|
screenshotDataUrl = snap.dataUrl;
|
|
1815
2027
|
}
|
|
1816
2028
|
} catch {
|
|
@@ -1881,12 +2093,20 @@ export class NoteInspector {
|
|
|
1881
2093
|
message: string,
|
|
1882
2094
|
scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
|
|
1883
2095
|
): Promise<void> {
|
|
1884
|
-
// 1.
|
|
2096
|
+
// 1. Fast regional capture: capture element under region center rather than heavy whole-document serialization
|
|
1885
2097
|
let screenshotDataUrl: string | undefined;
|
|
1886
2098
|
try {
|
|
1887
|
-
const
|
|
1888
|
-
|
|
1889
|
-
|
|
2099
|
+
const centerX = region.x + region.width / 2;
|
|
2100
|
+
const centerY = region.y + region.height / 2;
|
|
2101
|
+
const targetElement = (document.elementFromPoint(centerX, centerY) || document.body) as HTMLElement;
|
|
2102
|
+
|
|
2103
|
+
const snapPromise = this.screenshotDriver.captureElement(targetElement);
|
|
2104
|
+
const timeoutPromise = new Promise<{ ok: boolean; dataUrl?: string }>((resolve) =>
|
|
2105
|
+
setTimeout(() => resolve({ ok: false }), 800)
|
|
2106
|
+
);
|
|
2107
|
+
const snap = await Promise.race([snapPromise, timeoutPromise]);
|
|
2108
|
+
if (snap && snap.ok && snap.dataUrl) {
|
|
2109
|
+
screenshotDataUrl = snap.dataUrl;
|
|
1890
2110
|
}
|
|
1891
2111
|
} catch {
|
|
1892
2112
|
// Defensive
|
|
@@ -2005,7 +2225,11 @@ export class NoteInspector {
|
|
|
2005
2225
|
region.width,
|
|
2006
2226
|
region.height
|
|
2007
2227
|
);
|
|
2008
|
-
|
|
2228
|
+
try {
|
|
2229
|
+
resolve(canvas.toDataURL('image/png'));
|
|
2230
|
+
} catch {
|
|
2231
|
+
resolve(dataUrl);
|
|
2232
|
+
}
|
|
2009
2233
|
};
|
|
2010
2234
|
img.onerror = () => resolve(dataUrl);
|
|
2011
2235
|
img.src = dataUrl;
|
|
@@ -76,7 +76,9 @@ function resolveReactComponent(el: HTMLElement): ComponentSourceInfo | undefined
|
|
|
76
76
|
|
|
77
77
|
// Walk up fiber return tree to discover composite components and hierarchy
|
|
78
78
|
let curr = hostFiber;
|
|
79
|
-
|
|
79
|
+
let depth = 0;
|
|
80
|
+
while (curr && depth < 50) {
|
|
81
|
+
depth++;
|
|
80
82
|
const type = curr.type;
|
|
81
83
|
const name = getReactComponentName(type);
|
|
82
84
|
|
|
@@ -146,7 +148,9 @@ function resolveVueComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
|
146
148
|
const hierarchy: string[] = [];
|
|
147
149
|
|
|
148
150
|
let curr = vueParent;
|
|
149
|
-
|
|
151
|
+
let depth = 0;
|
|
152
|
+
while (curr && depth < 50) {
|
|
153
|
+
depth++;
|
|
150
154
|
const cType = curr.type || {};
|
|
151
155
|
const cName = cType.name || cType.__name || cType.displayName;
|
|
152
156
|
if (cName && !hierarchy.includes(cName)) {
|
|
@@ -192,7 +196,9 @@ function resolveVueComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
|
192
196
|
function resolveSvelteComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
193
197
|
try {
|
|
194
198
|
let curr: HTMLElement | null = el;
|
|
195
|
-
|
|
199
|
+
let depth = 0;
|
|
200
|
+
while (curr && depth < 50) {
|
|
201
|
+
depth++;
|
|
196
202
|
const meta = (curr as any).__svelte_meta;
|
|
197
203
|
if (meta && meta.loc) {
|
|
198
204
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HelloMessage, ClientCommand, CommandResponse } from '../../../core/src/index.js';
|
|
2
|
+
import { safeJsonStringify } from '../../../core/src/index.js';
|
|
2
3
|
import type { ClientCommandHandler } from '../commands/handler.js';
|
|
3
4
|
|
|
4
5
|
export interface TransportOptions {
|
|
@@ -111,15 +112,19 @@ export class WebSocketTransport {
|
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
public send(payload: any): void {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
try {
|
|
116
|
+
const raw = safeJsonStringify(payload);
|
|
117
|
+
if (this.isConnected()) {
|
|
118
|
+
try {
|
|
119
|
+
this.ws!.send(raw);
|
|
120
|
+
} catch {
|
|
121
|
+
this.enqueue(raw);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
119
124
|
this.enqueue(raw);
|
|
120
125
|
}
|
|
121
|
-
}
|
|
122
|
-
|
|
126
|
+
} catch {
|
|
127
|
+
// Defensive: never let payload serialization crash caller
|
|
123
128
|
}
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -148,18 +153,18 @@ export class WebSocketTransport {
|
|
|
148
153
|
private sendHello(): void {
|
|
149
154
|
if (typeof window === 'undefined') return;
|
|
150
155
|
|
|
151
|
-
const hello: HelloMessage = {
|
|
152
|
-
type: 'hello',
|
|
153
|
-
origin: window.location.origin,
|
|
154
|
-
url: window.location.href,
|
|
155
|
-
title: document.title,
|
|
156
|
-
userAgent: navigator.userAgent,
|
|
157
|
-
timestamp: Date.now(),
|
|
158
|
-
projectId: this.projectId,
|
|
159
|
-
};
|
|
160
|
-
|
|
161
156
|
try {
|
|
162
|
-
|
|
157
|
+
const hello: HelloMessage = {
|
|
158
|
+
type: 'hello',
|
|
159
|
+
origin: window.location.origin,
|
|
160
|
+
url: window.location.href,
|
|
161
|
+
title: document.title,
|
|
162
|
+
userAgent: navigator.userAgent,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
projectId: this.projectId,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
this.ws!.send(safeJsonStringify(hello));
|
|
163
168
|
} catch {
|
|
164
169
|
// Defensive
|
|
165
170
|
}
|