@leftium/gg 0.0.39 → 0.0.41

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.
Files changed (2) hide show
  1. package/dist/eruda/plugin.js +741 -178
  2. package/package.json +1 -1
@@ -21,10 +21,16 @@ export function createGgPlugin(options, gg) {
21
21
  const enabledNamespaces = new Set();
22
22
  // Last rendered entries (for hover tooltip arg lookup)
23
23
  let renderedEntries = [];
24
+ // Toast state for "namespace hidden" feedback
25
+ let lastHiddenPattern = null; // filterPattern before the hide (for undo)
26
+ let hasSeenToastExplanation = false; // first toast auto-expands help text
24
27
  // Settings UI state
25
28
  let settingsExpanded = false;
29
+ // Expression visibility toggle
30
+ let showExpressions = false;
26
31
  // Filter pattern persistence key (independent of localStorage.debug)
27
32
  const FILTER_KEY = 'gg-filter';
33
+ const SHOW_EXPRESSIONS_KEY = 'gg-show-expressions';
28
34
  // Namespace click action: 'open' uses Vite dev middleware, 'copy' copies formatted string, 'open-url' navigates to URI
29
35
  const NS_ACTION_KEY = 'gg-ns-action';
30
36
  const EDITOR_BIN_KEY = 'gg-editor-bin';
@@ -107,6 +113,7 @@ export function createGgPlugin(options, gg) {
107
113
  // Load filter state BEFORE registering _onLog hook, because setting _onLog
108
114
  // triggers replay of earlyLogBuffer and each entry checks filterPattern
109
115
  filterPattern = localStorage.getItem(FILTER_KEY) || 'gg:*';
116
+ showExpressions = localStorage.getItem(SHOW_EXPRESSIONS_KEY) === 'true';
110
117
  // Register the capture hook on gg
111
118
  if (gg) {
112
119
  gg._onLog = (entry) => {
@@ -155,6 +162,7 @@ export function createGgPlugin(options, gg) {
155
162
  wireUpResize();
156
163
  wireUpFilterUI();
157
164
  wireUpSettingsUI();
165
+ wireUpToast();
158
166
  renderLogs();
159
167
  },
160
168
  show() {
@@ -205,19 +213,40 @@ export function createGgPlugin(options, gg) {
205
213
  }
206
214
  });
207
215
  }
208
- function soloNamespace(namespace) {
209
- const ns = namespace.trim();
210
- // Toggle: if already soloed on this namespace, restore all
211
- if (filterPattern === ns) {
212
- filterPattern = 'gg:*';
213
- enabledNamespaces.clear();
214
- getAllCapturedNamespaces().forEach((n) => enabledNamespaces.add(n));
215
- return;
216
- }
217
- // Solo: show only this namespace
218
- filterPattern = ns;
216
+ function toggleNamespaces(namespaces, enable) {
217
+ const currentPattern = filterPattern || 'gg:*';
218
+ let parts = currentPattern
219
+ .split(',')
220
+ .map((p) => p.trim())
221
+ .filter(Boolean);
222
+ namespaces.forEach((namespace) => {
223
+ const ns = namespace.trim();
224
+ if (enable) {
225
+ // Remove any exclusion for this namespace
226
+ parts = parts.filter((p) => p !== `-${ns}`);
227
+ }
228
+ else {
229
+ // Add exclusion if not already present
230
+ const exclusion = `-${ns}`;
231
+ if (!parts.includes(exclusion)) {
232
+ parts.push(exclusion);
233
+ }
234
+ }
235
+ });
236
+ filterPattern = parts.join(',');
237
+ // Simplify pattern
238
+ filterPattern = simplifyPattern(filterPattern);
239
+ // Sync enabledNamespaces from the NEW pattern
240
+ const allNamespaces = getAllCapturedNamespaces();
219
241
  enabledNamespaces.clear();
220
- enabledNamespaces.add(ns);
242
+ const effectivePattern = filterPattern || 'gg:*';
243
+ allNamespaces.forEach((ns) => {
244
+ if (namespaceMatchesPattern(ns, effectivePattern)) {
245
+ enabledNamespaces.add(ns);
246
+ }
247
+ });
248
+ // Persist the new pattern
249
+ localStorage.setItem(FILTER_KEY, filterPattern);
221
250
  }
222
251
  function simplifyPattern(pattern) {
223
252
  if (!pattern)
@@ -296,11 +325,7 @@ export function createGgPlugin(options, gg) {
296
325
  }
297
326
  function gridColumns() {
298
327
  const ns = nsColWidth !== null ? `${nsColWidth}px` : 'auto';
299
- // When filter expanded: icons | diff | ns | handle | content
300
- // When collapsed: diff | ns | handle | content
301
- if (filterExpanded) {
302
- return `auto auto ${ns} 4px 1fr`;
303
- }
328
+ // Grid columns: diff | ns | handle | content
304
329
  return `auto ${ns} 4px 1fr`;
305
330
  }
306
331
  function buildHTML() {
@@ -316,67 +341,51 @@ export function createGgPlugin(options, gg) {
316
341
  .gg-log-entry {
317
342
  display: contents;
318
343
  }
319
- .gg-log-header {
320
- display: contents;
321
- }
322
- .gg-log-icons,
323
- .gg-log-diff,
324
- .gg-log-ns,
325
- .gg-log-handle,
326
- .gg-log-content {
327
- min-width: 0;
328
- align-self: start !important;
329
- border-top: 1px solid rgba(0,0,0,0.05);
330
- }
331
- .gg-log-icons {
332
- display: flex;
333
- gap: 2px;
334
- padding: 0 4px 0 0;
335
- white-space: nowrap;
336
- align-self: stretch !important;
337
- }
338
- .gg-log-icons button {
339
- all: unset;
340
- cursor: pointer;
341
- opacity: 0.35;
342
- padding: 4px 10px;
343
- line-height: 1;
344
- display: flex;
345
- align-items: center;
346
- }
347
- .gg-log-icons button:hover {
348
- opacity: 1;
349
- background: rgba(0,0,0,0.05);
350
- }
351
- .gg-solo-target {
352
- cursor: pointer;
353
- }
354
- .gg-solo-target:hover {
355
- background: rgba(0,0,0,0.05);
356
- }
357
- /* Clickable namespace cells with file metadata (open-in-editor) */
358
- .gg-log-ns[data-file] {
359
- cursor: pointer;
360
- text-decoration: underline;
361
- text-decoration-style: dotted;
362
- text-underline-offset: 3px;
363
- }
364
- .gg-log-ns[data-file]:hover {
365
- text-decoration-style: solid;
366
- background: rgba(0,0,0,0.05);
367
- }
368
- .gg-log-ns[data-file]::after {
369
- content: ' \u{1F4CB}';
370
- font-size: 10px;
371
- opacity: 0;
372
- transition: opacity 0.1s;
373
- }
374
- .gg-action-open .gg-log-ns[data-file]::after {
375
- content: ' \u{1F517}';
376
- }
377
- .gg-log-ns[data-file]:hover::after {
378
- opacity: 1;
379
- }
344
+ .gg-log-header {
345
+ display: contents;
346
+ }
347
+ .gg-log-diff,
348
+ .gg-log-ns,
349
+ .gg-log-handle,
350
+ .gg-log-content {
351
+ min-width: 0;
352
+ align-self: start !important;
353
+ border-top: 1px solid rgba(0,0,0,0.05);
354
+ }
355
+ .gg-reset-filter-btn:hover {
356
+ background: #1976D2 !important;
357
+ transform: translateY(-1px);
358
+ box-shadow: 0 2px 8px rgba(33, 150, 243, 0.4);
359
+ }
360
+ .gg-reset-filter-btn:active {
361
+ transform: translateY(0);
362
+ }
363
+ /* Clickable time diff with file metadata (open-in-editor) */
364
+ .gg-log-diff[data-file] {
365
+ cursor: pointer;
366
+ text-decoration: underline;
367
+ text-decoration-style: dotted;
368
+ text-underline-offset: 2px;
369
+ opacity: 0.85;
370
+ }
371
+ .gg-log-diff[data-file]:hover {
372
+ text-decoration-style: solid;
373
+ opacity: 1;
374
+ background: rgba(0,0,0,0.05);
375
+ }
376
+ /* Clickable namespace segments - always enabled for filtering */
377
+ .gg-ns-segment {
378
+ cursor: pointer;
379
+ padding: 1px 2px;
380
+ border-radius: 2px;
381
+ transition: background 0.1s;
382
+ }
383
+ .gg-ns-segment:hover {
384
+ background: rgba(0,0,0,0.1);
385
+ text-decoration: underline;
386
+ text-decoration-style: solid;
387
+ text-underline-offset: 2px;
388
+ }
380
389
  .gg-details {
381
390
  grid-column: 1 / -1;
382
391
  border-top: none;
@@ -390,13 +399,138 @@ export function createGgPlugin(options, gg) {
390
399
  padding: 4px 8px 4px 0;
391
400
  white-space: pre;
392
401
  }
393
- .gg-log-ns {
394
- font-weight: bold;
395
- white-space: nowrap;
396
- overflow: hidden;
397
- text-overflow: ellipsis;
398
- padding: 4px 8px 4px 0;
399
- }
402
+ .gg-log-ns {
403
+ font-weight: bold;
404
+ white-space: nowrap;
405
+ overflow: hidden;
406
+ padding: 4px 8px 4px 0;
407
+ display: flex;
408
+ align-items: center;
409
+ gap: 6px;
410
+ }
411
+ .gg-ns-text {
412
+ overflow: hidden;
413
+ text-overflow: ellipsis;
414
+ min-width: 0;
415
+ }
416
+ .gg-ns-hide {
417
+ all: unset;
418
+ cursor: pointer;
419
+ opacity: 0;
420
+ font-size: 14px;
421
+ font-weight: bold;
422
+ line-height: 1;
423
+ padding: 1px 4px;
424
+ transition: opacity 0.15s;
425
+ flex-shrink: 0;
426
+ }
427
+ .gg-log-ns:hover .gg-ns-hide {
428
+ opacity: 0.4;
429
+ }
430
+ .gg-ns-hide:hover {
431
+ opacity: 1 !important;
432
+ background: rgba(0,0,0,0.08);
433
+ border-radius: 3px;
434
+ }
435
+ /* Toast bar for "namespace hidden" feedback */
436
+ .gg-toast {
437
+ display: none;
438
+ background: #333;
439
+ color: #e0e0e0;
440
+ font-size: 12px;
441
+ font-family: monospace;
442
+ padding: 8px 12px;
443
+ border-radius: 6px 6px 0 0;
444
+ flex-shrink: 0;
445
+ align-items: center;
446
+ gap: 8px;
447
+ margin-top: 4px;
448
+ animation: gg-toast-slide-up 0.2s ease-out;
449
+ }
450
+ .gg-toast.visible {
451
+ display: flex;
452
+ flex-wrap: wrap;
453
+ }
454
+ @keyframes gg-toast-slide-up {
455
+ from { transform: translateY(100%); opacity: 0; }
456
+ to { transform: translateY(0); opacity: 1; }
457
+ }
458
+ .gg-toast-label {
459
+ opacity: 0.7;
460
+ flex-shrink: 0;
461
+ }
462
+ .gg-toast-ns {
463
+ display: inline-flex;
464
+ align-items: center;
465
+ gap: 0;
466
+ }
467
+ .gg-toast-segment {
468
+ cursor: pointer;
469
+ padding: 1px 3px;
470
+ border-radius: 2px;
471
+ color: #bbb;
472
+ text-decoration: line-through;
473
+ transition: background 0.1s, color 0.1s;
474
+ }
475
+ .gg-toast-segment:hover {
476
+ color: #ef5350;
477
+ background: rgba(239, 83, 80, 0.15);
478
+ }
479
+ .gg-toast-delim {
480
+ opacity: 0.5;
481
+ }
482
+ .gg-toast-actions {
483
+ display: flex;
484
+ align-items: center;
485
+ gap: 6px;
486
+ margin-left: auto;
487
+ flex-shrink: 0;
488
+ }
489
+ .gg-toast-btn {
490
+ all: unset;
491
+ cursor: pointer;
492
+ padding: 2px 8px;
493
+ border-radius: 3px;
494
+ font-size: 11px;
495
+ transition: background 0.1s;
496
+ }
497
+ .gg-toast-undo {
498
+ color: #64b5f6;
499
+ font-weight: bold;
500
+ }
501
+ .gg-toast-undo:hover {
502
+ background: rgba(100, 181, 246, 0.2);
503
+ }
504
+ .gg-toast-help {
505
+ color: #999;
506
+ font-size: 13px;
507
+ line-height: 1;
508
+ }
509
+ .gg-toast-help:hover {
510
+ color: #ccc;
511
+ background: rgba(255,255,255,0.1);
512
+ }
513
+ .gg-toast-dismiss {
514
+ color: #999;
515
+ font-size: 14px;
516
+ line-height: 1;
517
+ }
518
+ .gg-toast-dismiss:hover {
519
+ color: #fff;
520
+ background: rgba(255,255,255,0.1);
521
+ }
522
+ .gg-toast-explanation {
523
+ display: none;
524
+ width: 100%;
525
+ font-size: 11px;
526
+ opacity: 0.6;
527
+ padding-top: 4px;
528
+ margin-top: 4px;
529
+ border-top: 1px solid rgba(255,255,255,0.1);
530
+ }
531
+ .gg-toast-explanation.visible {
532
+ display: block;
533
+ }
400
534
  .gg-log-handle {
401
535
  width: 4px;
402
536
  cursor: col-resize;
@@ -416,14 +550,14 @@ export function createGgPlugin(options, gg) {
416
550
  .gg-log-handle.gg-dragging {
417
551
  background: rgba(0,0,0,0.15);
418
552
  }
419
- .gg-log-content {
420
- word-break: break-word;
421
- padding: 4px 0;
422
- position: relative;
423
- -webkit-user-select: text !important;
424
- user-select: text !important;
425
- cursor: text;
426
- }
553
+ .gg-log-content {
554
+ word-break: break-word;
555
+ padding: 4px 0;
556
+ position: relative;
557
+ -webkit-user-select: text !important;
558
+ user-select: text !important;
559
+ cursor: text;
560
+ }
427
561
  .gg-log-content * {
428
562
  -webkit-user-select: text !important;
429
563
  user-select: text !important;
@@ -474,6 +608,20 @@ export function createGgPlugin(options, gg) {
474
608
  .gg-log-content[data-src]:not(:has(.gg-expand)):hover::after {
475
609
  opacity: 1;
476
610
  }
611
+ /* Inline expression label (shown when expression toggle is on) */
612
+ .gg-inline-expr {
613
+ color: #888;
614
+ font-style: italic;
615
+ font-size: 11px;
616
+ }
617
+ /* When expressions are shown inline, suppress the CSS tooltip and magnifying glass on primitives */
618
+ .gg-show-expr .gg-log-content[data-src] {
619
+ cursor: text;
620
+ }
621
+ .gg-show-expr .gg-log-content[data-src]:not(:has(.gg-expand))::before,
622
+ .gg-show-expr .gg-log-content[data-src]:not(:has(.gg-expand))::after {
623
+ display: none;
624
+ }
477
625
  /* Expression icon inline with expandable object labels */
478
626
  .gg-src-icon {
479
627
  font-size: 10px;
@@ -727,18 +875,17 @@ export function createGgPlugin(options, gg) {
727
875
  display: block;
728
876
  padding: 8px 0;
729
877
  }
730
- /* Remove double borders on mobile - only border on entry wrapper */
731
- .gg-log-entry:not(:first-child) {
732
- border-top: 1px solid rgba(0,0,0,0.05);
733
- }
734
- .gg-log-icons,
735
- .gg-log-diff,
736
- .gg-log-ns,
737
- .gg-log-handle,
738
- .gg-log-content,
739
- .gg-details {
740
- border-top: none !important;
878
+ /* Remove double borders on mobile - only border on entry wrapper */
879
+ .gg-log-entry:not(:first-child) {
880
+ border-top: 1px solid rgba(0,0,0,0.05);
741
881
  }
882
+ .gg-log-diff,
883
+ .gg-log-ns,
884
+ .gg-log-handle,
885
+ .gg-log-content,
886
+ .gg-details {
887
+ border-top: none !important;
888
+ }
742
889
  .gg-log-header {
743
890
  display: flex;
744
891
  align-items: center;
@@ -775,7 +922,7 @@ export function createGgPlugin(options, gg) {
775
922
  <div class="eruda-gg${nsClickAction === 'open' || nsClickAction === 'open-url' ? ' gg-action-open' : ''}" style="padding: 10px; height: 100%; display: flex; flex-direction: column; font-size: 14px; touch-action: none; overscroll-behavior: contain;">
776
923
  <div class="gg-toolbar">
777
924
  <button class="gg-copy-btn">
778
- <span class="gg-btn-text">Copy</span>
925
+ <span class="gg-btn-text">📋 <span class="gg-copy-count">Copy 0 entries</span></span>
779
926
  <span class="gg-btn-icon" title="Copy">📋</span>
780
927
  </button>
781
928
  <button class="gg-filter-btn" style="text-align: left; white-space: nowrap;">
@@ -783,11 +930,15 @@ export function createGgPlugin(options, gg) {
783
930
  <span class="gg-btn-icon">NS: </span>
784
931
  <span class="gg-filter-summary"></span>
785
932
  </button>
933
+ <button class="gg-expressions-btn" style="background: ${showExpressions ? '#e8f5e9' : 'transparent'};" title="Toggle expression visibility in logs and clipboard">
934
+ <span class="gg-btn-text">\uD83D\uDD0D Expr</span>
935
+ <span class="gg-btn-icon" title="Expressions">\uD83D\uDD0D</span>
936
+ </button>
937
+ <span style="flex: 1;"></span>
786
938
  <button class="gg-settings-btn">
787
939
  <span class="gg-btn-text">⚙️ Settings</span>
788
940
  <span class="gg-btn-icon" title="Settings">⚙️</span>
789
941
  </button>
790
- <span class="gg-count" style="opacity: 0.6; white-space: nowrap; flex: 1; text-align: right;"></span>
791
942
  <button class="gg-clear-btn">
792
943
  <span class="gg-btn-text">Clear</span>
793
944
  <span class="gg-btn-icon" title="Clear">⊘</span>
@@ -795,7 +946,8 @@ export function createGgPlugin(options, gg) {
795
946
  </div>
796
947
  <div class="gg-filter-panel"></div>
797
948
  <div class="gg-settings-panel"></div>
798
- <div class="gg-log-container" style="flex: 1; overflow-y: auto; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
949
+ <div class="gg-log-container" style="flex: 1; overflow-y: auto; overflow-x: hidden; font-family: monospace; font-size: 12px; touch-action: pan-y; overscroll-behavior: contain;"></div>
950
+ <div class="gg-toast"></div>
799
951
  <iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
800
952
  </div>
801
953
  `;
@@ -870,6 +1022,19 @@ export function createGgPlugin(options, gg) {
870
1022
  renderLogs();
871
1023
  return;
872
1024
  }
1025
+ // Handle "other" checkbox
1026
+ if (target.classList.contains('gg-other-checkbox')) {
1027
+ const otherNamespacesJson = target.getAttribute('data-other-namespaces');
1028
+ if (!otherNamespacesJson)
1029
+ return;
1030
+ const otherNamespaces = JSON.parse(otherNamespacesJson);
1031
+ // Toggle all "other" namespaces at once
1032
+ toggleNamespaces(otherNamespaces, target.checked);
1033
+ // localStorage already saved in toggleNamespaces()
1034
+ renderFilterUI();
1035
+ renderLogs();
1036
+ return;
1037
+ }
873
1038
  // Handle individual namespace checkboxes
874
1039
  if (target.classList.contains('gg-ns-checkbox')) {
875
1040
  const namespace = target.getAttribute('data-namespace');
@@ -908,26 +1073,52 @@ export function createGgPlugin(options, gg) {
908
1073
  let checkboxesHTML = '';
909
1074
  if (simple && allNamespaces.length > 0) {
910
1075
  const allChecked = enabledCount === totalCount;
1076
+ // Count frequency of each namespace in the buffer
1077
+ const allEntries = buffer.getEntries();
1078
+ const nsCounts = new Map();
1079
+ allEntries.forEach((entry) => {
1080
+ nsCounts.set(entry.namespace, (nsCounts.get(entry.namespace) || 0) + 1);
1081
+ });
1082
+ // Sort ALL namespaces by frequency (most common first)
1083
+ const sortedAllNamespaces = [...allNamespaces].sort((a, b) => (nsCounts.get(b) || 0) - (nsCounts.get(a) || 0));
1084
+ // Take top 5 most common (regardless of enabled state)
1085
+ const displayedNamespaces = sortedAllNamespaces.slice(0, 5);
1086
+ // Calculate "other" namespaces (not in top 5)
1087
+ const displayedSet = new Set(displayedNamespaces);
1088
+ const otherNamespaces = allNamespaces.filter((ns) => !displayedSet.has(ns));
1089
+ const otherEnabledCount = otherNamespaces.filter((ns) => enabledNamespaces.has(ns)).length;
1090
+ const otherTotalCount = otherNamespaces.length;
1091
+ const otherChecked = otherEnabledCount > 0;
1092
+ const otherCount = otherNamespaces.reduce((sum, ns) => sum + (nsCounts.get(ns) || 0), 0);
911
1093
  checkboxesHTML = `
912
- <div class="gg-filter-checkboxes">
913
- <label class="gg-filter-checkbox" style="font-weight: bold;">
914
- <input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}>
915
- <span>ALL</span>
916
- </label>
917
- ${allNamespaces
1094
+ <div class="gg-filter-checkboxes">
1095
+ <label class="gg-filter-checkbox" style="font-weight: bold;">
1096
+ <input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}>
1097
+ <span>ALL</span>
1098
+ </label>
1099
+ ${displayedNamespaces
918
1100
  .map((ns) => {
919
1101
  // Check if namespace matches the current pattern
920
1102
  const checked = namespaceMatchesPattern(ns, effectivePattern);
1103
+ const count = nsCounts.get(ns) || 0;
921
1104
  return `
922
- <label class="gg-filter-checkbox">
923
- <input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
924
- <span>${escapeHtml(ns)}</span>
925
- </label>
926
- `;
1105
+ <label class="gg-filter-checkbox">
1106
+ <input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
1107
+ <span>${escapeHtml(ns)} (${count})</span>
1108
+ </label>
1109
+ `;
927
1110
  })
928
1111
  .join('')}
929
- </div>
930
- `;
1112
+ ${otherTotalCount > 0
1113
+ ? `
1114
+ <label class="gg-filter-checkbox" style="opacity: 0.7;">
1115
+ <input type="checkbox" class="gg-other-checkbox" ${otherChecked ? 'checked' : ''} data-other-namespaces='${JSON.stringify(otherNamespaces)}'>
1116
+ <span>other (${otherCount})</span>
1117
+ </label>
1118
+ `
1119
+ : ''}
1120
+ </div>
1121
+ `;
931
1122
  }
932
1123
  else if (!simple) {
933
1124
  checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
@@ -1169,6 +1360,9 @@ export function createGgPlugin(options, gg) {
1169
1360
  const time = new Date(e.timestamp).toISOString().slice(11, 19);
1170
1361
  // Trim namespace and strip 'gg:' prefix to save tokens
1171
1362
  const ns = e.namespace.trim().replace(/^gg:/, '');
1363
+ // Include expression suffix when toggle is enabled
1364
+ const hasSrcExpr = !e.level && e.src?.trim() && !/^['"`]/.test(e.src);
1365
+ const exprSuffix = showExpressions && hasSrcExpr ? ` \u2039${e.src}\u203A` : '';
1172
1366
  // Format args: compact JSON for objects, primitives as-is
1173
1367
  const argsStr = e.args
1174
1368
  .map((arg) => {
@@ -1179,7 +1373,7 @@ export function createGgPlugin(options, gg) {
1179
1373
  return stripAnsi(String(arg));
1180
1374
  })
1181
1375
  .join(' ');
1182
- return `${time} ${ns} ${argsStr}`;
1376
+ return `${time} ${ns} ${argsStr}${exprSuffix}`;
1183
1377
  })
1184
1378
  .join('\n');
1185
1379
  try {
@@ -1195,6 +1389,15 @@ export function createGgPlugin(options, gg) {
1195
1389
  document.body.removeChild(textarea);
1196
1390
  }
1197
1391
  });
1392
+ $el.find('.gg-expressions-btn').on('click', () => {
1393
+ showExpressions = !showExpressions;
1394
+ localStorage.setItem(SHOW_EXPRESSIONS_KEY, String(showExpressions));
1395
+ // Update button styling inline (toolbar is not re-rendered by renderLogs)
1396
+ const btn = $el.find('.gg-expressions-btn').get(0);
1397
+ if (btn)
1398
+ btn.style.background = showExpressions ? '#e8f5e9' : 'transparent';
1399
+ renderLogs();
1400
+ });
1198
1401
  }
1199
1402
  /** Substitute format variables ($ROOT, $FILE, $LINE, $COL) in a format string */
1200
1403
  function formatString(format, file, line, col) {
@@ -1248,6 +1451,161 @@ export function createGgPlugin(options, gg) {
1248
1451
  });
1249
1452
  }
1250
1453
  }
1454
+ /** Show toast bar after hiding a namespace via the x button */
1455
+ function showHideToast(namespace, previousPattern) {
1456
+ if (!$el)
1457
+ return;
1458
+ lastHiddenPattern = previousPattern;
1459
+ const toast = $el.find('.gg-toast').get(0);
1460
+ if (!toast)
1461
+ return;
1462
+ // Split namespace into segments with delimiters (same logic as log row rendering)
1463
+ const parts = namespace.split(/([:/@ \-_])/);
1464
+ const segments = [];
1465
+ const delimiters = [];
1466
+ for (let i = 0; i < parts.length; i++) {
1467
+ if (i % 2 === 0) {
1468
+ if (parts[i])
1469
+ segments.push(parts[i]);
1470
+ }
1471
+ else {
1472
+ delimiters.push(parts[i]);
1473
+ }
1474
+ }
1475
+ // Build clickable segment HTML
1476
+ let nsHTML = '';
1477
+ for (let i = 0; i < segments.length; i++) {
1478
+ const segment = escapeHtml(segments[i]);
1479
+ // Build filter pattern for this segment level
1480
+ let segFilter = '';
1481
+ for (let j = 0; j <= i; j++) {
1482
+ segFilter += segments[j];
1483
+ if (j < i) {
1484
+ segFilter += delimiters[j];
1485
+ }
1486
+ else if (j < segments.length - 1) {
1487
+ segFilter += delimiters[j] + '*';
1488
+ }
1489
+ }
1490
+ nsHTML += `<span class="gg-toast-segment" data-filter="${escapeHtml(segFilter)}">${segment}</span>`;
1491
+ if (i < segments.length - 1) {
1492
+ nsHTML += `<span class="gg-toast-delim">${escapeHtml(delimiters[i])}</span>`;
1493
+ }
1494
+ }
1495
+ // Auto-expand explanation on first use
1496
+ const showExplanation = !hasSeenToastExplanation;
1497
+ toast.innerHTML =
1498
+ `<button class="gg-toast-btn gg-toast-dismiss" title="Dismiss">\u00d7</button>` +
1499
+ `<span class="gg-toast-label">Hidden:</span>` +
1500
+ `<span class="gg-toast-ns">${nsHTML}</span>` +
1501
+ `<span class="gg-toast-actions">` +
1502
+ `<button class="gg-toast-btn gg-toast-undo">Undo</button>` +
1503
+ `<button class="gg-toast-btn gg-toast-help" title="Toggle help">?</button>` +
1504
+ `</span>` +
1505
+ `<div class="gg-toast-explanation${showExplanation ? ' visible' : ''}">` +
1506
+ `Click a segment above to hide all matching namespaces (e.g. click "api" to hide gg:api:*). ` +
1507
+ `Tip: you can also right-click any segment in the log to hide it directly.` +
1508
+ `</div>`;
1509
+ toast.classList.add('visible');
1510
+ if (showExplanation) {
1511
+ hasSeenToastExplanation = true;
1512
+ }
1513
+ }
1514
+ /** Dismiss the toast bar */
1515
+ function dismissToast() {
1516
+ if (!$el)
1517
+ return;
1518
+ const toast = $el.find('.gg-toast').get(0);
1519
+ if (toast) {
1520
+ toast.classList.remove('visible');
1521
+ }
1522
+ lastHiddenPattern = null;
1523
+ }
1524
+ /** Undo the last namespace hide */
1525
+ function undoHide() {
1526
+ if (!$el || lastHiddenPattern === null)
1527
+ return;
1528
+ // Restore the previous filter pattern
1529
+ filterPattern = lastHiddenPattern;
1530
+ localStorage.setItem(FILTER_KEY, filterPattern);
1531
+ // Sync enabledNamespaces from the restored pattern
1532
+ enabledNamespaces.clear();
1533
+ const effectivePattern = filterPattern || 'gg:*';
1534
+ getAllCapturedNamespaces().forEach((ns) => {
1535
+ if (namespaceMatchesPattern(ns, effectivePattern)) {
1536
+ enabledNamespaces.add(ns);
1537
+ }
1538
+ });
1539
+ dismissToast();
1540
+ renderFilterUI();
1541
+ renderLogs();
1542
+ }
1543
+ /** Wire up toast event handlers (called once after init) */
1544
+ function wireUpToast() {
1545
+ if (!$el)
1546
+ return;
1547
+ const toast = $el.find('.gg-toast').get(0);
1548
+ if (!toast)
1549
+ return;
1550
+ toast.addEventListener('click', (e) => {
1551
+ const target = e.target;
1552
+ // Undo button
1553
+ if (target.classList?.contains('gg-toast-undo')) {
1554
+ undoHide();
1555
+ return;
1556
+ }
1557
+ // Dismiss button
1558
+ if (target.classList?.contains('gg-toast-dismiss')) {
1559
+ dismissToast();
1560
+ return;
1561
+ }
1562
+ // Help toggle
1563
+ if (target.classList?.contains('gg-toast-help')) {
1564
+ const explanation = toast.querySelector('.gg-toast-explanation');
1565
+ if (explanation) {
1566
+ explanation.classList.toggle('visible');
1567
+ }
1568
+ return;
1569
+ }
1570
+ // Segment click: add exclusion for that pattern
1571
+ if (target.classList?.contains('gg-toast-segment')) {
1572
+ const filter = target.getAttribute('data-filter');
1573
+ if (!filter)
1574
+ return;
1575
+ // Add exclusion pattern (same logic as right-click segment)
1576
+ const currentPattern = filterPattern || 'gg:*';
1577
+ const exclusion = `-${filter}`;
1578
+ const parts = currentPattern.split(',').map((p) => p.trim());
1579
+ if (parts.includes(exclusion)) {
1580
+ // Already excluded, toggle off
1581
+ filterPattern = parts.filter((p) => p !== exclusion).join(',') || 'gg:*';
1582
+ }
1583
+ else {
1584
+ const hasInclusion = parts.some((p) => !p.startsWith('-'));
1585
+ if (hasInclusion) {
1586
+ filterPattern = `${currentPattern},${exclusion}`;
1587
+ }
1588
+ else {
1589
+ filterPattern = `gg:*,${exclusion}`;
1590
+ }
1591
+ }
1592
+ filterPattern = simplifyPattern(filterPattern);
1593
+ // Sync enabledNamespaces
1594
+ enabledNamespaces.clear();
1595
+ const effectivePattern = filterPattern || 'gg:*';
1596
+ getAllCapturedNamespaces().forEach((ns) => {
1597
+ if (namespaceMatchesPattern(ns, effectivePattern)) {
1598
+ enabledNamespaces.add(ns);
1599
+ }
1600
+ });
1601
+ localStorage.setItem(FILTER_KEY, filterPattern);
1602
+ dismissToast();
1603
+ renderFilterUI();
1604
+ renderLogs();
1605
+ return;
1606
+ }
1607
+ });
1608
+ }
1251
1609
  function wireUpExpanders() {
1252
1610
  if (!$el || expanderAttached)
1253
1611
  return;
@@ -1258,6 +1616,16 @@ export function createGgPlugin(options, gg) {
1258
1616
  return;
1259
1617
  containerEl.addEventListener('click', (e) => {
1260
1618
  const target = e.target;
1619
+ // Handle reset filter button (shown when all logs filtered out)
1620
+ if (target?.classList?.contains('gg-reset-filter-btn')) {
1621
+ filterPattern = 'gg:*';
1622
+ enabledNamespaces.clear();
1623
+ getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
1624
+ localStorage.setItem(FILTER_KEY, filterPattern);
1625
+ renderFilterUI();
1626
+ renderLogs();
1627
+ return;
1628
+ }
1261
1629
  // Handle expand/collapse
1262
1630
  if (target?.classList?.contains('gg-expand')) {
1263
1631
  const index = target.getAttribute('data-index');
@@ -1282,40 +1650,47 @@ export function createGgPlugin(options, gg) {
1282
1650
  }
1283
1651
  return;
1284
1652
  }
1285
- // Handle clicking namespace to open in editor (when filter collapsed)
1286
- if (target?.classList?.contains('gg-log-ns') &&
1287
- target.hasAttribute('data-file') &&
1288
- !target.classList.contains('gg-solo-target')) {
1289
- handleNamespaceClick(target);
1290
- return;
1291
- }
1292
- // Handle filter icon clicks (hide / solo)
1293
- if (target?.classList?.contains('gg-icon-hide') ||
1294
- target?.classList?.contains('gg-icon-solo')) {
1295
- const iconsDiv = target.closest('.gg-log-icons');
1296
- const namespace = iconsDiv?.getAttribute('data-namespace');
1297
- if (!namespace)
1653
+ // Handle clicking namespace segments - always filter
1654
+ if (target?.classList?.contains('gg-ns-segment')) {
1655
+ const filter = target.getAttribute('data-filter');
1656
+ if (!filter)
1298
1657
  return;
1299
- if (target.classList.contains('gg-icon-hide')) {
1300
- toggleNamespace(namespace, false);
1658
+ // Toggle behavior: if already at this filter, restore all
1659
+ if (filterPattern === filter) {
1660
+ filterPattern = 'gg:*';
1661
+ enabledNamespaces.clear();
1662
+ getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
1301
1663
  }
1302
1664
  else {
1303
- soloNamespace(namespace);
1665
+ filterPattern = filter;
1666
+ enabledNamespaces.clear();
1667
+ getAllCapturedNamespaces()
1668
+ .filter((ns) => namespaceMatchesPattern(ns, filter))
1669
+ .forEach((ns) => enabledNamespaces.add(ns));
1304
1670
  }
1305
1671
  localStorage.setItem(FILTER_KEY, filterPattern);
1306
1672
  renderFilterUI();
1307
1673
  renderLogs();
1308
1674
  return;
1309
1675
  }
1310
- // Handle clicking diff/ns cells to solo (same as 🎯)
1311
- if (target?.classList?.contains('gg-solo-target')) {
1676
+ // Handle clicking time diff to open in editor
1677
+ if (target?.classList?.contains('gg-log-diff') && target.hasAttribute('data-file')) {
1678
+ handleNamespaceClick(target);
1679
+ return;
1680
+ }
1681
+ // Handle clicking hide button for namespace
1682
+ if (target?.classList?.contains('gg-ns-hide')) {
1312
1683
  const namespace = target.getAttribute('data-namespace');
1313
1684
  if (!namespace)
1314
1685
  return;
1315
- soloNamespace(namespace);
1686
+ // Save current pattern for undo before hiding
1687
+ const previousPattern = filterPattern;
1688
+ toggleNamespace(namespace, false);
1316
1689
  localStorage.setItem(FILTER_KEY, filterPattern);
1317
1690
  renderFilterUI();
1318
1691
  renderLogs();
1692
+ // Show toast with undo option
1693
+ showHideToast(namespace, previousPattern);
1319
1694
  return;
1320
1695
  }
1321
1696
  // Clicking background (container or grid, not a log element) restores all
@@ -1330,6 +1705,116 @@ export function createGgPlugin(options, gg) {
1330
1705
  renderLogs();
1331
1706
  }
1332
1707
  });
1708
+ // Helper: show confirmation tooltip near target element
1709
+ function showConfirmationTooltip(containerEl, target, text) {
1710
+ const tip = containerEl.querySelector('.gg-hover-tooltip');
1711
+ if (!tip)
1712
+ return;
1713
+ tip.textContent = text;
1714
+ tip.style.display = 'block';
1715
+ const targetRect = target.getBoundingClientRect();
1716
+ let left = targetRect.left;
1717
+ let top = targetRect.bottom + 4;
1718
+ const tipRect = tip.getBoundingClientRect();
1719
+ if (left + tipRect.width > window.innerWidth) {
1720
+ left = window.innerWidth - tipRect.width - 8;
1721
+ }
1722
+ if (left < 4)
1723
+ left = 4;
1724
+ if (top + tipRect.height > window.innerHeight) {
1725
+ top = targetRect.top - tipRect.height - 4;
1726
+ }
1727
+ tip.style.left = `${left}px`;
1728
+ tip.style.top = `${top}px`;
1729
+ setTimeout(() => {
1730
+ tip.style.display = 'none';
1731
+ }, 1500);
1732
+ }
1733
+ // Right-click context actions
1734
+ containerEl.addEventListener('contextmenu', (e) => {
1735
+ const target = e.target;
1736
+ // Right-click namespace segment: hide that pattern
1737
+ if (target?.classList?.contains('gg-ns-segment')) {
1738
+ const filter = target.getAttribute('data-filter');
1739
+ if (!filter)
1740
+ return;
1741
+ e.preventDefault();
1742
+ // Add exclusion pattern: keep current base, add -<pattern>
1743
+ const currentPattern = filterPattern || 'gg:*';
1744
+ const exclusion = `-${filter}`;
1745
+ // Check if already excluded (toggle off)
1746
+ const parts = currentPattern.split(',').map((p) => p.trim());
1747
+ if (parts.includes(exclusion)) {
1748
+ // Remove the exclusion to un-hide
1749
+ filterPattern = parts.filter((p) => p !== exclusion).join(',') || 'gg:*';
1750
+ }
1751
+ else {
1752
+ // Ensure we have a base inclusion pattern
1753
+ const hasInclusion = parts.some((p) => !p.startsWith('-'));
1754
+ if (hasInclusion) {
1755
+ filterPattern = `${currentPattern},${exclusion}`;
1756
+ }
1757
+ else {
1758
+ filterPattern = `gg:*,${exclusion}`;
1759
+ }
1760
+ }
1761
+ filterPattern = simplifyPattern(filterPattern);
1762
+ // Sync enabledNamespaces from the new pattern
1763
+ enabledNamespaces.clear();
1764
+ const effectivePattern = filterPattern || 'gg:*';
1765
+ getAllCapturedNamespaces().forEach((ns) => {
1766
+ if (namespaceMatchesPattern(ns, effectivePattern)) {
1767
+ enabledNamespaces.add(ns);
1768
+ }
1769
+ });
1770
+ localStorage.setItem(FILTER_KEY, filterPattern);
1771
+ renderFilterUI();
1772
+ renderLogs();
1773
+ return;
1774
+ }
1775
+ // Right-click time diff: copy file location to clipboard
1776
+ if (target?.classList?.contains('gg-log-diff') && target.hasAttribute('data-file')) {
1777
+ e.preventDefault();
1778
+ const file = target.getAttribute('data-file') || '';
1779
+ const line = target.getAttribute('data-line');
1780
+ const col = target.getAttribute('data-col');
1781
+ const formatted = formatString(activeFormat(), file, line, col);
1782
+ navigator.clipboard.writeText(formatted).then(() => {
1783
+ showConfirmationTooltip(containerEl, target, `Copied: ${formatted}`);
1784
+ });
1785
+ return;
1786
+ }
1787
+ // Right-click message area: copy that single message
1788
+ const contentEl = target?.closest?.('.gg-log-content');
1789
+ if (contentEl) {
1790
+ const entryEl = contentEl.closest('.gg-log-entry');
1791
+ const entryIdx = entryEl?.getAttribute('data-entry');
1792
+ if (entryIdx === null || entryIdx === undefined)
1793
+ return;
1794
+ const entry = renderedEntries[Number(entryIdx)];
1795
+ if (!entry)
1796
+ return;
1797
+ e.preventDefault();
1798
+ const time = new Date(entry.timestamp).toISOString().slice(11, 19);
1799
+ const ns = entry.namespace.trim().replace(/^gg:/, '');
1800
+ // Include expression suffix when toggle is enabled
1801
+ const hasSrcExpr = !entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src);
1802
+ const exprSuffix = showExpressions && hasSrcExpr ? ` \u2039${entry.src}\u203A` : '';
1803
+ const argsStr = entry.args
1804
+ .map((arg) => {
1805
+ if (typeof arg === 'object' && arg !== null) {
1806
+ return JSON.stringify(arg);
1807
+ }
1808
+ return stripAnsi(String(arg));
1809
+ })
1810
+ .join(' ');
1811
+ const text = `${time} ${ns} ${argsStr}${exprSuffix}`;
1812
+ navigator.clipboard.writeText(text).then(() => {
1813
+ showConfirmationTooltip(containerEl, contentEl, 'Copied message');
1814
+ });
1815
+ return;
1816
+ }
1817
+ });
1333
1818
  // Hover tooltip for expandable objects/arrays.
1334
1819
  // The tooltip div is re-created after each renderLogs() call
1335
1820
  // since logContainer.html() destroys children. Event listeners query it dynamically.
@@ -1394,6 +1879,57 @@ export function createGgPlugin(options, gg) {
1394
1879
  if (tip)
1395
1880
  tip.style.display = 'none';
1396
1881
  });
1882
+ // Tooltip for time diff (open-in-editor action)
1883
+ containerEl.addEventListener('mouseover', (e) => {
1884
+ const target = e.target;
1885
+ if (!target?.classList?.contains('gg-log-diff'))
1886
+ return;
1887
+ if (!target.hasAttribute('data-file'))
1888
+ return;
1889
+ const file = target.getAttribute('data-file') || '';
1890
+ const line = target.getAttribute('data-line') || '1';
1891
+ const col = target.getAttribute('data-col') || '1';
1892
+ const tip = containerEl.querySelector('.gg-hover-tooltip');
1893
+ if (!tip)
1894
+ return;
1895
+ // Build tooltip content
1896
+ let actionText;
1897
+ if (nsClickAction === 'open' && DEV) {
1898
+ actionText = `Open in editor: ${file}:${line}:${col}`;
1899
+ }
1900
+ else if (nsClickAction === 'open-url') {
1901
+ actionText = `Open URL: ${formatString(activeFormat(), file, line, col)}`;
1902
+ }
1903
+ else {
1904
+ actionText = `Copy: ${formatString(activeFormat(), file, line, col)}`;
1905
+ }
1906
+ tip.textContent = actionText;
1907
+ tip.style.display = 'block';
1908
+ // Position below the target
1909
+ const targetRect = target.getBoundingClientRect();
1910
+ let left = targetRect.left;
1911
+ let top = targetRect.bottom + 4;
1912
+ // Keep tooltip within viewport
1913
+ const tipRect = tip.getBoundingClientRect();
1914
+ if (left + tipRect.width > window.innerWidth) {
1915
+ left = window.innerWidth - tipRect.width - 8;
1916
+ }
1917
+ if (left < 4)
1918
+ left = 4;
1919
+ if (top + tipRect.height > window.innerHeight) {
1920
+ top = targetRect.top - tipRect.height - 4;
1921
+ }
1922
+ tip.style.left = `${left}px`;
1923
+ tip.style.top = `${top}px`;
1924
+ });
1925
+ containerEl.addEventListener('mouseout', (e) => {
1926
+ const target = e.target;
1927
+ if (!target?.classList?.contains('gg-log-diff'))
1928
+ return;
1929
+ const tip = containerEl.querySelector('.gg-hover-tooltip');
1930
+ if (tip)
1931
+ tip.style.display = 'none';
1932
+ });
1397
1933
  expanderAttached = true;
1398
1934
  }
1399
1935
  function wireUpResize() {
@@ -1447,26 +1983,66 @@ export function createGgPlugin(options, gg) {
1447
1983
  if (!$el)
1448
1984
  return;
1449
1985
  const logContainer = $el.find('.gg-log-container');
1450
- const countSpan = $el.find('.gg-count');
1451
- if (!logContainer.length || !countSpan.length)
1986
+ const copyCountSpan = $el.find('.gg-copy-count');
1987
+ if (!logContainer.length || !copyCountSpan.length)
1452
1988
  return;
1453
1989
  const allEntries = buffer.getEntries();
1454
1990
  // Apply filtering
1455
1991
  const entries = allEntries.filter((entry) => enabledNamespaces.has(entry.namespace));
1456
1992
  renderedEntries = entries;
1457
1993
  const countText = entries.length === allEntries.length
1458
- ? `${entries.length} entries`
1459
- : `${entries.length} / ${allEntries.length} entries`;
1460
- countSpan.html(countText);
1994
+ ? `Copy ${entries.length} ${entries.length === 1 ? 'entry' : 'entries'}`
1995
+ : `Copy ${entries.length} / ${allEntries.length} ${entries.length === 1 ? 'entry' : 'entries'}`;
1996
+ copyCountSpan.html(countText);
1461
1997
  if (entries.length === 0) {
1462
- logContainer.html('<div style="padding: 20px; text-align: center; opacity: 0.5;">No logs captured yet. Call gg() to see output here.</div>');
1998
+ const hasFilteredLogs = allEntries.length > 0;
1999
+ const message = hasFilteredLogs
2000
+ ? `All ${allEntries.length} logs filtered out.`
2001
+ : 'No logs captured yet. Call gg() to see output here.';
2002
+ const resetButton = hasFilteredLogs
2003
+ ? '<button class="gg-reset-filter-btn" style="margin-top: 12px; padding: 10px 20px; cursor: pointer; border: 1px solid #2196F3; background: #2196F3; color: white; border-radius: 6px; font-size: 13px; font-weight: 500; transition: background 0.2s;">Show all logs (gg:*)</button>'
2004
+ : '';
2005
+ logContainer.html(`<div style="padding: 20px; text-align: center; opacity: 0.5;">${message}<div>${resetButton}</div></div>`);
1463
2006
  return;
1464
2007
  }
1465
- const logsHTML = `<div class="gg-log-grid" style="grid-template-columns: ${gridColumns()};">${entries
2008
+ const logsHTML = `<div class="gg-log-grid${filterExpanded ? ' filter-mode' : ''}${showExpressions ? ' gg-show-expr' : ''}" style="grid-template-columns: ${gridColumns()};">${entries
1466
2009
  .map((entry, index) => {
1467
2010
  const color = entry.color || '#0066cc';
1468
2011
  const diff = `+${humanize(entry.diff)}`;
1469
- const ns = escapeHtml(entry.namespace);
2012
+ // Split namespace into clickable segments on multiple delimiters: : @ / - _
2013
+ const parts = entry.namespace.split(/([:/@ \-_])/);
2014
+ const nsSegments = [];
2015
+ const delimiters = [];
2016
+ for (let i = 0; i < parts.length; i++) {
2017
+ if (i % 2 === 0) {
2018
+ // Even indices are segments
2019
+ if (parts[i])
2020
+ nsSegments.push(parts[i]);
2021
+ }
2022
+ else {
2023
+ // Odd indices are delimiters
2024
+ delimiters.push(parts[i]);
2025
+ }
2026
+ }
2027
+ let nsHTML = '';
2028
+ for (let i = 0; i < nsSegments.length; i++) {
2029
+ const segment = escapeHtml(nsSegments[i]);
2030
+ // Build filter pattern: reconstruct namespace up to this point
2031
+ let filterPattern = '';
2032
+ for (let j = 0; j <= i; j++) {
2033
+ filterPattern += nsSegments[j];
2034
+ if (j < i) {
2035
+ filterPattern += delimiters[j];
2036
+ }
2037
+ else if (j < nsSegments.length - 1) {
2038
+ filterPattern += delimiters[j] + '*';
2039
+ }
2040
+ }
2041
+ nsHTML += `<span class="gg-ns-segment" data-filter="${escapeHtml(filterPattern)}">${segment}</span>`;
2042
+ if (i < nsSegments.length - 1) {
2043
+ nsHTML += escapeHtml(delimiters[i]);
2044
+ }
2045
+ }
1470
2046
  // Format each arg individually - objects are expandable
1471
2047
  let argsHTML = '';
1472
2048
  let detailsHTML = '';
@@ -1490,7 +2066,7 @@ export function createGgPlugin(options, gg) {
1490
2066
  return `<tr>${cells}</tr>`;
1491
2067
  })
1492
2068
  .join('');
1493
- argsHTML = `<table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table>`;
2069
+ argsHTML = `<div style="overflow-x: auto;"><table style="border-collapse: collapse; margin: 2px 0; font-family: monospace;"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRowsHtml}</tbody></table></div>`;
1494
2070
  }
1495
2071
  else if (entry.args.length > 0) {
1496
2072
  argsHTML = entry.args
@@ -1507,7 +2083,11 @@ export function createGgPlugin(options, gg) {
1507
2083
  // data-entry/data-arg for hover tooltip lookup, data-src for expression context
1508
2084
  const srcAttr = srcExpr ? ` data-src="${srcExpr}"` : '';
1509
2085
  const srcIcon = srcExpr ? `<span class="gg-src-icon">\uD83D\uDD0D</span>` : '';
1510
- 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>`;
2086
+ // Show expression inline after preview when toggle is enabled
2087
+ const inlineExpr = showExpressions && srcExpr
2088
+ ? ` <span class="gg-inline-expr">\u2039${srcExpr}\u203A</span>`
2089
+ : '';
2090
+ 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}${inlineExpr}</span>`;
1511
2091
  }
1512
2092
  else {
1513
2093
  // Parse ANSI codes first, then convert URLs to clickable links
@@ -1521,33 +2101,11 @@ export function createGgPlugin(options, gg) {
1521
2101
  })
1522
2102
  .join(' ');
1523
2103
  }
1524
- // Filter icons column (only when expanded)
1525
- const iconsCol = filterExpanded
1526
- ? `<div class="gg-log-icons" data-namespace="${ns}">` +
1527
- `<button class="gg-icon-hide" title="Hide this namespace">🗑</button>` +
1528
- `<button class="gg-icon-solo" title="Show only this namespace">🎯</button>` +
1529
- `</div>`
1530
- : '';
1531
- // When filter expanded, diff+ns are clickable (solo) with data-namespace
1532
- const soloAttr = filterExpanded ? ` data-namespace="${ns}"` : '';
1533
- const soloClass = filterExpanded ? ' gg-solo-target' : '';
2104
+ // Time diff will be clickable for open-in-editor when file metadata exists
1534
2105
  // Open-in-editor data attributes (file, line, col)
1535
2106
  const fileAttr = entry.file ? ` data-file="${escapeHtml(entry.file)}"` : '';
1536
2107
  const lineAttr = entry.line ? ` data-line="${entry.line}"` : '';
1537
2108
  const colAttr = entry.col ? ` data-col="${entry.col}"` : '';
1538
- let fileTitleText = '';
1539
- if (entry.file) {
1540
- if (nsClickAction === 'open' && DEV) {
1541
- fileTitleText = `Open in editor: ${entry.file}${entry.line ? ':' + entry.line : ''}${entry.col ? ':' + entry.col : ''}`;
1542
- }
1543
- else if (nsClickAction === 'open-url') {
1544
- fileTitleText = `Open URL: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
1545
- }
1546
- else {
1547
- fileTitleText = `Copy: ${formatString(activeFormat(), entry.file, String(entry.line || 1), String(entry.col || 1))}`;
1548
- }
1549
- }
1550
- const fileTitle = fileTitleText ? ` title="${escapeHtml(fileTitleText)}"` : '';
1551
2109
  // Level class for info/warn/error styling
1552
2110
  const levelClass = entry.level === 'info'
1553
2111
  ? ' gg-level-info'
@@ -1565,14 +2123,19 @@ export function createGgPlugin(options, gg) {
1565
2123
  `<div class="gg-stack-content" data-stack-id="${stackId}">${escapeHtml(entry.stack)}</div>`;
1566
2124
  }
1567
2125
  // Desktop: grid layout, Mobile: stacked layout
1568
- return (`<div class="gg-log-entry${levelClass}">` +
2126
+ // Expression tooltip: skip table entries (tableData) -- expression is just gg.table(...) which isn't useful
2127
+ const hasSrcExpr = !entry.level && !entry.tableData && entry.src?.trim() && !/^['"`]/.test(entry.src);
2128
+ // For primitives-only entries, append inline expression when showExpressions is enabled
2129
+ const inlineExprForPrimitives = showExpressions && hasSrcExpr && !argsHTML.includes('gg-expand')
2130
+ ? ` <span class="gg-inline-expr">\u2039${escapeHtml(entry.src)}\u203A</span>`
2131
+ : '';
2132
+ return (`<div class="gg-log-entry${levelClass}" data-entry="${index}">` +
1569
2133
  `<div class="gg-log-header">` +
1570
- iconsCol +
1571
- `<div class="gg-log-diff${soloClass}" style="color: ${color};"${soloAttr}>${diff}</div>` +
1572
- `<div class="gg-log-ns${soloClass}" style="color: ${color};"${soloAttr}${fileAttr}${lineAttr}${colAttr}${fileTitle}>${ns}</div>` +
2134
+ `<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
2135
+ `<div class="gg-log-ns" style="color: ${color};" data-namespace="${escapeHtml(entry.namespace)}"><span class="gg-ns-text">${nsHTML}</span><button class="gg-ns-hide" data-namespace="${escapeHtml(entry.namespace)}" title="Hide this namespace">\u00d7</button></div>` +
1573
2136
  `<div class="gg-log-handle"></div>` +
1574
2137
  `</div>` +
1575
- `<div class="gg-log-content"${!entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src) ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${stackHTML}</div>` +
2138
+ `<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${argsHTML}${inlineExprForPrimitives}${stackHTML}</div>` +
1576
2139
  detailsHTML +
1577
2140
  `</div>`);
1578
2141
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"