@leftium/gg 0.0.33 → 0.0.34

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.
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Node.js-specific debug implementation.
3
+ *
4
+ * Output: process.stderr via util.formatWithOptions.
5
+ * Persistence: process.env.DEBUG
6
+ * Format (patched): +123ms namespace message (ANSI colored)
7
+ */
8
+ import { setup, humanize } from './common.js';
9
+ import tty from 'tty';
10
+ import util from 'util';
11
+ /**
12
+ * Basic ANSI colors (6) — used when 256-color support is not detected.
13
+ * Extended 256-color palette matches debug@4 for color-hash stability.
14
+ */
15
+ const basicColors = [6, 2, 3, 4, 5, 1];
16
+ const extendedColors = [
17
+ 20, 21, 26, 27, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45,
18
+ 56, 57, 62, 63, 68, 69, 74, 75, 76, 77, 78, 79, 80, 81,
19
+ 92, 93, 98, 99, 112, 113, 128, 129, 134, 135, 148, 149,
20
+ 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171,
21
+ 172, 173, 178, 179, 184, 185, 196, 197, 198, 199, 200, 201,
22
+ 202, 203, 204, 205, 206, 207, 208, 209, 214, 215, 220, 221
23
+ ];
24
+ /** Detect 256-color support via supports-color (optional) or heuristic */
25
+ function detectColors() {
26
+ try {
27
+ // Try supports-color if available (same as debug@4)
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
29
+ const supportsColor = require('supports-color');
30
+ if (supportsColor && (supportsColor.stderr || supportsColor).level >= 2) {
31
+ return extendedColors;
32
+ }
33
+ }
34
+ catch {
35
+ // Not installed — fall through
36
+ }
37
+ return basicColors;
38
+ }
39
+ /**
40
+ * Build inspectOpts from DEBUG_* environment variables.
41
+ * Supports: DEBUG_COLORS, DEBUG_DEPTH, DEBUG_SHOW_HIDDEN, DEBUG_HIDE_DATE
42
+ */
43
+ const inspectOpts = Object.keys(process.env)
44
+ .filter((key) => /^debug_/i.test(key))
45
+ .reduce((obj, key) => {
46
+ const prop = key
47
+ .substring(6)
48
+ .toLowerCase()
49
+ .replace(/_([a-z])/g, (_, k) => k.toUpperCase());
50
+ let val = process.env[key];
51
+ if (/^(yes|on|true|enabled)$/i.test(val))
52
+ val = true;
53
+ else if (/^(no|off|false|disabled)$/i.test(val))
54
+ val = false;
55
+ else if (val === 'null')
56
+ val = null;
57
+ else
58
+ val = Number(val);
59
+ obj[prop] = val;
60
+ return obj;
61
+ }, {});
62
+ function useColors() {
63
+ return 'colors' in inspectOpts
64
+ ? Boolean(inspectOpts.colors)
65
+ : tty.isatty(process.stderr.fd);
66
+ }
67
+ function getDate() {
68
+ if (inspectOpts.hideDate)
69
+ return '';
70
+ return new Date().toISOString() + ' ';
71
+ }
72
+ /**
73
+ * Format args with ANSI colors and gg's patched prefix order:
74
+ * +123ms namespace message
75
+ */
76
+ function formatArgs(args) {
77
+ const name = this.namespace;
78
+ const useCol = this.useColors;
79
+ if (useCol) {
80
+ const c = Number(this.color);
81
+ const colorCode = '\u001B[3' + (c < 8 ? String(c) : '8;5;' + c);
82
+ const h = ('+' + humanize(this.diff)).padStart(6);
83
+ const prefix = `${colorCode};1m${h} ${name} \u001B[0m`;
84
+ args[0] = prefix + String(args[0]).split('\n').join('\n' + prefix);
85
+ // Append empty color reset (preserves arg count parity from original debug)
86
+ args.push(colorCode + '' + '\u001B[0m');
87
+ }
88
+ else {
89
+ args[0] = getDate() + name + ' ' + args[0];
90
+ }
91
+ }
92
+ function log(...args) {
93
+ process.stderr.write(util.formatWithOptions(inspectOpts, ...args) + '\n');
94
+ }
95
+ function save(namespaces) {
96
+ if (namespaces) {
97
+ process.env.DEBUG = namespaces;
98
+ }
99
+ else {
100
+ delete process.env.DEBUG;
101
+ }
102
+ }
103
+ function load() {
104
+ return process.env.DEBUG || '';
105
+ }
106
+ function init(instance) {
107
+ // Each instance gets its own inspectOpts copy (for per-instance color override)
108
+ instance.inspectOpts = { ...inspectOpts };
109
+ }
110
+ const env = {
111
+ formatArgs,
112
+ save,
113
+ load,
114
+ useColors,
115
+ colors: detectColors(),
116
+ log,
117
+ init,
118
+ formatters: {
119
+ /** %o → util.inspect, single line */
120
+ o(v) {
121
+ const opts = this.inspectOpts || {};
122
+ opts.colors = this.useColors;
123
+ return util.inspect(v, opts)
124
+ .split('\n')
125
+ .map((str) => str.trim())
126
+ .join(' ');
127
+ },
128
+ /** %O → util.inspect, multi-line */
129
+ O(v) {
130
+ const opts = this.inspectOpts || {};
131
+ opts.colors = this.useColors;
132
+ return util.inspect(v, opts);
133
+ }
134
+ }
135
+ };
136
+ const debug = setup(env);
137
+ export default debug;
@@ -79,17 +79,6 @@ export async function loadEruda(options) {
79
79
  // Ensure tool is always visible in case user customizes
80
80
  tool: ['console', 'elements', 'network', 'resources', 'info', 'snippets', 'sources']
81
81
  });
