browsertrack 0.2.2 → 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.
@@ -72,12 +72,19 @@ interface SelectorOptions {
72
72
  maxClasses?: number;
73
73
  maxOuterHTMLLength?: number;
74
74
  maxInnerTextLength?: number;
75
+ maxParentDepth?: number;
75
76
  }
76
77
  /**
77
78
  * Builds a clean, human-readable semantic selector for a DOM element.
78
- * Prioritizes data-testid/data-test, id, semantic tag + specific class, or concise parent path.
79
+ * Prioritizes data-testid/data-component, id, semantic tag + curated class, aria/role, or concise unique parent path.
79
80
  */
80
81
  declare function getSemanticSelector(element: HTMLElement | Element, options?: SelectorOptions): string;
82
+ /**
83
+ * If an element under the cursor is an inline or leaf formatting element
84
+ * (e.g. svg, path, i, b, strong, or an inner span without attributes)
85
+ * inside an interactive container or component, resolves to the parent container.
86
+ */
87
+ declare function resolveMeaningfulTarget(element: HTMLElement | Element): HTMLElement;
81
88
  /**
82
89
  * Truncates string to specified max length with ellipsis
83
90
  */
@@ -106,4 +113,4 @@ declare function safeExecute<T>(fn: () => T, fallback: T, onError?: (err: any) =
106
113
  */
107
114
  declare function safeAsync<T>(action: Promise<T> | (() => Promise<T>), fallback: T, onError?: (err: any) => void): Promise<T>;
108
115
 
109
- export { BROWSER_SECURITY_RULES, type FingerprintInput, REDACTED_PLACEHOLDER, SENSITIVE_KEY_PATTERNS, SENSITIVE_QUERY_PARAMS, type SelectorOptions, computeFingerprint, extractSourceFromStack, getSemanticSelector, isSensitiveKey, normalizeErrorMessage, normalizeSourceFile, redactHeaders, redactSensitiveData, redactUrl, safeAsync, safeExecute, safeJsonParse, safeJsonStringify, truncate };
116
+ export { BROWSER_SECURITY_RULES, type FingerprintInput, REDACTED_PLACEHOLDER, SENSITIVE_KEY_PATTERNS, SENSITIVE_QUERY_PARAMS, type SelectorOptions, computeFingerprint, extractSourceFromStack, getSemanticSelector, isSensitiveKey, normalizeErrorMessage, normalizeSourceFile, redactHeaders, redactSensitiveData, redactUrl, resolveMeaningfulTarget, safeAsync, safeExecute, safeJsonParse, safeJsonStringify, truncate };
@@ -12,6 +12,7 @@ import {
12
12
  redactHeaders,
13
13
  redactSensitiveData,
14
14
  redactUrl,
15
+ resolveMeaningfulTarget,
15
16
  safeAsync,
16
17
  safeExecute,
17
18
  safeJsonParse,
@@ -19,7 +20,7 @@ import {
19
20
  standardRules,
20
21
  truncate,
21
22
  visulimaRedact
22
- } from "../chunk-QRZ57ME3.js";
23
+ } from "../chunk-ONPW7AYL.js";
23
24
  export {
24
25
  BROWSER_SECURITY_RULES,
25
26
  REDACTED_PLACEHOLDER,
@@ -34,6 +35,7 @@ export {
34
35
  redactHeaders,
35
36
  redactSensitiveData,
36
37
  redactUrl,
38
+ resolveMeaningfulTarget,
37
39
  safeAsync,
38
40
  safeExecute,
39
41
  safeJsonParse,
@@ -11,8 +11,8 @@ import {
11
11
  createHttpHandler,
12
12
  getDaemonConfig,
13
13
  setupWebSocketServer
14
- } from "../chunk-AYSVE6NG.js";
15
- import "../chunk-QRZ57ME3.js";
14
+ } from "../chunk-G5CIZSQM.js";
15
+ import "../chunk-ONPW7AYL.js";
16
16
  export {
17
17
  BrowserTrackDaemon,
18
18
  IncidentEngine,
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { B as Breadcrumb, C as CaptureElementParams, a as CaptureElementResult, b as ClientCommand, c as ClientEventMessage, d as CommandResponse, e as CommandType, f as ComponentSourceInfo, g as ConsoleEvent, h as ConsoleLevel, D as DOMRectJson, E as ElementContext, i as ElementStyleResult, j as ElementSummary, k as EventType, H as HelloMessage, N as NavigateParams, l as NavigationEvent, m as NetworkEvent, n as NoteStatus, o as NoteTarget, p as NoteType, q as NoteVerificationResult, O as OverflowCheckResult, P as PageStateResult, Q as QueryElementParams, r as QueryElementResult, R as RegionContext, s as ReloadParams, t as RuntimeErrorEvent, S as ScenarioDetail, u as ScenarioOverview, v as ScrollContext, V as ViewportContext, w as VisualNote } from './commands-fjuqKzkm.js';
2
2
  export { I as Incident, a as IncidentOccurrence, b as IncidentSeverity, c as IncidentSource, d as IncidentStatus, P as ProbeResult, e as ProbeType, f as Project, S as Session, V as VerificationProbe, g as VerificationRecipe, h as VerificationResult } from './projects-DB7S312i.js';
3
- export { BROWSER_SECURITY_RULES, FingerprintInput, REDACTED_PLACEHOLDER, SENSITIVE_KEY_PATTERNS, SENSITIVE_QUERY_PARAMS, SelectorOptions, computeFingerprint, extractSourceFromStack, getSemanticSelector, isSensitiveKey, normalizeErrorMessage, normalizeSourceFile, redactHeaders, redactSensitiveData, redactUrl, safeAsync, safeExecute, safeJsonParse, safeJsonStringify, truncate } from './core/index.js';
3
+ export { BROWSER_SECURITY_RULES, FingerprintInput, REDACTED_PLACEHOLDER, SENSITIVE_KEY_PATTERNS, SENSITIVE_QUERY_PARAMS, SelectorOptions, computeFingerprint, extractSourceFromStack, getSemanticSelector, isSensitiveKey, normalizeErrorMessage, normalizeSourceFile, redactHeaders, redactSensitiveData, redactUrl, resolveMeaningfulTarget, safeAsync, safeExecute, safeJsonParse, safeJsonStringify, truncate } from './core/index.js';
4
4
  export { BrowserTrackClient, getClient, init as initClient } from './client/index.js';
5
5
  export { BrowserTrackDaemon, createDaemon } from './daemon/index.js';
6
- export { c as createMcpServer } from './server-DjV7RWQM.js';
6
+ export { c as createMcpServer } from './server-BRG-RQQP.js';
7
7
  export { Rules, standardRules, redact as visulimaRedact } from '@visulima/redact';
8
8
  import './engine-CmchnMDq.js';
9
9
  import 'ws';
package/dist/index.js CHANGED
@@ -2,14 +2,14 @@ import {
2
2
  BrowserTrackClient,
3
3
  getClient,
4
4
  init
5
- } from "./chunk-4HRLW6YF.js";
5
+ } from "./chunk-PG4JJDCV.js";
6
6
  import {
7
7
  createMcpServer
8
- } from "./chunk-TWEYRBDU.js";
8
+ } from "./chunk-5NR5K3ER.js";
9
9
  import {
10
10
  BrowserTrackDaemon,
11
11
  createDaemon
12
- } from "./chunk-AYSVE6NG.js";
12
+ } from "./chunk-G5CIZSQM.js";
13
13
  import {
14
14
  BROWSER_SECURITY_RULES,
15
15
  REDACTED_PLACEHOLDER,
@@ -24,6 +24,7 @@ import {
24
24
  redactHeaders,
25
25
  redactSensitiveData,
26
26
  redactUrl,
27
+ resolveMeaningfulTarget,
27
28
  safeAsync,
28
29
  safeExecute,
29
30
  safeJsonParse,
@@ -31,7 +32,7 @@ import {
31
32
  standardRules,
32
33
  truncate,
33
34
  visulimaRedact
34
- } from "./chunk-QRZ57ME3.js";
35
+ } from "./chunk-ONPW7AYL.js";
35
36
  export {
36
37
  BROWSER_SECURITY_RULES,
37
38
  BrowserTrackClient,
@@ -52,6 +53,7 @@ export {
52
53
  redactHeaders,
53
54
  redactSensitiveData,
54
55
  redactUrl,
56
+ resolveMeaningfulTarget,
55
57
  safeAsync,
56
58
  safeExecute,
57
59
  safeJsonParse,
@@ -1,5 +1,5 @@
1
1
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
- export { M as McpContext, a as McpServerOptions, c as createMcpServer, h as handleToolCall, i as isDaemonRunning } from '../server-DjV7RWQM.js';
2
+ export { M as McpContext, a as McpServerOptions, c as createMcpServer, h as handleToolCall, i as isDaemonRunning } from '../server-BRG-RQQP.js';
3
3
  import '@modelcontextprotocol/sdk/server/index.js';
4
4
  import '../engine-CmchnMDq.js';
5
5
  import '../commands-fjuqKzkm.js';
package/dist/mcp/index.js CHANGED
@@ -3,9 +3,9 @@ import {
3
3
  createMcpServer,
4
4
  handleToolCall,
5
5
  isDaemonRunning
6
- } from "../chunk-TWEYRBDU.js";
7
- import "../chunk-AYSVE6NG.js";
8
- import "../chunk-QRZ57ME3.js";
6
+ } from "../chunk-5NR5K3ER.js";
7
+ import "../chunk-G5CIZSQM.js";
8
+ import "../chunk-ONPW7AYL.js";
9
9
  export {
10
10
  TOOLS,
11
11
  createMcpServer,
@@ -7,6 +7,7 @@ interface McpContext {
7
7
  verificationEngine?: VerificationEngine;
8
8
  noteVerificationEngine?: NoteVerificationEngine;
9
9
  daemonUrl?: string;
10
+ daemonInitPromise?: Promise<void>;
10
11
  }
11
12
  declare function handleToolCall(name: string, args: any, ctx: McpContext): Promise<any>;
12
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browsertrack",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Local browser diagnostics + MCP bridge for coding agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- :host {
228
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
229
- color-scheme: dark;
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.2s ease;
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 {
@@ -1108,6 +1247,14 @@ export class NoteInspector {
1108
1247
 
1109
1248
  const rect = targetEl ? targetEl.getBoundingClientRect() : note.target?.boundingRect;
1110
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
+
1111
1258
  const pin = document.createElement('div');
1112
1259
  pin.className = markerClass;
1113
1260
  pin.setAttribute('data-note-id', note.id);
@@ -1361,7 +1508,8 @@ export class NoteInspector {
1361
1508
  }
1362
1509
 
1363
1510
  if (e.altKey || this.activeMode === 'element') {
1364
- const target = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
1511
+ const rawTarget = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null;
1512
+ const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
1365
1513
  if (target && target !== this.container && !this.container?.contains(target)) {
1366
1514
  this.hoveredElement = target;
1367
1515
  this.updateHighlight(target);
@@ -1394,7 +1542,8 @@ export class NoteInspector {
1394
1542
  e.preventDefault();
1395
1543
  e.stopPropagation();
1396
1544
 
1397
- const target = (this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY)) as HTMLElement | null;
1545
+ const rawTarget = (this.hoveredElement || document.elementFromPoint(e.clientX, e.clientY)) as HTMLElement | null;
1546
+ const target = rawTarget ? resolveMeaningfulTarget(rawTarget) : null;
1398
1547
  if (target && target !== this.container && !this.container?.contains(target)) {
1399
1548
  this.selectedElement = target;
1400
1549
  this.openNoteEditor(target, 'element');
@@ -1408,9 +1557,15 @@ export class NoteInspector {
1408
1557
  }
1409
1558
  };
1410
1559
 
1411
- // 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
1412
1561
  const onKeyDown = (e: KeyboardEvent) => {
1413
1562
  try {
1563
+ if (e.altKey && (e.key === 't' || e.key === 'T')) {
1564
+ e.preventDefault();
1565
+ this.toggleToolbarCollapse();
1566
+ return;
1567
+ }
1568
+
1414
1569
  if (e.key === 'Escape') {
1415
1570
  if (this.cardOverlay && this.cardOverlay.parentElement && this.shadowRoot) {
1416
1571
  this.shadowRoot.removeChild(this.cardOverlay);
@@ -1542,6 +1697,11 @@ export class NoteInspector {
1542
1697
  this.regionBox.appendChild(badge);
1543
1698
  }
1544
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
+ }
1545
1705
  };
1546
1706
 
1547
1707
  this.regionOverlay.onmouseup = (e: MouseEvent) => {
@@ -1610,6 +1770,22 @@ export class NoteInspector {
1610
1770
  const selector = getSemanticSelector(el);
1611
1771
  const label = this.activeScenario ? `🎬 Step ${this.activeScenario.stepNumber} · ${selector}` : selector;
1612
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
+ }
1613
1789
  }
1614
1790
 
1615
1791
  private hideHighlight(): void {
@@ -1797,10 +1973,13 @@ export class NoteInspector {
1797
1973
  if (btnSave) btnSave.disabled = true;
1798
1974
  if (btnFinishFlow) btnFinishFlow.disabled = true;
1799
1975
 
1976
+ // Close modal immediately for instant, snappy user experience
1977
+ closeModal();
1978
+
1800
1979
  try {
1801
1980
  await options.onSave(message, action);
1802
- } finally {
1803
- closeModal();
1981
+ } catch {
1982
+ this.showToast('Failed to save note', '⚠️');
1804
1983
  }
1805
1984
  };
1806
1985
 
@@ -1835,11 +2014,15 @@ export class NoteInspector {
1835
2014
  const rect = targetEl.getBoundingClientRect();
1836
2015
  const selector = noteType === 'page' ? 'body' : getSemanticSelector(targetEl);
1837
2016
 
1838
- // 1. Capture screenshot via screenshotDriver
2017
+ // 1. Capture screenshot via screenshotDriver with fast timeout protection
1839
2018
  let screenshotDataUrl: string | undefined;
1840
2019
  try {
1841
- const snap = await this.screenshotDriver.captureElement(targetEl);
1842
- if (snap.ok) {
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) {
1843
2026
  screenshotDataUrl = snap.dataUrl;
1844
2027
  }
1845
2028
  } catch {
@@ -1910,12 +2093,20 @@ export class NoteInspector {
1910
2093
  message: string,
1911
2094
  scenario?: { scenarioId?: string; stepNumber?: number; scenarioTitle?: string }
1912
2095
  ): Promise<void> {
1913
- // 1. Capture full page screenshot and crop to region
2096
+ // 1. Fast regional capture: capture element under region center rather than heavy whole-document serialization
1914
2097
  let screenshotDataUrl: string | undefined;
1915
2098
  try {
1916
- const snap = await this.screenshotDriver.captureElement(document.body || document.documentElement);
1917
- if (snap.ok && snap.dataUrl) {
1918
- screenshotDataUrl = await this.cropDataUrl(snap.dataUrl, region);
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;
1919
2110
  }
1920
2111
  } catch {
1921
2112
  // Defensive