browsertrack 0.2.0 → 0.2.1
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 +2 -0
- package/dist/{chunk-UP5JKCFY.js → chunk-464D4U2U.js} +3 -2
- package/dist/chunk-464D4U2U.js.map +1 -0
- package/dist/{chunk-WB7ZKWK7.js → chunk-INXDWPJW.js} +354 -9
- package/dist/chunk-INXDWPJW.js.map +1 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +357 -10
- package/dist/client/index.d.ts +36 -4
- package/dist/client/index.js +7 -3
- package/dist/client.iife.js +42 -19
- package/dist/{notes-BMnonq46.d.ts → commands-fjuqKzkm.d.ts} +125 -114
- package/dist/core/index.d.ts +2 -2
- package/dist/daemon/index.d.ts +3 -3
- package/dist/{engine-B43IohQY.d.ts → engine-CmchnMDq.d.ts} +2 -2
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/mcp/index.d.ts +4 -4
- package/dist/mcp/index.js +1 -1
- package/dist/{projects-D5J-egVN.d.ts → projects-DB7S312i.d.ts} +1 -1
- package/dist/{server-BztYp1Zc.d.ts → server-DiVmTrIR.d.ts} +1 -1
- package/docs/component-resolver.md +108 -0
- package/docs/index.md +1 -0
- package/docs/mcp-reference.md +2 -2
- package/docs/visual-notes.md +36 -0
- package/package.json +1 -1
- package/packages/client/src/client.ts +18 -1
- package/packages/client/src/config.ts +84 -1
- package/packages/client/src/index.ts +4 -1
- package/packages/client/src/interceptors/interaction.ts +3 -0
- package/packages/client/src/notes/inspector.ts +107 -5
- package/packages/client/src/source/resolver.ts +272 -0
- package/packages/core/src/types/events.ts +3 -0
- package/packages/core/src/types/notes.ts +11 -0
- package/packages/mcp/src/handlers.ts +1 -0
- package/test/client/component-resolver.test.ts +141 -0
- package/test/client/interceptors.test.ts +56 -0
- package/dist/chunk-UP5JKCFY.js.map +0 -1
- package/dist/chunk-WB7ZKWK7.js.map +0 -1
|
@@ -12,12 +12,25 @@ export interface BrowserDiagClientOptions {
|
|
|
12
12
|
enabled?: boolean;
|
|
13
13
|
shortcut?: string;
|
|
14
14
|
showBadges?: boolean;
|
|
15
|
+
showToolbar?: boolean;
|
|
15
16
|
maskSelectors?: string[];
|
|
16
17
|
};
|
|
18
|
+
/**
|
|
19
|
+
* Hide visible BrowserTrack UI (toolbar dock, pins, hover overlay) by default.
|
|
20
|
+
*/
|
|
21
|
+
hidden?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Custom query parameter(s) used to hide the UI (e.g. 'e2e', 'no_ui', 'clean').
|
|
24
|
+
* Built-in query params like ?bt=0, ?bt=false, ?bt=hidden, ?browsertrack=false, ?no_bt are supported automatically.
|
|
25
|
+
*/
|
|
26
|
+
hideQueryParam?: string | string[];
|
|
17
27
|
debug?: boolean;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
|
-
export const DEFAULT_OPTIONS: Required<Omit<BrowserDiagClientOptions, 'notes'
|
|
30
|
+
export const DEFAULT_OPTIONS: Required<Omit<BrowserDiagClientOptions, 'notes' | 'hideQueryParam'>> & {
|
|
31
|
+
hideQueryParam?: string | string[];
|
|
32
|
+
notes: Required<NonNullable<BrowserDiagClientOptions['notes']>>;
|
|
33
|
+
} = {
|
|
21
34
|
daemonUrl: 'ws://127.0.0.1:7331',
|
|
22
35
|
projectId: '',
|
|
23
36
|
maxBreadcrumbs: 50,
|
|
@@ -31,7 +44,77 @@ export const DEFAULT_OPTIONS: Required<Omit<BrowserDiagClientOptions, 'notes'>>
|
|
|
31
44
|
enabled: true,
|
|
32
45
|
shortcut: 'Alt+Click',
|
|
33
46
|
showBadges: true,
|
|
47
|
+
showToolbar: true,
|
|
34
48
|
maskSelectors: ['input[type="password"]', '[data-sensitive]'],
|
|
35
49
|
},
|
|
50
|
+
hidden: false,
|
|
51
|
+
hideQueryParam: undefined,
|
|
36
52
|
debug: false,
|
|
37
53
|
};
|
|
54
|
+
|
|
55
|
+
const HIDE_VALUES = new Set(['0', 'false', 'hidden', 'hide', 'off', 'none', 'disabled', 'ui_off', 'silent']);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Checks if the tool's visible UI should be hidden based on URL search query parameters.
|
|
59
|
+
*/
|
|
60
|
+
export function shouldHideUIFromUrl(customParam?: string | string[], searchString?: string): boolean {
|
|
61
|
+
let search = searchString;
|
|
62
|
+
if (search === undefined) {
|
|
63
|
+
if (typeof window === 'undefined' || !window.location) return false;
|
|
64
|
+
search = window.location.search;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!search) return false;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const params = new URLSearchParams(search);
|
|
71
|
+
|
|
72
|
+
// 1. Check custom parameters if configured
|
|
73
|
+
if (customParam) {
|
|
74
|
+
const customKeys = Array.isArray(customParam) ? customParam : [customParam];
|
|
75
|
+
for (const key of customKeys) {
|
|
76
|
+
if (params.has(key)) {
|
|
77
|
+
const val = (params.get(key) || '').toLowerCase().trim();
|
|
78
|
+
if (val === '' || val === '1' || val === 'true' || HIDE_VALUES.has(val)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. Boolean flags without values or with 1/true: ?no_bt, ?no_browsertrack, ?hide_bt, ?hide_browsertrack
|
|
86
|
+
for (const flag of ['no_bt', 'no_browsertrack', 'hide_bt', 'hide_browsertrack']) {
|
|
87
|
+
if (params.has(flag)) {
|
|
88
|
+
const val = (params.get(flag) || '').toLowerCase().trim();
|
|
89
|
+
if (val === '' || val === '1' || val === 'true' || val === 'yes') {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 3. Value-based parameters: ?bt=false, ?bt=0, ?bt=hidden, ?bt=off
|
|
96
|
+
if (params.has('bt')) {
|
|
97
|
+
const val = (params.get('bt') || '').toLowerCase().trim();
|
|
98
|
+
if (HIDE_VALUES.has(val)) return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (params.has('browsertrack')) {
|
|
102
|
+
const val = (params.get('browsertrack') || '').toLowerCase().trim();
|
|
103
|
+
if (HIDE_VALUES.has(val)) return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (params.has('bt_ui')) {
|
|
107
|
+
const val = (params.get('bt_ui') || '').toLowerCase().trim();
|
|
108
|
+
if (HIDE_VALUES.has(val) || val === '0' || val === 'false') return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (params.has('bt_hide')) {
|
|
112
|
+
const val = (params.get('bt_hide') || '').toLowerCase().trim();
|
|
113
|
+
if (val === '' || val === '1' || val === 'true') return true;
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Defensive in non-standard environments
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
@@ -7,6 +7,7 @@ export * from './breadcrumbs.js';
|
|
|
7
7
|
export * from './screenshot/driver.js';
|
|
8
8
|
export * from './screenshot/browser-script-driver.js';
|
|
9
9
|
export * from './notes/inspector.js';
|
|
10
|
+
export * from './source/resolver.js';
|
|
10
11
|
|
|
11
12
|
let defaultClient: BrowserTrackClient | null = null;
|
|
12
13
|
|
|
@@ -40,9 +41,11 @@ if (typeof window !== 'undefined') {
|
|
|
40
41
|
const autoInit = currentScript?.getAttribute('data-auto-init') !== 'false';
|
|
41
42
|
const daemonUrl = currentScript?.getAttribute('data-daemon-url') || undefined;
|
|
42
43
|
const projectId = currentScript?.getAttribute('data-project-id') || undefined;
|
|
44
|
+
const hidden = currentScript?.getAttribute('data-hidden') === 'true';
|
|
45
|
+
const hideQueryParam = currentScript?.getAttribute('data-hide-query-param') || undefined;
|
|
43
46
|
|
|
44
47
|
if (autoInit) {
|
|
45
|
-
init({ daemonUrl, projectId });
|
|
48
|
+
init({ daemonUrl, projectId, hidden, hideQueryParam });
|
|
46
49
|
}
|
|
47
50
|
} catch {
|
|
48
51
|
// If auto-start fails, don't crash
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Breadcrumb, ElementSummary } from '../../../core/src/index.js';
|
|
2
2
|
import { getSemanticSelector, truncate } from '../../../core/src/index.js';
|
|
3
|
+
import { resolveComponentSource } from '../source/resolver.js';
|
|
3
4
|
|
|
4
5
|
export type InteractionCallback = (breadcrumb: Breadcrumb, elementSummary?: ElementSummary) => void;
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ export function extractElementSummary(el: HTMLElement | Element): ElementSummary
|
|
|
12
13
|
const tag = el.tagName.toLowerCase();
|
|
13
14
|
const id = el.id || undefined;
|
|
14
15
|
const classes = Array.from(el.classList || []);
|
|
16
|
+
const componentSource = resolveComponentSource(el as HTMLElement);
|
|
15
17
|
|
|
16
18
|
let boundingRect: ElementSummary['boundingRect'] = undefined;
|
|
17
19
|
let visible = true;
|
|
@@ -53,6 +55,7 @@ export function extractElementSummary(el: HTMLElement | Element): ElementSummary
|
|
|
53
55
|
visible,
|
|
54
56
|
innerText,
|
|
55
57
|
outerHTML,
|
|
58
|
+
componentSource,
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
|
|
@@ -2,6 +2,8 @@ import type { ElementContext, NoteTarget, RegionContext, VisualNote } from '../.
|
|
|
2
2
|
import { getSemanticSelector, 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
|
+
import { shouldHideUIFromUrl } from '../config.js';
|
|
6
|
+
import { resolveComponentSource } from '../source/resolver.js';
|
|
5
7
|
|
|
6
8
|
export type NoteInspectMode = 'element' | 'region' | 'page' | 'idle';
|
|
7
9
|
|
|
@@ -9,6 +11,9 @@ export interface InspectorOptions {
|
|
|
9
11
|
shortcut?: string; // e.g. "Alt+Click"
|
|
10
12
|
maskSelectors?: string[];
|
|
11
13
|
showToolbar?: boolean;
|
|
14
|
+
showBadges?: boolean;
|
|
15
|
+
hidden?: boolean;
|
|
16
|
+
hideQueryParam?: string | string[];
|
|
12
17
|
onNoteCreated?: (note: Partial<VisualNote>) => void;
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -28,6 +33,7 @@ export interface ActiveScenarioState {
|
|
|
28
33
|
* 5. Real-time Saved Note & Step Markers (pins with step sequencing numbers)
|
|
29
34
|
* 6. Interactive Note & Step Detail Card (with previous/next step walk-through navigation)
|
|
30
35
|
* 7. Floating quick dock / toolbar in bottom-right corner with Notes count and Flow controls
|
|
36
|
+
* 8. Automatic invisibility via query parameter (?bt=0, ?bt=false, ?bt=hidden, ?no_bt, custom query params)
|
|
31
37
|
*/
|
|
32
38
|
export class NoteInspector {
|
|
33
39
|
private transport: WebSocketTransport;
|
|
@@ -58,15 +64,20 @@ export class NoteInspector {
|
|
|
58
64
|
|
|
59
65
|
private savedNotes: VisualNote[] = [];
|
|
60
66
|
private showMarkers = true;
|
|
67
|
+
private isHidden = false;
|
|
61
68
|
private cleanups: (() => void)[] = [];
|
|
62
69
|
|
|
63
70
|
constructor(transport: WebSocketTransport, screenshotDriver: ScreenshotDriver, options: InspectorOptions = {}) {
|
|
64
71
|
this.transport = transport;
|
|
65
72
|
this.screenshotDriver = screenshotDriver;
|
|
73
|
+
const hiddenByQuery = shouldHideUIFromUrl(options.hideQueryParam);
|
|
74
|
+
this.isHidden = options.hidden === true || hiddenByQuery;
|
|
75
|
+
this.showMarkers = options.showBadges !== false && !this.isHidden;
|
|
66
76
|
this.options = {
|
|
67
77
|
shortcut: 'Alt+Click',
|
|
68
78
|
maskSelectors: ['input[type="password"]', '[data-sensitive]'],
|
|
69
79
|
showToolbar: true,
|
|
80
|
+
showBadges: true,
|
|
70
81
|
...options,
|
|
71
82
|
};
|
|
72
83
|
}
|
|
@@ -158,6 +169,44 @@ export class NoteInspector {
|
|
|
158
169
|
}
|
|
159
170
|
}
|
|
160
171
|
|
|
172
|
+
public isVisible(): boolean {
|
|
173
|
+
return !this.isHidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public setVisible(visible: boolean): void {
|
|
177
|
+
this.isHidden = !visible;
|
|
178
|
+
this.showMarkers = this.options.showBadges !== false && !this.isHidden;
|
|
179
|
+
|
|
180
|
+
if (this.container) {
|
|
181
|
+
this.container.style.display = this.isHidden ? 'none' : 'block';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.toolbarElement) {
|
|
185
|
+
this.toolbarElement.style.display = this.isHidden ? 'none' : 'flex';
|
|
186
|
+
} else if (!this.isHidden && this.options.showToolbar !== false) {
|
|
187
|
+
this.createToolbar();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (this.isHidden) {
|
|
191
|
+
this.hideHighlight();
|
|
192
|
+
this.hideRegionOverlay();
|
|
193
|
+
if (this.modalOverlay && this.shadowRoot) {
|
|
194
|
+
this.shadowRoot.removeChild(this.modalOverlay);
|
|
195
|
+
this.modalOverlay = null;
|
|
196
|
+
}
|
|
197
|
+
if (this.cardOverlay && this.shadowRoot) {
|
|
198
|
+
this.shadowRoot.removeChild(this.cardOverlay);
|
|
199
|
+
this.cardOverlay = null;
|
|
200
|
+
}
|
|
201
|
+
if (this.markersContainer) {
|
|
202
|
+
this.markersContainer.innerHTML = '';
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
this.renderMarkers();
|
|
206
|
+
this.updateToolbarCount();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
161
210
|
private ensureContainer(): ShadowRoot | null {
|
|
162
211
|
if (typeof document === 'undefined') return null;
|
|
163
212
|
|
|
@@ -166,6 +215,9 @@ export class NoteInspector {
|
|
|
166
215
|
this.container.id = 'browsertrack-inspector-host';
|
|
167
216
|
this.container.style.cssText =
|
|
168
217
|
'all: initial; position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;';
|
|
218
|
+
if (this.isHidden) {
|
|
219
|
+
this.container.style.display = 'none';
|
|
220
|
+
}
|
|
169
221
|
|
|
170
222
|
this.shadowRoot = this.container.attachShadow({ mode: 'open' });
|
|
171
223
|
|
|
@@ -611,6 +663,22 @@ export class NoteInspector {
|
|
|
611
663
|
color: #cbd5e1;
|
|
612
664
|
}
|
|
613
665
|
|
|
666
|
+
.bt-component-pill {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
gap: 6px;
|
|
670
|
+
background: rgba(15, 23, 42, 0.95);
|
|
671
|
+
border: 1px solid rgba(99, 102, 241, 0.35);
|
|
672
|
+
border-radius: 6px;
|
|
673
|
+
padding: 6px 10px;
|
|
674
|
+
font-size: 11.5px;
|
|
675
|
+
color: #c7d2fe;
|
|
676
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
677
|
+
overflow: hidden;
|
|
678
|
+
text-overflow: ellipsis;
|
|
679
|
+
white-space: nowrap;
|
|
680
|
+
}
|
|
681
|
+
|
|
614
682
|
.bt-scenario-stepper {
|
|
615
683
|
display: flex;
|
|
616
684
|
align-items: center;
|
|
@@ -859,7 +927,7 @@ export class NoteInspector {
|
|
|
859
927
|
this.shadowRoot.appendChild(this.toastContainer);
|
|
860
928
|
|
|
861
929
|
// Floating Toolbar
|
|
862
|
-
if (this.options.showToolbar) {
|
|
930
|
+
if (this.options.showToolbar && !this.isHidden) {
|
|
863
931
|
this.createToolbar();
|
|
864
932
|
}
|
|
865
933
|
}
|
|
@@ -978,7 +1046,7 @@ export class NoteInspector {
|
|
|
978
1046
|
if (!root || !this.markersContainer) return;
|
|
979
1047
|
|
|
980
1048
|
this.markersContainer.innerHTML = '';
|
|
981
|
-
if (!this.showMarkers) return;
|
|
1049
|
+
if (!this.showMarkers || this.isHidden) return;
|
|
982
1050
|
|
|
983
1051
|
const currentPath = window.location.pathname;
|
|
984
1052
|
// Filter open notes on this route or global
|
|
@@ -1143,6 +1211,32 @@ export class NoteInspector {
|
|
|
1143
1211
|
|
|
1144
1212
|
${scenarioStepsHtml}
|
|
1145
1213
|
|
|
1214
|
+
${
|
|
1215
|
+
note.elementContext?.componentSource?.componentName
|
|
1216
|
+
? `<div class="bt-component-pill" title="${
|
|
1217
|
+
note.elementContext.componentSource.sourceFile
|
|
1218
|
+
? note.elementContext.componentSource.sourceFile +
|
|
1219
|
+
(note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : '')
|
|
1220
|
+
: note.elementContext.componentSource.componentName
|
|
1221
|
+
}">
|
|
1222
|
+
<span>🧬</span>
|
|
1223
|
+
<span style="font-weight: 600; color: #a5b4fc;"><${note.elementContext.componentSource.componentName}></span>
|
|
1224
|
+
${
|
|
1225
|
+
note.elementContext.componentSource.sourceFile
|
|
1226
|
+
? `<span style="color: #64748b; font-size: 11px; margin-left: 4px;">${note.elementContext.componentSource.sourceFile}${
|
|
1227
|
+
note.elementContext.componentSource.sourceLine ? ':' + note.elementContext.componentSource.sourceLine : ''
|
|
1228
|
+
}</span>`
|
|
1229
|
+
: ''
|
|
1230
|
+
}
|
|
1231
|
+
${
|
|
1232
|
+
note.elementContext.componentSource.framework
|
|
1233
|
+
? `<span class="bt-mode-badge" style="background: rgba(99, 102, 241, 0.15); color: #818cf8; margin-left: auto; font-size: 10px;">${note.elementContext.componentSource.framework.toUpperCase()}</span>`
|
|
1234
|
+
: ''
|
|
1235
|
+
}
|
|
1236
|
+
</div>`
|
|
1237
|
+
: ''
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1146
1240
|
<div class="bt-target-pill" title="${contextText}">
|
|
1147
1241
|
<span>🏷️</span>
|
|
1148
1242
|
<span class="bt-pill-content">${contextText}</span>
|
|
@@ -1249,7 +1343,7 @@ export class NoteInspector {
|
|
|
1249
1343
|
private setupListeners(): void {
|
|
1250
1344
|
// 1. Mouse move: hover highlight in element mode or with Alt key
|
|
1251
1345
|
const onMouseMove = (e: MouseEvent) => {
|
|
1252
|
-
if (this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
|
|
1346
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay || this.isDraggingRegion || this.activeMode === 'region') return;
|
|
1253
1347
|
|
|
1254
1348
|
// Don't highlight elements if mouse is over our own inspector UI
|
|
1255
1349
|
const path = e.composedPath ? e.composedPath() : [];
|
|
@@ -1274,7 +1368,7 @@ export class NoteInspector {
|
|
|
1274
1368
|
|
|
1275
1369
|
// 2. Click: Alt+Click or Element mode click selects element and opens editor
|
|
1276
1370
|
const onClick = (e: MouseEvent) => {
|
|
1277
|
-
if (this.modalOverlay || this.cardOverlay) return;
|
|
1371
|
+
if (this.isHidden || this.modalOverlay || this.cardOverlay) return;
|
|
1278
1372
|
|
|
1279
1373
|
// If clicking inside inspector toolbar or container, do NOT intercept
|
|
1280
1374
|
const path = e.composedPath ? e.composedPath() : [];
|
|
@@ -1505,6 +1599,11 @@ export class NoteInspector {
|
|
|
1505
1599
|
this.updateHighlight(targetEl);
|
|
1506
1600
|
}
|
|
1507
1601
|
|
|
1602
|
+
const comp = noteType === 'element' ? resolveComponentSource(targetEl) : undefined;
|
|
1603
|
+
const compPrefix = comp?.componentName
|
|
1604
|
+
? `🧬 <${comp.componentName}>${comp.sourceFile ? ' (' + comp.sourceFile + (comp.sourceLine ? ':' + comp.sourceLine : '') + ')' : ''} · `
|
|
1605
|
+
: '';
|
|
1606
|
+
|
|
1508
1607
|
this.renderNoteModal({
|
|
1509
1608
|
title: this.activeScenario
|
|
1510
1609
|
? `Step ${this.activeScenario.stepNumber}: ${this.activeScenario.title}`
|
|
@@ -1513,7 +1612,7 @@ export class NoteInspector {
|
|
|
1513
1612
|
pillText:
|
|
1514
1613
|
noteType === 'page'
|
|
1515
1614
|
? `Page Viewport: ${window.innerWidth} × ${window.innerHeight} px`
|
|
1516
|
-
: `${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
|
|
1615
|
+
: `${compPrefix}${getSemanticSelector(targetEl)} (${Math.round(targetEl.getBoundingClientRect().width)}×${Math.round(targetEl.getBoundingClientRect().height)}) · Viewport: ${window.innerWidth}×${window.innerHeight}`,
|
|
1517
1616
|
onSave: async (message, action) => {
|
|
1518
1617
|
let scenarioParam: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string } | undefined;
|
|
1519
1618
|
|
|
@@ -1863,12 +1962,15 @@ export class NoteInspector {
|
|
|
1863
1962
|
innerText = truncate(innerText, 200);
|
|
1864
1963
|
}
|
|
1865
1964
|
|
|
1965
|
+
const componentSource = resolveComponentSource(el);
|
|
1966
|
+
|
|
1866
1967
|
return {
|
|
1867
1968
|
selector,
|
|
1868
1969
|
tag: el.tagName.toLowerCase(),
|
|
1869
1970
|
attributes,
|
|
1870
1971
|
outerHTML,
|
|
1871
1972
|
innerText,
|
|
1973
|
+
componentSource,
|
|
1872
1974
|
parent: el.parentElement
|
|
1873
1975
|
? {
|
|
1874
1976
|
selector: getSemanticSelector(el.parentElement),
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { ComponentSourceInfo } from '../../../core/src/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolves framework component metadata, source file paths, line numbers,
|
|
5
|
+
* and component hierarchy directly from a live DOM element.
|
|
6
|
+
* Supports React (Fiber), Vue 2 & 3, Svelte, Custom Elements, and standard data attributes.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveComponentSource(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
9
|
+
if (!el || typeof el !== 'object') return undefined;
|
|
10
|
+
|
|
11
|
+
// 1. Try React Fiber Resolution
|
|
12
|
+
const reactInfo = resolveReactComponent(el);
|
|
13
|
+
if (reactInfo) return reactInfo;
|
|
14
|
+
|
|
15
|
+
// 2. Try Vue (Vue 3 / Vue 2) Resolution
|
|
16
|
+
const vueInfo = resolveVueComponent(el);
|
|
17
|
+
if (vueInfo) return vueInfo;
|
|
18
|
+
|
|
19
|
+
// 3. Try Svelte Resolution
|
|
20
|
+
const svelteInfo = resolveSvelteComponent(el);
|
|
21
|
+
if (svelteInfo) return svelteInfo;
|
|
22
|
+
|
|
23
|
+
// 4. Try Web Component / Custom Element
|
|
24
|
+
if (el.tagName && el.tagName.includes('-')) {
|
|
25
|
+
return {
|
|
26
|
+
framework: 'web-component',
|
|
27
|
+
componentName: el.tagName.toLowerCase(),
|
|
28
|
+
hierarchy: [el.tagName.toLowerCase()],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 5. Try Data Attribute Annotations (e.g. data-component, data-source-file)
|
|
33
|
+
const attrInfo = resolveDataAttributeComponent(el);
|
|
34
|
+
if (attrInfo) return attrInfo;
|
|
35
|
+
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extracts React Fiber debug source, owner, and hierarchy
|
|
41
|
+
*/
|
|
42
|
+
function resolveReactComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const fiberKey = Object.keys(el).find(
|
|
45
|
+
(key) => key.startsWith('__reactFiber$') || key.startsWith('__reactInternalInstance$')
|
|
46
|
+
);
|
|
47
|
+
if (!fiberKey) return undefined;
|
|
48
|
+
|
|
49
|
+
const hostFiber = (el as any)[fiberKey];
|
|
50
|
+
if (!hostFiber) return undefined;
|
|
51
|
+
|
|
52
|
+
let sourceFile: string | undefined;
|
|
53
|
+
let sourceLine: number | undefined;
|
|
54
|
+
let sourceColumn: number | undefined;
|
|
55
|
+
let componentName: string | undefined;
|
|
56
|
+
const hierarchy: string[] = [];
|
|
57
|
+
let props: Record<string, any> | undefined;
|
|
58
|
+
|
|
59
|
+
// Check host fiber debugSource first (often populated by JSX transform)
|
|
60
|
+
if (hostFiber._debugSource) {
|
|
61
|
+
sourceFile = hostFiber._debugSource.fileName;
|
|
62
|
+
sourceLine = hostFiber._debugSource.lineNumber;
|
|
63
|
+
sourceColumn = hostFiber._debugSource.columnNumber;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check _debugOwner if available
|
|
67
|
+
if (hostFiber._debugOwner) {
|
|
68
|
+
const ownerType = hostFiber._debugOwner.type;
|
|
69
|
+
componentName = getReactComponentName(ownerType);
|
|
70
|
+
if (!sourceFile && hostFiber._debugOwner._debugSource) {
|
|
71
|
+
sourceFile = hostFiber._debugOwner._debugSource.fileName;
|
|
72
|
+
sourceLine = hostFiber._debugOwner._debugSource.lineNumber;
|
|
73
|
+
sourceColumn = hostFiber._debugOwner._debugSource.columnNumber;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Walk up fiber return tree to discover composite components and hierarchy
|
|
78
|
+
let curr = hostFiber;
|
|
79
|
+
while (curr) {
|
|
80
|
+
const type = curr.type;
|
|
81
|
+
const name = getReactComponentName(type);
|
|
82
|
+
|
|
83
|
+
if (name && !name.startsWith('html:') && name !== 'Fragment') {
|
|
84
|
+
if (!componentName) {
|
|
85
|
+
componentName = name;
|
|
86
|
+
}
|
|
87
|
+
if (!hierarchy.includes(name)) {
|
|
88
|
+
hierarchy.unshift(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if composite component has debug source
|
|
92
|
+
if (!sourceFile && curr._debugSource) {
|
|
93
|
+
sourceFile = curr._debugSource.fileName;
|
|
94
|
+
sourceLine = curr._debugSource.lineNumber;
|
|
95
|
+
sourceColumn = curr._debugSource.columnNumber;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Capture safe props from the nearest custom component
|
|
99
|
+
if (!props && curr.memoizedProps && typeof curr.memoizedProps === 'object') {
|
|
100
|
+
props = sanitizeProps(curr.memoizedProps);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
curr = curr.return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!componentName && !sourceFile) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
framework: 'react',
|
|
113
|
+
componentName,
|
|
114
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
115
|
+
sourceLine,
|
|
116
|
+
sourceColumn,
|
|
117
|
+
hierarchy: hierarchy.length > 0 ? hierarchy : componentName ? [componentName] : undefined,
|
|
118
|
+
props,
|
|
119
|
+
};
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getReactComponentName(type: any): string | undefined {
|
|
126
|
+
if (!type) return undefined;
|
|
127
|
+
if (typeof type === 'string') return undefined; // Host DOM tag (div, span, button)
|
|
128
|
+
if (type.displayName) return type.displayName;
|
|
129
|
+
if (type.name) return type.name;
|
|
130
|
+
if (type.render?.displayName) return type.render.displayName;
|
|
131
|
+
if (type.render?.name) return type.render.name;
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Extracts Vue 3 and Vue 2 component instances and file paths
|
|
137
|
+
*/
|
|
138
|
+
function resolveVueComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
139
|
+
try {
|
|
140
|
+
// Vue 3
|
|
141
|
+
const vueParent = (el as any).__vueParentComponent || (el as any).__vnode?.ctx;
|
|
142
|
+
if (vueParent) {
|
|
143
|
+
const type = vueParent.type || {};
|
|
144
|
+
const componentName = type.name || type.__name || type.displayName || 'AnonymousComponent';
|
|
145
|
+
const sourceFile = type.__file;
|
|
146
|
+
const hierarchy: string[] = [];
|
|
147
|
+
|
|
148
|
+
let curr = vueParent;
|
|
149
|
+
while (curr) {
|
|
150
|
+
const cType = curr.type || {};
|
|
151
|
+
const cName = cType.name || cType.__name || cType.displayName;
|
|
152
|
+
if (cName && !hierarchy.includes(cName)) {
|
|
153
|
+
hierarchy.unshift(cName);
|
|
154
|
+
}
|
|
155
|
+
curr = curr.parent;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
framework: 'vue',
|
|
160
|
+
componentName,
|
|
161
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
162
|
+
hierarchy: hierarchy.length > 0 ? hierarchy : [componentName],
|
|
163
|
+
props: vueParent.props ? sanitizeProps(vueParent.props) : undefined,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Vue 2
|
|
168
|
+
const vue2Instance = (el as any).__vue__;
|
|
169
|
+
if (vue2Instance) {
|
|
170
|
+
const options = vue2Instance.$options || {};
|
|
171
|
+
const componentName = options.name || options._componentTag || 'VueComponent';
|
|
172
|
+
const sourceFile = options.__file;
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
framework: 'vue',
|
|
176
|
+
componentName,
|
|
177
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
178
|
+
hierarchy: [componentName],
|
|
179
|
+
props: vue2Instance.$props ? sanitizeProps(vue2Instance.$props) : undefined,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return undefined;
|
|
184
|
+
} catch {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Extracts Svelte __svelte_meta debug locations
|
|
191
|
+
*/
|
|
192
|
+
function resolveSvelteComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
193
|
+
try {
|
|
194
|
+
let curr: HTMLElement | null = el;
|
|
195
|
+
while (curr) {
|
|
196
|
+
const meta = (curr as any).__svelte_meta;
|
|
197
|
+
if (meta && meta.loc) {
|
|
198
|
+
return {
|
|
199
|
+
framework: 'svelte',
|
|
200
|
+
sourceFile: normalizeFilePath(meta.loc.file),
|
|
201
|
+
sourceLine: meta.loc.line,
|
|
202
|
+
sourceColumn: meta.loc.column,
|
|
203
|
+
componentName: meta.loc.file ? getBaseNameWithoutExt(meta.loc.file) : undefined,
|
|
204
|
+
hierarchy: meta.loc.file ? [getBaseNameWithoutExt(meta.loc.file)] : undefined,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
curr = curr.parentElement;
|
|
208
|
+
}
|
|
209
|
+
return undefined;
|
|
210
|
+
} catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Resolves components from standard data attributes (e.g. data-component="UserCard")
|
|
217
|
+
*/
|
|
218
|
+
function resolveDataAttributeComponent(el: HTMLElement): ComponentSourceInfo | undefined {
|
|
219
|
+
try {
|
|
220
|
+
const compEl = el.closest('[data-component], [data-component-name], [data-source-file]');
|
|
221
|
+
if (!compEl) return undefined;
|
|
222
|
+
|
|
223
|
+
const componentName =
|
|
224
|
+
compEl.getAttribute('data-component') || compEl.getAttribute('data-component-name') || undefined;
|
|
225
|
+
const sourceFile = compEl.getAttribute('data-source-file') || undefined;
|
|
226
|
+
const lineAttr = compEl.getAttribute('data-source-line');
|
|
227
|
+
const sourceLine = lineAttr ? parseInt(lineAttr, 10) : undefined;
|
|
228
|
+
|
|
229
|
+
if (!componentName && !sourceFile) return undefined;
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
framework: 'vanilla',
|
|
233
|
+
componentName,
|
|
234
|
+
sourceFile: normalizeFilePath(sourceFile),
|
|
235
|
+
sourceLine: isNaN(sourceLine as number) ? undefined : sourceLine,
|
|
236
|
+
hierarchy: componentName ? [componentName] : undefined,
|
|
237
|
+
};
|
|
238
|
+
} catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function normalizeFilePath(filePath?: string): string | undefined {
|
|
244
|
+
if (!filePath) return undefined;
|
|
245
|
+
// Strip query strings or vite bundle hashes (e.g. /App.tsx?t=123)
|
|
246
|
+
const cleaned = filePath.split('?')[0];
|
|
247
|
+
return cleaned;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getBaseNameWithoutExt(filePath: string): string {
|
|
251
|
+
const parts = filePath.split('/');
|
|
252
|
+
const last = parts[parts.length - 1] || filePath;
|
|
253
|
+
return last.split('.')[0] || last;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sanitizeProps(props: Record<string, any>): Record<string, any> {
|
|
257
|
+
const result: Record<string, any> = {};
|
|
258
|
+
for (const [key, value] of Object.entries(props)) {
|
|
259
|
+
if (key.startsWith('__') || typeof value === 'function') continue;
|
|
260
|
+
|
|
261
|
+
if (value === null || value === undefined) {
|
|
262
|
+
result[key] = value;
|
|
263
|
+
} else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
264
|
+
result[key] = value;
|
|
265
|
+
} else if (Array.isArray(value)) {
|
|
266
|
+
result[key] = `Array(${value.length})`;
|
|
267
|
+
} else if (typeof value === 'object') {
|
|
268
|
+
result[key] = '[Object]';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ComponentSourceInfo } from './notes.js';
|
|
2
|
+
|
|
1
3
|
export type EventType =
|
|
2
4
|
| 'runtime_error'
|
|
3
5
|
| 'unhandled_rejection'
|
|
@@ -17,6 +19,7 @@ export interface ElementSummary {
|
|
|
17
19
|
attributes?: Record<string, string>;
|
|
18
20
|
outerHTML?: string;
|
|
19
21
|
innerText?: string;
|
|
22
|
+
componentSource?: ComponentSourceInfo;
|
|
20
23
|
boundingRect?: {
|
|
21
24
|
x: number;
|
|
22
25
|
y: number;
|
|
@@ -39,12 +39,23 @@ export interface NoteTarget {
|
|
|
39
39
|
confidence?: 'high' | 'medium' | 'low';
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface ComponentSourceInfo {
|
|
43
|
+
framework?: 'react' | 'vue' | 'svelte' | 'web-component' | 'vanilla';
|
|
44
|
+
componentName?: string;
|
|
45
|
+
sourceFile?: string;
|
|
46
|
+
sourceLine?: number;
|
|
47
|
+
sourceColumn?: number;
|
|
48
|
+
hierarchy?: string[];
|
|
49
|
+
props?: Record<string, any>;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
export interface ElementContext {
|
|
43
53
|
selector: string;
|
|
44
54
|
tag: string;
|
|
45
55
|
attributes?: Record<string, string>;
|
|
46
56
|
outerHTML?: string;
|
|
47
57
|
innerText?: string;
|
|
58
|
+
componentSource?: ComponentSourceInfo;
|
|
48
59
|
parent?: {
|
|
49
60
|
selector: string;
|
|
50
61
|
tag: string;
|
|
@@ -104,6 +104,7 @@ export async function handleToolCall(name: string, args: any, ctx: McpContext):
|
|
|
104
104
|
visible: incident.lastElement.visible,
|
|
105
105
|
innerText: incident.lastElement.innerText,
|
|
106
106
|
outerHTML: incident.lastElement.outerHTML,
|
|
107
|
+
componentSource: incident.lastElement.componentSource,
|
|
107
108
|
}
|
|
108
109
|
: undefined,
|
|
109
110
|
recentBreadcrumbs: breadcrumbsTimeline,
|