82
- // Auto-enable localStorage.debug if requested and unset
83
- if (options.autoEnable !== false) {
84
- try {
85
- if (!localStorage.getItem('debug')) {
86
- localStorage.setItem('debug', 'gg:*');
87
- }
88
- }
89
- catch {
90
- // localStorage might not be available
91
- }
92
- }
93
82
  // Register gg plugin
94
83
  // Import gg and pass it to the plugin directly
95
84
  const { gg, runGgDiagnostics } = await import('../gg.js');
@@ -19,8 +19,12 @@ export function createGgPlugin(options, gg) {
19
19
  let filterExpanded = false;
20
20
  let filterPattern = '';
21
21
  const enabledNamespaces = new Set();
22
+ // Last rendered entries (for hover tooltip arg lookup)
23
+ let renderedEntries = [];
22
24
  // Settings UI state
23
25
  let settingsExpanded = false;
26
+ // Filter pattern persistence key (independent of localStorage.debug)
27
+ const FILTER_KEY = 'gg-filter';
24
28
  // Namespace click action: 'open' uses Vite dev middleware, 'copy' copies formatted string, 'open-url' navigates to URI
25
29
  const NS_ACTION_KEY = 'gg-ns-action';
26
30
  const EDITOR_BIN_KEY = 'gg-editor-bin';
@@ -78,13 +82,6 @@ export function createGgPlugin(options, gg) {
78
82
  };
79
83
  let nsClickAction = localStorage.getItem(NS_ACTION_KEY) || (DEV ? 'open' : 'copy');
80
84
  let editorBin = localStorage.getItem(EDITOR_BIN_KEY) || '';
81
- // Separate format strings for copy vs URL modes (each persisted independently)
82
- // Migrate from old single-key format (gg-editor-format) if present
83
- const legacyFormat = localStorage.getItem('gg-editor-format');
84
- if (legacyFormat && !localStorage.getItem(COPY_FORMAT_KEY)) {
85
- localStorage.setItem(COPY_FORMAT_KEY, legacyFormat);
86
- localStorage.removeItem('gg-editor-format');
87
- }
88
85
  let copyFormat = localStorage.getItem(COPY_FORMAT_KEY) || copyPresets['Raw path'];
89
86
  let urlFormat = localStorage.getItem(URL_FORMAT_KEY) || uriPresets['VS Code'];
90
87
  let projectRoot = localStorage.getItem(PROJECT_ROOT_KEY) || '';
@@ -107,24 +104,27 @@ export function createGgPlugin(options, gg) {
107
104
  name: 'GG',
108
105
  init($container) {
109
106
  $el = $container;
107
+ // Load filter state BEFORE registering _onLog hook, because setting _onLog
108
+ // triggers replay of earlyLogBuffer and each entry checks filterPattern
109
+ filterPattern = localStorage.getItem(FILTER_KEY) || 'gg:*';
110
110
  // Register the capture hook on gg
111
111
  if (gg) {
112
112
  gg._onLog = (entry) => {
113
+ // Track namespaces for filter UI update (check BEFORE pushing to buffer)
114
+ const hadNamespace = getAllCapturedNamespaces().includes(entry.namespace);
113
115
  buffer.push(entry);
114
116
  // Add new namespace to enabledNamespaces if it matches the current pattern
115
117
  const effectivePattern = filterPattern || 'gg:*';
116
118
  if (namespaceMatchesPattern(entry.namespace, effectivePattern)) {
117
119
  enabledNamespaces.add(entry.namespace);
118
120
  }
119
- // Update filter UI if expanded (new namespace may have appeared)
120
- if (filterExpanded) {
121
+ // Update filter UI if new namespace appeared (updates button summary count)
122
+ if (!hadNamespace) {
121
123
  renderFilterUI();
122
124
  }
123
125
  renderLogs();
124
126
  };
125
127
  }
126
- // Load initial filter state
127
- filterPattern = localStorage.getItem('debug') || '';
128
128
  // Probe for openInEditorPlugin (status 222) and auto-populate $ROOT in dev mode
129
129
  if (DEV) {
130
130
  fetch('/__open-in-editor?file=+')
@@ -420,21 +420,38 @@ export function createGgPlugin(options, gg) {
420
420
  word-break: break-word;
421
421
  padding: 4px 0;
422
422
  position: relative;
423
+ -webkit-user-select: text !important;
424
+ user-select: text !important;
425
+ cursor: text;
426
+ }
427
+ .gg-log-content * {
428
+ -webkit-user-select: text !important;
429
+ user-select: text !important;
430
+ }
431
+ .gg-log-diff, .gg-log-ns {
432
+ -webkit-user-select: text !important;
433
+ user-select: text !important;
423
434
  }
424
- /* Fast custom tooltip for src expression (no delay like native title) */
435
+ .gg-details, .gg-details * {
436
+ -webkit-user-select: text !important;
437
+ user-select: text !important;
438
+ cursor: text;
439
+ }
440
+ /* Fast custom tooltip for src expression on primitive-only rows (no expandable objects) */
425
441
  .gg-log-content[data-src] {
426
442
  cursor: help;
427
443
  }
428
- .gg-log-content[data-src]::before {
429
- content: '🔍';
444
+ /* Show icon only on primitive rows (no .gg-expand child) */
445
+ .gg-log-content[data-src]:not(:has(.gg-expand))::before {
446
+ content: '\uD83D\uDD0D';
430
447
  font-size: 10px;
431
448
  margin-right: 4px;
432
449
  opacity: 0.4;
433
450
  }
434
- .gg-log-content[data-src]:hover::before {
451
+ .gg-log-content[data-src]:not(:has(.gg-expand)):hover::before {
435
452
  opacity: 1;
436
453
  }
437
- .gg-log-content[data-src]::after {
454
+ .gg-log-content[data-src]:not(:has(.gg-expand))::after {
438
455
  content: attr(data-src);
439
456
  position: absolute;
440
457
  top: 100%;
@@ -454,9 +471,56 @@ export function createGgPlugin(options, gg) {
454
471
  overflow: hidden;
455
472
  text-overflow: ellipsis;
456
473
  }
457
- .gg-log-content[data-src]:hover::after {
474
+ .gg-log-content[data-src]:not(:has(.gg-expand)):hover::after {
475
+ opacity: 1;
476
+ }
477
+ /* Expression icon inline with expandable object labels */
478
+ .gg-src-icon {
479
+ font-size: 10px;
480
+ margin-right: 2px;
481
+ opacity: 0.4;
482
+ cursor: pointer;
483
+ }
484
+ .gg-expand:hover .gg-src-icon,
485
+ .gg-src-icon:hover {
458
486
  opacity: 1;
459
487
  }
488
+ /* Hover tooltip for expandable objects/arrays */
489
+ .gg-hover-tooltip {
490
+ display: none;
491
+ position: fixed;
492
+ background: #1e1e1e;
493
+ color: #d4d4d4;
494
+ font-size: 11px;
495
+ font-family: monospace;
496
+ padding: 8px 10px;
497
+ border-radius: 4px;
498
+ white-space: pre;
499
+ pointer-events: none;
500
+ z-index: 100000;
501
+ max-width: min(90vw, 500px);
502
+ max-height: 300px;
503
+ overflow: auto;
504
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
505
+ line-height: 1.4;
506
+ }
507
+ .gg-hover-tooltip-src {
508
+ color: #9cdcfe;
509
+ font-style: italic;
510
+ margin-bottom: 4px;
511
+ padding-bottom: 4px;
512
+ border-bottom: 1px solid #444;
513
+ }
514
+ /* Expression header inside expanded details */
515
+ .gg-details-src {
516
+ color: #555;
517
+ font-style: italic;
518
+ font-family: monospace;
519
+ font-size: 11px;
520
+ margin-bottom: 6px;
521
+ padding-bottom: 4px;
522
+ border-bottom: 1px solid #ddd;
523
+ }
460
524
  .gg-filter-panel {
461
525
  background: #f5f5f5;
462
526
  padding: 10px;
@@ -683,7 +747,7 @@ export function createGgPlugin(options, gg) {
683
747
  }
684
748
  function applyPatternFromInput(value) {
685
749
  filterPattern = value;
686
- localStorage.setItem('debug', filterPattern);
750
+ localStorage.setItem(FILTER_KEY, filterPattern);
687
751
  // Sync enabledNamespaces from the new pattern
688
752
  const allNamespaces = getAllCapturedNamespaces();
689
753
  enabledNamespaces.clear();
@@ -746,7 +810,7 @@ export function createGgPlugin(options, gg) {
746
810
  filterPattern = `gg:*,${exclusions}`;
747
811
  enabledNamespaces.clear();
748
812
  }
749
- localStorage.setItem('debug', filterPattern);
813
+ localStorage.setItem(FILTER_KEY, filterPattern);
750
814
  renderFilterUI();
751
815
  renderLogs();
752
816
  return;
@@ -893,6 +957,37 @@ export function createGgPlugin(options, gg) {
893
957
  else if (nsClickAction === 'copy' || nsClickAction === 'open-url') {
894
958
  optionsSection = renderFormatSection(nsClickAction === 'open-url');
895
959
  }
960
+ // Native Console section
961
+ const currentDebugValue = localStorage.getItem('debug');
962
+ const debugDisplay = currentDebugValue !== null ? `'${escapeHtml(currentDebugValue)}'` : 'not set';
963
+ const currentFilter = filterPattern || 'gg:*';
964
+ const debugMatchesFilter = currentDebugValue === currentFilter;
965
+ const debugIncludesGg = currentDebugValue !== null &&
966
+ (currentDebugValue.includes('gg:') || currentDebugValue === '*');
967
+ const nativeConsoleSection = `
968
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #ddd;">
969
+ <div class="gg-settings-label">Native Console Output</div>
970
+ <div style="font-size: 11px; opacity: 0.7; margin-bottom: 8px;">
971
+ gg messages always appear in this GG panel. To also see them in the browser's native console, set <code>localStorage.debug</code> below.
972
+ For server-side: <code>DEBUG=gg:* npm run dev</code>
973
+ </div>
974
+ <div style="font-family: monospace; font-size: 12px; margin-bottom: 6px;">
975
+ localStorage.debug = ${debugDisplay}
976
+ ${debugIncludesGg ? '<span style="color: green;">✅</span>' : '<span style="color: #999;">⚫ gg:* not included</span>'}
977
+ </div>
978
+ <div style="display: flex; gap: 6px; flex-wrap: wrap;">
979
+ <button class="gg-sync-debug-btn" style="padding: 4px 10px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; background: ${debugMatchesFilter ? '#e8e8e8' : '#fff'};"${debugMatchesFilter ? ' disabled' : ''}>
980
+ ${debugMatchesFilter ? 'In sync' : `Set to '${escapeHtml(currentFilter)}'`}
981
+ </button>
982
+ <button class="gg-clear-debug-btn" style="padding: 4px 10px; font-size: 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 3px; background: #fff;"${currentDebugValue === null ? ' disabled' : ''}>
983
+ Clear
984
+ </button>
985
+ </div>
986
+ <div style="font-size: 10px; opacity: 0.5; margin-top: 4px;">
987
+ Changes take effect on next page reload.
988
+ </div>
989
+ </div>
990
+ `;
896
991
  settingsPanel.innerHTML = `
897
992
  ${callSitesWarning}
898
993
  <div class="gg-settings-label">When namespace clicked:</div>
@@ -911,6 +1006,7 @@ export function createGgPlugin(options, gg) {
911
1006
  </label>
912
1007
  </div>
913
1008
  ${optionsSection}
1009
+ ${nativeConsoleSection}
914
1010
  `;
915
1011
  }
916
1012
  else {
@@ -970,7 +1066,7 @@ export function createGgPlugin(options, gg) {
970
1066
  target.blur();
971
1067
  }
972
1068
  });
973
- // Preset button clicks + editor bin button clicks
1069
+ // Preset button clicks + editor bin button clicks + native console buttons
974
1070
  settingsPanel.addEventListener('click', (e) => {
975
1071
  const target = e.target;
976
1072
  if (target.classList.contains('gg-preset-btn')) {
@@ -988,6 +1084,17 @@ export function createGgPlugin(options, gg) {
988
1084
  renderSettingsUI();
989
1085
  }
990
1086
  }
1087
+ // Native Console: sync localStorage.debug to current gg-filter
1088
+ if (target.classList.contains('gg-sync-debug-btn')) {
1089
+ const currentFilter = localStorage.getItem(FILTER_KEY) || 'gg:*';
1090
+ localStorage.setItem('debug', currentFilter);
1091
+ renderSettingsUI();
1092
+ }
1093
+ // Native Console: clear localStorage.debug
1094
+ if (target.classList.contains('gg-clear-debug-btn')) {
1095
+ localStorage.removeItem('debug');
1096
+ renderSettingsUI();
1097
+ }
991
1098
  });
992
1099
  }
993
1100
  function wireUpButtons() {
@@ -1127,7 +1234,7 @@ export function createGgPlugin(options, gg) {
1127
1234
  else {
1128
1235
  soloNamespace(namespace);
1129
1236
  }
1130
- localStorage.setItem('debug', filterPattern);
1237
+ localStorage.setItem(FILTER_KEY, filterPattern);
1131
1238
  renderFilterUI();
1132
1239
  renderLogs();
1133
1240
  return;
@@ -1138,7 +1245,7 @@ export function createGgPlugin(options, gg) {
1138
1245
  if (!namespace)
1139
1246
  return;
1140
1247
  soloNamespace(namespace);
1141
- localStorage.setItem('debug', filterPattern);
1248
+ localStorage.setItem(FILTER_KEY, filterPattern);
1142
1249
  renderFilterUI();
1143
1250
  renderLogs();
1144
1251
  return;
@@ -1150,11 +1257,75 @@ export function createGgPlugin(options, gg) {
1150
1257
  filterPattern = 'gg:*';
1151
1258
  enabledNamespaces.clear();
1152
1259
  getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
1153
- localStorage.setItem('debug', filterPattern);
1260
+ localStorage.setItem(FILTER_KEY, filterPattern);
1154
1261
  renderFilterUI();
1155
1262
  renderLogs();
1156
1263
  }
1157
1264
  });
1265
+ // Hover tooltip for expandable objects/arrays.
1266
+ // The tooltip div is re-created after each renderLogs() call
1267
+ // since logContainer.html() destroys children. Event listeners query it dynamically.
1268
+ containerEl.addEventListener('mouseover', (e) => {
1269
+ const target = e.target?.closest?.('.gg-expand');
1270
+ if (!target)
1271
+ return;
1272
+ const entryIdx = target.getAttribute('data-entry');
1273
+ const argIdx = target.getAttribute('data-arg');
1274
+ if (entryIdx === null || argIdx === null)
1275
+ return;
1276
+ const entry = renderedEntries[Number(entryIdx)];
1277
+ if (!entry)
1278
+ return;
1279
+ const arg = entry.args[Number(argIdx)];
1280
+ if (arg === undefined)
1281
+ return;
1282
+ const tip = containerEl.querySelector('.gg-hover-tooltip');
1283
+ if (!tip)
1284
+ return;
1285
+ const srcExpr = target.getAttribute('data-src');
1286
+ // Build tooltip content using DOM API (safe, no HTML injection)
1287
+ tip.textContent = '';
1288
+ if (srcExpr) {
1289
+ const srcDiv = document.createElement('div');
1290
+ srcDiv.className = 'gg-hover-tooltip-src';
1291
+ srcDiv.textContent = srcExpr;
1292
+ tip.appendChild(srcDiv);
1293
+ }
1294
+ const pre = document.createElement('pre');
1295
+ pre.style.margin = '0';
1296
+ pre.textContent = JSON.stringify(arg, null, 2);
1297
+ tip.appendChild(pre);
1298
+ tip.style.display = 'block';
1299
+ // Position below the hovered element using viewport coords (fixed positioning)
1300
+ const targetRect = target.getBoundingClientRect();
1301
+ let left = targetRect.left;
1302
+ let top = targetRect.bottom + 4;
1303
+ // Keep tooltip within viewport
1304
+ const tipRect = tip.getBoundingClientRect();
1305
+ if (left + tipRect.width > window.innerWidth) {
1306
+ left = window.innerWidth - tipRect.width - 8;
1307
+ }
1308
+ if (left < 4)
1309
+ left = 4;
1310
+ // If tooltip would go below viewport, show above instead
1311
+ if (top + tipRect.height > window.innerHeight) {
1312
+ top = targetRect.top - tipRect.height - 4;
1313
+ }
1314
+ tip.style.left = `${left}px`;
1315
+ tip.style.top = `${top}px`;
1316
+ });
1317
+ containerEl.addEventListener('mouseout', (e) => {
1318
+ const target = e.target?.closest?.('.gg-expand');
1319
+ if (!target)
1320
+ return;
1321
+ // Only hide if we're not moving to another child of the same .gg-expand
1322
+ const related = e.relatedTarget;
1323
+ if (related?.closest?.('.gg-expand') === target)
1324
+ return;
1325
+ const tip = containerEl.querySelector('.gg-hover-tooltip');
1326
+ if (tip)
1327
+ tip.style.display = 'none';
1328
+ });
1158
1329
  expanderAttached = true;
1159
1330
  }
1160
1331
  function wireUpResize() {
@@ -1214,6 +1385,7 @@ export function createGgPlugin(options, gg) {
1214
1385
  const allEntries = buffer.getEntries();
1215
1386
  // Apply filtering
1216
1387
  const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
1388
+ renderedEntries = entries;
1217
1389
  const countText = entries.length === allEntries.length
1218
1390
  ? `${entries.length} entries`
1219
1391
  : `${entries.length} / ${allEntries.length} entries`;
@@ -1230,6 +1402,8 @@ export function createGgPlugin(options, gg) {
1230
1402
  // Format each arg individually - objects are expandable
1231
1403
  let argsHTML = '';
1232
1404
  let detailsHTML = '';
1405
+ // Source expression for this entry (used in hover tooltips and expanded details)
1406
+ const srcExpr = entry.src?.trim() && !/^['"`]/.test(entry.src) ? escapeHtml(entry.src) : '';
1233
1407
  if (entry.args.length > 0) {
1234
1408
  argsHTML = entry.args
1235
1409
  .map((arg, argIdx) => {
@@ -1238,9 +1412,14 @@ export function createGgPlugin(options, gg) {
1238
1412
  const preview = Array.isArray(arg) ? `Array(${arg.length})` : 'Object';
1239
1413
  const jsonStr = escapeHtml(JSON.stringify(arg, null, 2));
1240
1414
  const uniqueId = `${index}-${argIdx}`;
1415
+ // Expression header inside expanded details
1416
+ const srcHeader = srcExpr ? `<div class="gg-details-src">${srcExpr}</div>` : '';
1241
1417
  // Store details separately to render after the row
1242
- detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;"><pre style="margin: 0;">${jsonStr}</pre></div>`;
1243
- return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}">${preview}</span>`;
1418
+ detailsHTML += `<div class="gg-details" data-index="${uniqueId}" style="display: none; margin: 4px 0 8px 0; padding: 8px; background: #f8f8f8; border-left: 3px solid ${color}; font-size: 11px; overflow-x: auto;">${srcHeader}<pre style="margin: 0;">${jsonStr}</pre></div>`;
1419
+ // data-entry/data-arg for hover tooltip lookup, data-src for expression context
1420
+ const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
1421
+ const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
1422
+ return `<span style="color: #888; cursor: pointer; text-decoration: underline;" class="gg-expand" data-index="${uniqueId}" data-entry="${index}" data-arg="${argIdx}"${srcAttr}>${srcIcon}${preview}</span>`;
1244
1423
  }
1245
1424
  else {
1246
1425
  // Parse ANSI codes first, then convert URLs to clickable links
@@ -1295,6 +1474,13 @@ export function createGgPlugin(options, gg) {
1295
1474
  })
1296
1475
  .join('')}</div>`;
1297
1476
  logContainer.html(logsHTML);
1477
+ // Append hover tooltip div (destroyed by .html() each render, so re-create)
1478
+ const containerDom = logContainer.get(0);
1479
+ if (containerDom) {
1480
+ const tip = document.createElement('div');
1481
+ tip.className = 'gg-hover-tooltip';
1482
+ containerDom.appendChild(tip);
1483
+ }
1298
1484
  // Re-wire expanders after rendering
1299
1485
  wireUpExpanders();
1300
1486
  // Auto-scroll to bottom
@@ -1326,6 +1512,7 @@ export function createGgPlugin(options, gg) {
1326
1512
  */
1327
1513
  function stripAnsi(text) {
1328
1514
  // Remove all ANSI escape codes
1515
+ // eslint-disable-next-line no-control-regex
1329
1516
  return text.replace(/\x1b\[[0-9;]*m/g, '');
1330
1517
  }
1331
1518
  // Standard ANSI 3/4-bit color palette
@@ -1376,6 +1563,7 @@ export function createGgPlugin(options, gg) {
1376
1563
  */
1377
1564
  function parseAnsiToHtml(text) {
1378
1565
  // ANSI escape sequence regex
1566
+ // eslint-disable-next-line no-control-regex
1379
1567
  const ansiRegex = /\x1b\[([0-9;]+)m/g;
1380
1568
  let html = '';
1381
1569
  let lastIndex = 0;
@@ -12,11 +12,6 @@ export interface GgErudaOptions {
12
12
  * @default 2000
13
13
  */
14
14
  maxEntries?: number;
15
- /**
16
- * Auto-enable localStorage.debug = 'gg:*' if unset
17
- * @default true
18
- */
19
- autoEnable?: boolean;
20
15
  /**
21
16
  * Additional Eruda options passed to eruda.init()
22
17
  * @default {}
@@ -11,6 +11,33 @@ export interface GgCallSitesPluginOptions {
11
11
  */
12
12
  srcRootPattern?: string;
13
13
  }
14
+ /**
15
+ * A range of source code that contains JS expressions, tagged with its context.
16
+ * - 'script': inside a `<script>` block — use object literal `{...}` syntax
17
+ * - 'template': inside a template expression `{...}` or event handler — use `gg._o()` syntax
18
+ */
19
+ export interface CodeRange {
20
+ start: number;
21
+ end: number;
22
+ context: 'script' | 'template';
23
+ }
24
+ /**
25
+ * A function scope range, mapping a byte range to the enclosing function name.
26
+ * Built from the estree AST during `collectCodeRanges()`.
27
+ */
28
+ export interface FunctionScope {
29
+ start: number;
30
+ end: number;
31
+ name: string;
32
+ }
33
+ /**
34
+ * Result of `collectCodeRanges()` — code ranges plus function scope info.
35
+ */
36
+ export interface SvelteCodeInfo {
37
+ ranges: CodeRange[];
38
+ /** Function scopes extracted from the estree AST, sorted by start position. */
39
+ functionScopes: FunctionScope[];
40
+ }
14
41
  /**
15
42
  * Vite plugin that rewrites `gg(...)` and `gg.ns(...)` calls to
16
43
  * `gg._ns({ns, file, line, col}, ...)` at build time. This gives each call
@@ -29,3 +56,60 @@ export interface GgCallSitesPluginOptions {
29
56
  * });
30
57
  */
31
58
  export default function ggCallSitesPlugin(options?: GgCallSitesPluginOptions): Plugin;
59
+ /**
60
+ * Parse JavaScript/TypeScript code using acorn to extract function scopes.
61
+ * Returns function scope ranges for accurate function name detection in .js/.ts files.
62
+ * Uses @sveltejs/acorn-typescript plugin to handle TypeScript syntax.
63
+ *
64
+ * For .svelte files, use `collectCodeRanges()` instead (which uses svelte.parse()).
65
+ */
66
+ export declare function parseJavaScript(code: string): FunctionScope[];
67
+ /**
68
+ * Use `svelte.parse()` to collect all code ranges and function scopes in a .svelte file.
69
+ *
70
+ * Code ranges identify where JS expressions live:
71
+ * - `<script>` blocks (context: 'script')
72
+ * - Template expressions: `{expr}`, `onclick={expr}`, `bind:value={expr}`,
73
+ * `class:name={expr}`, `{#if expr}`, `{#each expr}`, etc. (context: 'template')
74
+ *
75
+ * Function scopes are extracted from the estree AST in script blocks, mapping
76
+ * byte ranges to enclosing function names. This replaces regex-based function
77
+ * detection for .svelte files.
78
+ *
79
+ * Text nodes (prose) are NOT included, so `gg()` in `<p>text gg()</p>` is never transformed.
80
+ */
81
+ export declare function collectCodeRanges(code: string): SvelteCodeInfo;
82
+ /**
83
+ * Find the innermost enclosing function name for a byte position
84
+ * using the pre-built function scope map.
85
+ * Returns empty string if not inside any named function.
86
+ */
87
+ export declare function findEnclosingFunctionFromScopes(pos: number, scopes: FunctionScope[]): string;
88
+ /**
89
+ * Escape a string for embedding as a single-quoted JS string literal.
90
+ */
91
+ export declare function escapeForString(s: string): string;
92
+ /**
93
+ * Transform gg() and gg.ns() calls in source code to gg._ns({ns, file, line, col, src}, ...) calls.
94
+ *
95
+ * Handles:
96
+ * - bare gg(expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
97
+ * - gg.ns('label', expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
98
+ * - label supports template variables: $NS, $FN, $FILE, $LINE, $COL
99
+ * - plain label (no variables) is used as-is (no auto @fn append)
100
+ * - gg.enable, gg.disable, gg.clearPersist, gg._onLog, gg._ns → left untouched
101
+ * - gg inside strings and comments → left untouched
102
+ *
103
+ * For .svelte files, `svelteInfo` (from `collectCodeRanges()`) determines which
104
+ * positions contain JS code and provides AST-based function scope detection.
105
+ * Script ranges use `{...}` object literal syntax; template ranges use `gg._o()`
106
+ * function-call syntax (no braces in Svelte markup). Positions outside any code
107
+ * range (e.g. prose text) are skipped.
108
+ *
109
+ * For .js/.ts files, `jsFunctionScopes` (from `parseJavaScript()`) provides
110
+ * AST-based function scope detection (no regex fallback).
111
+ */
112
+ export declare function transformGgCalls(code: string, shortPath: string, filePath: string, svelteInfo?: SvelteCodeInfo, jsFunctionScopes?: FunctionScope[]): {
113
+ code: string;
114
+ map: null;
115
+ } | null;