@leftium/gg 0.0.49 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -2
- package/dist/debug/browser.d.ts +1 -1
- package/dist/debug/browser.js +14 -6
- package/dist/debug/node.js +5 -3
- package/dist/eruda/buffer.d.ts +4 -0
- package/dist/eruda/buffer.js +16 -0
- package/dist/eruda/loader.js +58 -0
- package/dist/eruda/plugin.d.ts +5 -1
- package/dist/eruda/plugin.js +1219 -379
- package/dist/eruda/types.d.ts +20 -1
- package/dist/gg-call-sites-plugin.js +11 -4
- package/dist/gg-file-sink-plugin.d.ts +6 -0
- package/dist/gg-file-sink-plugin.js +394 -0
- package/dist/gg.d.ts +3 -0
- package/dist/gg.js +160 -62
- package/dist/open-in-editor.js +1 -1
- package/dist/pattern.d.ts +23 -0
- package/dist/pattern.js +41 -0
- package/dist/vite.d.ts +12 -2
- package/dist/vite.js +7 -1
- package/package.json +17 -17
package/dist/eruda/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DEV } from 'esm-env';
|
|
2
2
|
import { LogBuffer } from './buffer.js';
|
|
3
3
|
import { Virtualizer, elementScroll, observeElementOffset, observeElementRect, measureElement } from '@tanstack/virtual-core';
|
|
4
|
+
import { matchesGlob, matchesPattern as _matchesPattern } from '../pattern.js';
|
|
4
5
|
const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
|
|
5
6
|
/**
|
|
6
7
|
* Creates the gg Eruda plugin
|
|
@@ -9,7 +10,10 @@ const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_P
|
|
|
9
10
|
* Methods: $el.html(), $el.show(), $el.hide(), $el.find(), $el.on()
|
|
10
11
|
*/
|
|
11
12
|
export function createGgPlugin(options, gg) {
|
|
12
|
-
const
|
|
13
|
+
const _savedCap = typeof localStorage !== 'undefined'
|
|
14
|
+
? parseInt(localStorage.getItem('gg-buffer-cap') ?? '', 10)
|
|
15
|
+
: NaN;
|
|
16
|
+
const buffer = new LogBuffer(!isNaN(_savedCap) && _savedCap > 0 ? _savedCap : (options.maxEntries ?? 2000));
|
|
13
17
|
// The licia jQuery-like wrapper Eruda passes to init()
|
|
14
18
|
let $el = null;
|
|
15
19
|
let expanderAttached = false;
|
|
@@ -17,7 +21,6 @@ export function createGgPlugin(options, gg) {
|
|
|
17
21
|
// null = auto (fit content), number = user-dragged px width
|
|
18
22
|
let nsColWidth = null;
|
|
19
23
|
// Filter UI state
|
|
20
|
-
let filterExpanded = false;
|
|
21
24
|
let filterPattern = '';
|
|
22
25
|
const enabledNamespaces = new Set();
|
|
23
26
|
// Virtual scroll: filtered indices into the buffer (the "rows" the virtualizer sees)
|
|
@@ -29,16 +32,36 @@ export function createGgPlugin(options, gg) {
|
|
|
29
32
|
// stack IDs (e.g. "stack-42") for stack traces.
|
|
30
33
|
const expandedDetails = new Set();
|
|
31
34
|
const expandedStacks = new Set();
|
|
32
|
-
// Toast state
|
|
35
|
+
// Toast state
|
|
33
36
|
let lastHiddenPattern = null; // filterPattern before the hide (for undo)
|
|
37
|
+
let lastDroppedPattern = null; // keepPattern before the drop (for undo)
|
|
38
|
+
let lastKeptPattern = null; // keepPattern before the [+] keep (for undo)
|
|
39
|
+
let lastKeptNamespaceInfo = null; // sentinel entry to restore on undo
|
|
40
|
+
let toastMode = 'hide'; // which layer the current toast targets
|
|
34
41
|
let hasSeenToastExplanation = false; // first toast auto-expands help text
|
|
42
|
+
// Sentinel section debounce: pending rAF for re-rendering dropped sentinels
|
|
43
|
+
let sentinelRenderPending = false;
|
|
44
|
+
// Whether the sentinel section is expanded (persists across re-renders)
|
|
45
|
+
let sentinelExpanded = true;
|
|
35
46
|
// Settings UI state
|
|
36
47
|
let settingsExpanded = false;
|
|
48
|
+
// Layer 1: Keep gate pattern ('gg-keep') — controls which loggs enter the ring buffer.
|
|
49
|
+
// Default 'gg:*' (keep all gg namespaces). Users narrow it to reduce buffer pressure.
|
|
50
|
+
let keepPattern = 'gg:*';
|
|
51
|
+
// Native console output toggle ('gg-console')
|
|
52
|
+
// Default: true (gg works without Eruda), but Eruda flips it to false on init
|
|
53
|
+
// unless the user has explicitly set it.
|
|
54
|
+
let ggConsoleEnabled = true;
|
|
37
55
|
// Expression visibility toggle
|
|
38
56
|
let showExpressions = false;
|
|
39
|
-
// Filter pattern persistence
|
|
40
|
-
const
|
|
57
|
+
// Filter pattern persistence keys
|
|
58
|
+
const SHOW_KEY = 'gg-show'; // Layer 2: which kept loggs to display in panel + console
|
|
59
|
+
const KEEP_KEY = 'gg-keep'; // Layer 1: which loggs enter the ring buffer
|
|
60
|
+
const CONSOLE_KEY = 'gg-console'; // Whether shown loggs also go to native console
|
|
41
61
|
const SHOW_EXPRESSIONS_KEY = 'gg-show-expressions';
|
|
62
|
+
const BUFFER_CAP_KEY = 'gg-buffer-cap'; // Ring buffer capacity (maxEntries)
|
|
63
|
+
// Backward-compat alias (old key name — ignored after migration)
|
|
64
|
+
const LEGACY_FILTER_KEY = 'gg-filter';
|
|
42
65
|
// Namespace click action: 'open' uses Vite dev middleware, 'copy' copies formatted string, 'open-url' navigates to URI
|
|
43
66
|
const NS_ACTION_KEY = 'gg-ns-action';
|
|
44
67
|
const EDITOR_BIN_KEY = 'gg-editor-bin';
|
|
@@ -50,6 +73,11 @@ export function createGgPlugin(options, gg) {
|
|
|
50
73
|
let pendingEntries = []; // new entries since last render
|
|
51
74
|
// All namespaces ever seen (maintained incrementally, avoids scanning buffer)
|
|
52
75
|
const allNamespacesSet = new Set();
|
|
76
|
+
// Phase 2: loggs dropped by the keep gate, tracked outside the ring buffer.
|
|
77
|
+
// Key: namespace string. Grows with distinct dropped namespaces (expected: tens, not thousands).
|
|
78
|
+
const droppedNamespaces = new Map();
|
|
79
|
+
// Total loggs ever received (kept + dropped).
|
|
80
|
+
let receivedTotal = 0;
|
|
53
81
|
// Plugin detection state (probed once at init)
|
|
54
82
|
let openInEditorPluginDetected = null; // null = not yet probed
|
|
55
83
|
// Editor bins for launch-editor (common first, then alphabetical)
|
|
@@ -126,8 +154,7 @@ export function createGgPlugin(options, gg) {
|
|
|
126
154
|
function formatEntryForClipboard(entry, includeExpressions) {
|
|
127
155
|
// Extract HH:MM:SS.mmm from timestamp (with milliseconds)
|
|
128
156
|
const time = new Date(entry.timestamp).toISOString().slice(11, 23);
|
|
129
|
-
|
|
130
|
-
const ns = entry.namespace.trim().replace(/^gg:/, '');
|
|
157
|
+
const ns = entry.namespace.trim();
|
|
131
158
|
// Include expression on its own line above the value when toggle is enabled
|
|
132
159
|
const hasSrcExpr = !entry.level && entry.src?.trim() && !/^['"`]/.test(entry.src);
|
|
133
160
|
const exprLine = includeExpressions && hasSrcExpr ? `\u2039${entry.src}\u203A\n` : '';
|
|
@@ -147,18 +174,77 @@ export function createGgPlugin(options, gg) {
|
|
|
147
174
|
name: 'GG',
|
|
148
175
|
init($container) {
|
|
149
176
|
$el = $container;
|
|
150
|
-
// Load filter state BEFORE registering _onLog hook, because
|
|
177
|
+
// Load filter state BEFORE registering _onLog hook, because registering
|
|
151
178
|
// triggers replay of earlyLogBuffer and each entry checks filterPattern
|
|
152
|
-
|
|
179
|
+
const _legacyFilter = localStorage.getItem(LEGACY_FILTER_KEY);
|
|
180
|
+
filterPattern = localStorage.getItem(SHOW_KEY) || _legacyFilter || 'gg:*';
|
|
181
|
+
keepPattern = localStorage.getItem(KEEP_KEY) || 'gg:*';
|
|
153
182
|
showExpressions = localStorage.getItem(SHOW_EXPRESSIONS_KEY) === 'true';
|
|
154
|
-
//
|
|
183
|
+
// gg-console: Eruda flips to false on init, unless user explicitly set it.
|
|
184
|
+
// This lets gg work zero-config (console output enabled) before Eruda loads,
|
|
185
|
+
// while silencing the noise when the Eruda panel is in use.
|
|
186
|
+
const userSetConsole = localStorage.getItem(CONSOLE_KEY);
|
|
187
|
+
if (userSetConsole === null) {
|
|
188
|
+
// Not explicitly set — Eruda auto-flips to false
|
|
189
|
+
localStorage.setItem(CONSOLE_KEY, 'false');
|
|
190
|
+
ggConsoleEnabled = false;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
ggConsoleEnabled = userSetConsole !== 'false';
|
|
194
|
+
}
|
|
195
|
+
// Tell the debug factory to reload its enabled state from gg-show/gg-console.
|
|
196
|
+
// browser.ts load() now reads gg-console + gg-show instead of localStorage.debug.
|
|
197
|
+
// Re-calling enable() with the right pattern updates which namespaces output to console.
|
|
198
|
+
import('../debug/index.js').then(({ default: dbg }) => {
|
|
199
|
+
try {
|
|
200
|
+
// Only call enable() when console output is on — enable('') would
|
|
201
|
+
// call localStorage.removeItem('gg-show'), wiping the persisted Show filter.
|
|
202
|
+
// When console is disabled, load() in browser.ts already returns '' via
|
|
203
|
+
// the gg-console=false check, so no enable() call is needed.
|
|
204
|
+
if (ggConsoleEnabled) {
|
|
205
|
+
dbg.enable(filterPattern || 'gg:*');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// ignore
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// Register the capture hook on gg (prefer multi-listener API, fall back to legacy)
|
|
155
213
|
if (gg) {
|
|
156
|
-
|
|
214
|
+
const onEntry = (entry) => {
|
|
215
|
+
// Track total received (before any filtering)
|
|
216
|
+
receivedTotal++;
|
|
217
|
+
// Layer 1: Keep gate — drop loggs that don't match gg-keep
|
|
218
|
+
const effectiveKeep = keepPattern || 'gg:*';
|
|
219
|
+
if (!namespaceMatchesPattern(entry.namespace, effectiveKeep)) {
|
|
220
|
+
// Logg is dropped — not stored in ring buffer. Track it in droppedNamespaces.
|
|
221
|
+
const typeKey = entry.level ?? 'log';
|
|
222
|
+
const existing = droppedNamespaces.get(entry.namespace);
|
|
223
|
+
if (existing) {
|
|
224
|
+
existing.lastSeen = entry.timestamp;
|
|
225
|
+
existing.total++;
|
|
226
|
+
existing.byType[typeKey] = (existing.byType[typeKey] ?? 0) + 1;
|
|
227
|
+
existing.preview = entry;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
droppedNamespaces.set(entry.namespace, {
|
|
231
|
+
namespace: entry.namespace,
|
|
232
|
+
firstSeen: entry.timestamp,
|
|
233
|
+
lastSeen: entry.timestamp,
|
|
234
|
+
total: 1,
|
|
235
|
+
byType: { [typeKey]: 1 },
|
|
236
|
+
preview: entry
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
// Schedule debounced sentinel re-render
|
|
240
|
+
scheduleSentinelRender();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
157
243
|
// Track namespaces incrementally (O(1) instead of scanning buffer)
|
|
158
244
|
const isNewNamespace = !allNamespacesSet.has(entry.namespace);
|
|
159
245
|
allNamespacesSet.add(entry.namespace);
|
|
160
246
|
buffer.push(entry);
|
|
161
|
-
//
|
|
247
|
+
// Layer 2: Show filter — track which namespaces are currently visible
|
|
162
248
|
const effectivePattern = filterPattern || 'gg:*';
|
|
163
249
|
if (namespaceMatchesPattern(entry.namespace, effectivePattern)) {
|
|
164
250
|
enabledNamespaces.add(entry.namespace);
|
|
@@ -179,6 +265,15 @@ export function createGgPlugin(options, gg) {
|
|
|
179
265
|
});
|
|
180
266
|
}
|
|
181
267
|
};
|
|
268
|
+
if (gg.addLogListener) {
|
|
269
|
+
gg.addLogListener(onEntry);
|
|
270
|
+
// Store reference for removal on destroy
|
|
271
|
+
gg.__ggErudaListener = onEntry;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
// Legacy fallback: single-slot _onLog
|
|
275
|
+
gg._onLog = onEntry;
|
|
276
|
+
}
|
|
182
277
|
}
|
|
183
278
|
// Probe for openInEditorPlugin (status 222) and auto-populate $ROOT in dev mode
|
|
184
279
|
if (DEV) {
|
|
@@ -187,7 +282,7 @@ export function createGgPlugin(options, gg) {
|
|
|
187
282
|
openInEditorPluginDetected = r.status === 222;
|
|
188
283
|
// If plugin detected, fetch project root for $ROOT variable
|
|
189
284
|
if (openInEditorPluginDetected && !projectRoot) {
|
|
190
|
-
return fetch('/__gg
|
|
285
|
+
return fetch('/__gg/project-root').then((r) => r.text());
|
|
191
286
|
}
|
|
192
287
|
})
|
|
193
288
|
.then((root) => {
|
|
@@ -209,6 +304,7 @@ export function createGgPlugin(options, gg) {
|
|
|
209
304
|
wireUpExpanders();
|
|
210
305
|
wireUpResize();
|
|
211
306
|
wireUpFilterUI();
|
|
307
|
+
wireUpKeepUI();
|
|
212
308
|
wireUpSettingsUI();
|
|
213
309
|
wireUpToast();
|
|
214
310
|
// Discard any entries queued during early-buffer replay (before the DOM
|
|
@@ -231,13 +327,22 @@ export function createGgPlugin(options, gg) {
|
|
|
231
327
|
},
|
|
232
328
|
destroy() {
|
|
233
329
|
if (gg) {
|
|
234
|
-
|
|
330
|
+
const listener = gg
|
|
331
|
+
.__ggErudaListener;
|
|
332
|
+
if (gg.removeLogListener && listener) {
|
|
333
|
+
gg.removeLogListener(listener);
|
|
334
|
+
delete gg.__ggErudaListener;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
gg._onLog = null;
|
|
338
|
+
}
|
|
235
339
|
}
|
|
236
340
|
// Clean up virtualizer
|
|
237
341
|
if (virtualizer && $el) {
|
|
238
342
|
const containerDom = $el.find('.gg-log-container').get(0);
|
|
239
343
|
if (containerDom) {
|
|
240
|
-
const cleanup = containerDom
|
|
344
|
+
const cleanup = containerDom
|
|
345
|
+
.__ggVirtualCleanup;
|
|
241
346
|
if (cleanup)
|
|
242
347
|
cleanup();
|
|
243
348
|
}
|
|
@@ -245,9 +350,39 @@ export function createGgPlugin(options, gg) {
|
|
|
245
350
|
}
|
|
246
351
|
buffer.clear();
|
|
247
352
|
allNamespacesSet.clear();
|
|
353
|
+
droppedNamespaces.clear();
|
|
248
354
|
filteredIndices = [];
|
|
355
|
+
receivedTotal = 0;
|
|
356
|
+
},
|
|
357
|
+
/** Returns a read-only view of the dropped-namespace tracking map (Phase 2 data layer). */
|
|
358
|
+
getDroppedNamespaces() {
|
|
359
|
+
return droppedNamespaces;
|
|
249
360
|
}
|
|
250
361
|
};
|
|
362
|
+
function toggleKeepNamespace(namespace, enable) {
|
|
363
|
+
const currentPattern = keepPattern || 'gg:*';
|
|
364
|
+
const ns = namespace.trim();
|
|
365
|
+
const parts = currentPattern
|
|
366
|
+
.split(',')
|
|
367
|
+
.map((p) => p.trim())
|
|
368
|
+
.filter(Boolean);
|
|
369
|
+
if (enable) {
|
|
370
|
+
// Remove exclusion — namespace is now kept
|
|
371
|
+
const filtered = parts.filter((p) => p !== `-${ns}`);
|
|
372
|
+
keepPattern = filtered.join(',') || 'gg:*';
|
|
373
|
+
// Remove from droppedNamespaces so sentinel disappears
|
|
374
|
+
droppedNamespaces.delete(ns);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
// Add exclusion — namespace is dropped
|
|
378
|
+
const exclusion = `-${ns}`;
|
|
379
|
+
if (!parts.includes(exclusion))
|
|
380
|
+
parts.push(exclusion);
|
|
381
|
+
keepPattern = parts.join(',');
|
|
382
|
+
}
|
|
383
|
+
keepPattern = simplifyPattern(keepPattern) || 'gg:*';
|
|
384
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
385
|
+
}
|
|
251
386
|
function toggleNamespace(namespace, enable) {
|
|
252
387
|
const currentPattern = filterPattern || 'gg:*';
|
|
253
388
|
const ns = namespace.trim();
|
|
@@ -310,21 +445,38 @@ export function createGgPlugin(options, gg) {
|
|
|
310
445
|
enabledNamespaces.add(ns);
|
|
311
446
|
}
|
|
312
447
|
});
|
|
313
|
-
|
|
314
|
-
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
448
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
315
449
|
}
|
|
316
450
|
function simplifyPattern(pattern) {
|
|
317
451
|
if (!pattern)
|
|
318
452
|
return '';
|
|
319
|
-
// Remove empty parts
|
|
320
|
-
|
|
453
|
+
// Remove empty parts and duplicates
|
|
454
|
+
const parts = Array.from(new Set(pattern
|
|
321
455
|
.split(',')
|
|
322
456
|
.map((p) => p.trim())
|
|
323
|
-
.filter(Boolean);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
457
|
+
.filter(Boolean)));
|
|
458
|
+
const inclusions = parts.filter((p) => !p.startsWith('-'));
|
|
459
|
+
const exclusions = parts.filter((p) => p.startsWith('-'));
|
|
460
|
+
const hasWildcardBase = inclusions.includes('gg:*') || inclusions.includes('*');
|
|
461
|
+
const wildcardBase = inclusions.includes('gg:*') ? 'gg:*' : '*';
|
|
462
|
+
// If there's a wildcard base (gg:* or *), drop all other inclusions (they're subsumed)
|
|
463
|
+
const finalInclusions = hasWildcardBase ? [wildcardBase] : inclusions;
|
|
464
|
+
// Drop exclusions that are subsumed by a broader exclusion.
|
|
465
|
+
// e.g. -routes/demo-helpers.ts:validation is subsumed by -routes/demo-*
|
|
466
|
+
const finalExclusions = exclusions.filter((excl) => {
|
|
467
|
+
const exclNs = excl.slice(1); // strip leading '-'
|
|
468
|
+
// Keep this exclusion only if no other exclusion is broader and covers it
|
|
469
|
+
return !exclusions.some((other) => {
|
|
470
|
+
if (other === excl)
|
|
471
|
+
return false;
|
|
472
|
+
const otherNs = other.slice(1);
|
|
473
|
+
return matchesGlob(exclNs, otherNs);
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
// If there's a wildcard base, also drop inclusions that match no exclusion boundary
|
|
477
|
+
// (they can't add any namespace that * doesn't already include)
|
|
478
|
+
// Final inclusions are already collapsed to ['*'] above, so nothing more to do.
|
|
479
|
+
return [...finalInclusions, ...finalExclusions].join(',');
|
|
328
480
|
}
|
|
329
481
|
function getAllCapturedNamespaces() {
|
|
330
482
|
return Array.from(allNamespacesSet).sort();
|
|
@@ -332,54 +484,20 @@ export function createGgPlugin(options, gg) {
|
|
|
332
484
|
function namespaceMatchesPattern(namespace, pattern) {
|
|
333
485
|
if (!pattern)
|
|
334
486
|
return true; // Empty pattern = show all
|
|
335
|
-
|
|
336
|
-
const parts = pattern.split(',').map((p) => p.trim());
|
|
337
|
-
let included = false;
|
|
338
|
-
let excluded = false;
|
|
339
|
-
for (const part of parts) {
|
|
340
|
-
if (part.startsWith('-')) {
|
|
341
|
-
// Exclusion pattern
|
|
342
|
-
const excludePattern = part.slice(1);
|
|
343
|
-
if (matchesGlob(namespace, excludePattern)) {
|
|
344
|
-
excluded = true;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
// Inclusion pattern
|
|
349
|
-
if (matchesGlob(namespace, part)) {
|
|
350
|
-
included = true;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
// If no inclusion patterns, default to included
|
|
355
|
-
const hasInclusions = parts.some((p) => !p.startsWith('-'));
|
|
356
|
-
if (!hasInclusions)
|
|
357
|
-
included = true;
|
|
358
|
-
return included && !excluded;
|
|
359
|
-
}
|
|
360
|
-
function matchesGlob(str, pattern) {
|
|
361
|
-
// Trim both for comparison (namespaces may have trailing spaces from padEnd)
|
|
362
|
-
const s = str.trim();
|
|
363
|
-
const p = pattern.trim();
|
|
364
|
-
// Convert glob pattern to regex
|
|
365
|
-
const regexPattern = p
|
|
366
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
367
|
-
.replace(/\*/g, '.*'); // * becomes .*
|
|
368
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
369
|
-
return regex.test(s);
|
|
487
|
+
return _matchesPattern(namespace, pattern);
|
|
370
488
|
}
|
|
371
489
|
function isSimplePattern(pattern) {
|
|
372
490
|
if (!pattern)
|
|
373
491
|
return true;
|
|
374
492
|
// Simple patterns:
|
|
375
|
-
// 1. '
|
|
376
|
-
// 2. Explicit comma-separated list of exact namespaces
|
|
493
|
+
// 1. '*' with optional exclusions (e.g. '*,-api:verbose:*')
|
|
494
|
+
// 2. Explicit comma-separated list of exact namespaces (no wildcards, no exclusions)
|
|
377
495
|
const parts = pattern.split(',').map((p) => p.trim());
|
|
378
|
-
// Check if it's 'gg:*' based (with exclusions)
|
|
379
|
-
const hasWildcardBase = parts.some((p) => p === '
|
|
496
|
+
// Check if it's '*' or 'gg:*' based (with exclusions)
|
|
497
|
+
const hasWildcardBase = parts.some((p) => p === '*' || p === 'gg:*');
|
|
380
498
|
if (hasWildcardBase) {
|
|
381
|
-
// All other parts must be exclusions
|
|
382
|
-
const otherParts = parts.filter((p) => p !== '
|
|
499
|
+
// All other parts must be plain exclusions (no wildcards in the exclusion)
|
|
500
|
+
const otherParts = parts.filter((p) => p !== '*' && p !== 'gg:*');
|
|
383
501
|
return otherParts.every((p) => p.startsWith('-') && !p.includes('*', 1));
|
|
384
502
|
}
|
|
385
503
|
// Check if it's an explicit list (no wildcards)
|
|
@@ -393,6 +511,30 @@ export function createGgPlugin(options, gg) {
|
|
|
393
511
|
// at a time so auto would resize based on visible subset.
|
|
394
512
|
return `3.5em ${ns} 4px 1fr`;
|
|
395
513
|
}
|
|
514
|
+
// ─── Inline SVG icons ────────────────────────────────────────────────────
|
|
515
|
+
// All icons use currentColor so they inherit the namespace color on log rows
|
|
516
|
+
// or the green tint on sentinel keep buttons.
|
|
517
|
+
const SVG_ATTR = `viewBox="0 0 12 12" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"`;
|
|
518
|
+
/** Eye with diagonal slash — hide from view (Layer 2 / gg-show) */
|
|
519
|
+
const ICON_HIDE = `<svg ${SVG_ATTR}>` +
|
|
520
|
+
`<path d="M1 6 C2.5 2.5 9.5 2.5 11 6 C9.5 9.5 2.5 9.5 1 6"/>` +
|
|
521
|
+
`<circle cx="6" cy="6" r="1.5" fill="currentColor" stroke="none"/>` +
|
|
522
|
+
`<line x1="9.5" y1="1.5" x2="2.5" y2="10.5"/>` +
|
|
523
|
+
`</svg>`;
|
|
524
|
+
/** Trash can — drop from buffer (Layer 1 / gg-keep) */
|
|
525
|
+
const ICON_DROP = `<svg ${SVG_ATTR}>` +
|
|
526
|
+
`<line x1="2" y1="3.5" x2="10" y2="3.5"/>` +
|
|
527
|
+
`<path d="M4.5 3.5V2.5h3v1"/>` +
|
|
528
|
+
`<path d="M3 3.5l.5 7h5l.5-7"/>` +
|
|
529
|
+
`<line x1="5" y1="5.5" x2="5" y2="9"/>` +
|
|
530
|
+
`<line x1="7" y1="5.5" x2="7" y2="9"/>` +
|
|
531
|
+
`</svg>`;
|
|
532
|
+
/** Plus inside a circle — keep in buffer (Layer 1 / gg-keep) */
|
|
533
|
+
const ICON_KEEP = `<svg ${SVG_ATTR}>` +
|
|
534
|
+
`<circle cx="6" cy="6" r="4.5"/>` +
|
|
535
|
+
`<line x1="6" y1="3.5" x2="6" y2="8.5"/>` +
|
|
536
|
+
`<line x1="3.5" y1="6" x2="8.5" y2="6"/>` +
|
|
537
|
+
`</svg>`;
|
|
396
538
|
function buildHTML() {
|
|
397
539
|
return `
|
|
398
540
|
<style>
|
|
@@ -458,6 +600,7 @@ export function createGgPlugin(options, gg) {
|
|
|
458
600
|
text-decoration-style: solid;
|
|
459
601
|
text-underline-offset: 2px;
|
|
460
602
|
}
|
|
603
|
+
|
|
461
604
|
.gg-details {
|
|
462
605
|
grid-column: 1 / -1;
|
|
463
606
|
border-top: none;
|
|
@@ -485,25 +628,213 @@ export function createGgPlugin(options, gg) {
|
|
|
485
628
|
text-overflow: ellipsis;
|
|
486
629
|
min-width: 0;
|
|
487
630
|
}
|
|
488
|
-
.gg-ns-hide
|
|
631
|
+
.gg-ns-hide,
|
|
632
|
+
.gg-ns-drop {
|
|
489
633
|
all: unset;
|
|
490
634
|
cursor: pointer;
|
|
491
635
|
opacity: 0;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
transition: opacity 0.15s;
|
|
636
|
+
display: inline-flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
padding: 2px 3px;
|
|
639
|
+
border-radius: 3px;
|
|
640
|
+
transition: opacity 0.15s, background 0.1s;
|
|
497
641
|
flex-shrink: 0;
|
|
498
642
|
}
|
|
499
|
-
|
|
500
|
-
|
|
643
|
+
.gg-ns-hide svg,
|
|
644
|
+
.gg-ns-drop svg,
|
|
645
|
+
.gg-sentinel-keep svg {
|
|
646
|
+
pointer-events: none;
|
|
647
|
+
}
|
|
648
|
+
.gg-log-ns:hover .gg-ns-hide,
|
|
649
|
+
.gg-log-ns:hover .gg-ns-drop {
|
|
650
|
+
opacity: 0.35;
|
|
501
651
|
}
|
|
502
652
|
.gg-ns-hide:hover {
|
|
503
653
|
opacity: 1 !important;
|
|
504
654
|
background: rgba(0,0,0,0.08);
|
|
505
|
-
border-radius: 3px;
|
|
506
655
|
}
|
|
656
|
+
.gg-ns-drop:hover {
|
|
657
|
+
opacity: 1 !important;
|
|
658
|
+
background: rgba(200,50,0,0.12);
|
|
659
|
+
}
|
|
660
|
+
/* Sentinel section: collapsible above log container */
|
|
661
|
+
.gg-sentinel-section {
|
|
662
|
+
flex-shrink: 0;
|
|
663
|
+
background: #f9f9f9;
|
|
664
|
+
border-bottom: 2px solid rgba(0,0,0,0.1);
|
|
665
|
+
}
|
|
666
|
+
.gg-sentinel-header {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
gap: 6px;
|
|
670
|
+
padding: 4px 10px;
|
|
671
|
+
cursor: pointer;
|
|
672
|
+
font-size: 11px;
|
|
673
|
+
opacity: 0.6;
|
|
674
|
+
user-select: none;
|
|
675
|
+
}
|
|
676
|
+
.gg-sentinel-header:hover {
|
|
677
|
+
opacity: 1;
|
|
678
|
+
background: rgba(0,0,0,0.03);
|
|
679
|
+
}
|
|
680
|
+
.gg-sentinel-toggle {
|
|
681
|
+
font-size: 10px;
|
|
682
|
+
}
|
|
683
|
+
.gg-sentinel-rows {
|
|
684
|
+
max-height: 120px;
|
|
685
|
+
overflow-y: auto;
|
|
686
|
+
}
|
|
687
|
+
.gg-sentinel-rows.collapsed {
|
|
688
|
+
display: none;
|
|
689
|
+
}
|
|
690
|
+
.gg-sentinel-row {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: flex-start;
|
|
693
|
+
gap: 6px;
|
|
694
|
+
padding: 5px 10px 4px;
|
|
695
|
+
border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
696
|
+
color: #888;
|
|
697
|
+
font-family: monospace;
|
|
698
|
+
font-size: 12px;
|
|
699
|
+
}
|
|
700
|
+
.gg-sentinel-row:last-child {
|
|
701
|
+
border-bottom: none;
|
|
702
|
+
}
|
|
703
|
+
.gg-sentinel-keep {
|
|
704
|
+
all: unset;
|
|
705
|
+
cursor: pointer;
|
|
706
|
+
color: #4caf50;
|
|
707
|
+
flex-shrink: 0;
|
|
708
|
+
display: inline-flex;
|
|
709
|
+
align-items: center;
|
|
710
|
+
padding: 2px 3px;
|
|
711
|
+
border-radius: 3px;
|
|
712
|
+
transition: background 0.1s;
|
|
713
|
+
}
|
|
714
|
+
.gg-sentinel-keep:hover {
|
|
715
|
+
background: rgba(76,175,80,0.15);
|
|
716
|
+
}
|
|
717
|
+
.gg-sentinel-ns {
|
|
718
|
+
font-weight: bold;
|
|
719
|
+
color: #777;
|
|
720
|
+
}
|
|
721
|
+
.gg-sentinel-count {
|
|
722
|
+
color: #999;
|
|
723
|
+
flex-shrink: 0;
|
|
724
|
+
white-space: nowrap;
|
|
725
|
+
}
|
|
726
|
+
.gg-sentinel-preview {
|
|
727
|
+
color: #aaa;
|
|
728
|
+
font-style: italic;
|
|
729
|
+
overflow: hidden;
|
|
730
|
+
text-overflow: ellipsis;
|
|
731
|
+
white-space: nowrap;
|
|
732
|
+
min-width: 0;
|
|
733
|
+
flex: 1;
|
|
734
|
+
}
|
|
735
|
+
/* Pipeline row */
|
|
736
|
+
.gg-pipeline {
|
|
737
|
+
display: flex;
|
|
738
|
+
flex-wrap: wrap;
|
|
739
|
+
align-items: center;
|
|
740
|
+
gap: 2px;
|
|
741
|
+
padding: 3px 2px;
|
|
742
|
+
flex-shrink: 0;
|
|
743
|
+
margin-bottom: 2px;
|
|
744
|
+
}
|
|
745
|
+
.gg-pipeline-arrow {
|
|
746
|
+
font-size: 11px;
|
|
747
|
+
opacity: 0.35;
|
|
748
|
+
flex-shrink: 0;
|
|
749
|
+
user-select: none;
|
|
750
|
+
}
|
|
751
|
+
.gg-pipeline-node {
|
|
752
|
+
font-size: 11px;
|
|
753
|
+
font-family: monospace;
|
|
754
|
+
background: rgba(0,0,0,0.06);
|
|
755
|
+
border-radius: 4px;
|
|
756
|
+
padding: 2px 6px;
|
|
757
|
+
white-space: nowrap;
|
|
758
|
+
color: #444;
|
|
759
|
+
border: none;
|
|
760
|
+
cursor: default;
|
|
761
|
+
}
|
|
762
|
+
button.gg-pipeline-node {
|
|
763
|
+
cursor: pointer;
|
|
764
|
+
}
|
|
765
|
+
button.gg-pipeline-node:hover {
|
|
766
|
+
background: rgba(0,0,0,0.11);
|
|
767
|
+
}
|
|
768
|
+
.gg-buf-size-input {
|
|
769
|
+
width: 5em;
|
|
770
|
+
font-family: monospace;
|
|
771
|
+
font-size: 11px;
|
|
772
|
+
padding: 1px 4px;
|
|
773
|
+
border: 1px solid rgba(0,0,0,0.25);
|
|
774
|
+
border-radius: 3px;
|
|
775
|
+
background: #fff;
|
|
776
|
+
}
|
|
777
|
+
.gg-pipeline-handle {
|
|
778
|
+
all: unset;
|
|
779
|
+
font-size: 10px;
|
|
780
|
+
font-family: monospace;
|
|
781
|
+
color: #888;
|
|
782
|
+
cursor: pointer;
|
|
783
|
+
padding: 1px 5px;
|
|
784
|
+
border-radius: 3px;
|
|
785
|
+
border: 1px solid rgba(0,0,0,0.15);
|
|
786
|
+
white-space: nowrap;
|
|
787
|
+
transition: background 0.1s, color 0.1s;
|
|
788
|
+
user-select: none;
|
|
789
|
+
}
|
|
790
|
+
.gg-pipeline-handle:hover,
|
|
791
|
+
.gg-pipeline-handle.active {
|
|
792
|
+
background: rgba(0,0,0,0.08);
|
|
793
|
+
color: #222;
|
|
794
|
+
}
|
|
795
|
+
.gg-pipeline-handle.active {
|
|
796
|
+
border-color: rgba(0,0,0,0.3);
|
|
797
|
+
}
|
|
798
|
+
/* Pipeline panels (keep / show) */
|
|
799
|
+
.gg-pipeline-panel {
|
|
800
|
+
flex-shrink: 0;
|
|
801
|
+
margin-bottom: 4px;
|
|
802
|
+
}
|
|
803
|
+
.gg-pipeline-panel-header {
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
gap: 6px;
|
|
807
|
+
padding: 2px 2px 4px;
|
|
808
|
+
}
|
|
809
|
+
.gg-filter-label {
|
|
810
|
+
font-size: 11px;
|
|
811
|
+
opacity: 0.6;
|
|
812
|
+
white-space: nowrap;
|
|
813
|
+
flex-shrink: 0;
|
|
814
|
+
}
|
|
815
|
+
.gg-keep-input,
|
|
816
|
+
.gg-show-input {
|
|
817
|
+
flex: 1;
|
|
818
|
+
min-width: 0;
|
|
819
|
+
padding: 3px 6px;
|
|
820
|
+
font-family: monospace;
|
|
821
|
+
font-size: 13px;
|
|
822
|
+
border: 1px solid rgba(0,0,0,0.2);
|
|
823
|
+
border-radius: 3px;
|
|
824
|
+
background: transparent;
|
|
825
|
+
}
|
|
826
|
+
.gg-filter-count {
|
|
827
|
+
font-size: 11px;
|
|
828
|
+
opacity: 0.5;
|
|
829
|
+
white-space: nowrap;
|
|
830
|
+
flex-shrink: 0;
|
|
831
|
+
}
|
|
832
|
+
.gg-filter-details-body {
|
|
833
|
+
background: #f5f5f5;
|
|
834
|
+
padding: 8px 10px;
|
|
835
|
+
border-radius: 4px;
|
|
836
|
+
margin-bottom: 4px;
|
|
837
|
+
}
|
|
507
838
|
/* Toast bar for "namespace hidden" feedback */
|
|
508
839
|
.gg-toast {
|
|
509
840
|
display: none;
|
|
@@ -796,25 +1127,7 @@ export function createGgPlugin(options, gg) {
|
|
|
796
1127
|
.gg-stack-content.expanded {
|
|
797
1128
|
display: block;
|
|
798
1129
|
}
|
|
799
|
-
|
|
800
|
-
background: #f5f5f5;
|
|
801
|
-
padding: 10px;
|
|
802
|
-
margin-bottom: 8px;
|
|
803
|
-
border-radius: 4px;
|
|
804
|
-
flex-shrink: 0;
|
|
805
|
-
display: none;
|
|
806
|
-
}
|
|
807
|
-
.gg-filter-panel.expanded {
|
|
808
|
-
display: block;
|
|
809
|
-
}
|
|
810
|
-
.gg-filter-pattern {
|
|
811
|
-
width: 100%;
|
|
812
|
-
padding: 4px 8px;
|
|
813
|
-
font-family: monospace;
|
|
814
|
-
font-size: 16px;
|
|
815
|
-
margin-bottom: 8px;
|
|
816
|
-
}
|
|
817
|
-
.gg-filter-checkboxes {
|
|
1130
|
+
.gg-filter-checkboxes {
|
|
818
1131
|
display: flex;
|
|
819
1132
|
flex-wrap: wrap;
|
|
820
1133
|
gap: 8px;
|
|
@@ -997,11 +1310,6 @@ export function createGgPlugin(options, gg) {
|
|
|
997
1310
|
<span class="gg-btn-text">📋 <span class="gg-copy-count">Copy 0 entries</span></span>
|
|
998
1311
|
<span class="gg-btn-icon" title="Copy">📋</span>
|
|
999
1312
|
</button>
|
|
1000
|
-
<button class="gg-filter-btn" style="text-align: left; white-space: nowrap;">
|
|
1001
|
-
<span class="gg-btn-text">Namespaces: </span>
|
|
1002
|
-
<span class="gg-btn-icon">NS: </span>
|
|
1003
|
-
<span class="gg-filter-summary"></span>
|
|
1004
|
-
</button>
|
|
1005
1313
|
<button class="gg-expressions-btn" style="background: ${showExpressions ? '#e8f5e9' : 'transparent'};" title="Toggle expression visibility in logs and clipboard">
|
|
1006
1314
|
<span class="gg-btn-text">\uD83D\uDD0D Expr</span>
|
|
1007
1315
|
<span class="gg-btn-icon" title="Expressions">\uD83D\uDD0D</span>
|
|
@@ -1016,9 +1324,36 @@ export function createGgPlugin(options, gg) {
|
|
|
1016
1324
|
<span class="gg-btn-icon" title="Clear">⊘</span>
|
|
1017
1325
|
</button>
|
|
1018
1326
|
</div>
|
|
1019
|
-
|
|
1327
|
+
<div class="gg-pipeline">
|
|
1328
|
+
<span class="gg-pipeline-node gg-pipeline-recv" title="Total loggs received by gg"></span>
|
|
1329
|
+
<span class="gg-pipeline-arrow">→</span>
|
|
1330
|
+
<button class="gg-pipeline-handle gg-pipeline-keep-handle" title="Edit keep filter (Layer 1: ring buffer gate)">keep</button>
|
|
1331
|
+
<span class="gg-pipeline-arrow">→</span>
|
|
1332
|
+
<button class="gg-pipeline-node gg-pipeline-buf" title="Click to change buffer size"></button>
|
|
1333
|
+
<span class="gg-pipeline-arrow">→</span>
|
|
1334
|
+
<button class="gg-pipeline-handle gg-pipeline-show-handle" title="Edit show filter (Layer 2: display filter)">show</button>
|
|
1335
|
+
<span class="gg-pipeline-arrow">→</span>
|
|
1336
|
+
<span class="gg-pipeline-node gg-pipeline-vis" title="Loggs currently visible"></span>
|
|
1337
|
+
</div>
|
|
1338
|
+
<div class="gg-pipeline-panel gg-keep-panel" style="display:none;">
|
|
1339
|
+
<div class="gg-pipeline-panel-header">
|
|
1340
|
+
<span class="gg-filter-label">Keep:</span>
|
|
1341
|
+
<input class="gg-keep-input" type="text" value="${escapeHtml(keepPattern)}" placeholder="gg:*" title="gg-keep: which loggs enter the ring buffer">
|
|
1342
|
+
<span class="gg-keep-filter-summary gg-filter-count"></span>
|
|
1343
|
+
</div>
|
|
1344
|
+
<div class="gg-keep-filter-panel gg-filter-details-body"></div>
|
|
1345
|
+
</div>
|
|
1346
|
+
<div class="gg-pipeline-panel gg-show-panel" style="display:none;">
|
|
1347
|
+
<div class="gg-pipeline-panel-header">
|
|
1348
|
+
<span class="gg-filter-label">Show:</span>
|
|
1349
|
+
<input class="gg-show-input" type="text" value="${escapeHtml(filterPattern)}" placeholder="gg:*" title="gg-show: which kept loggs to display">
|
|
1350
|
+
<span class="gg-filter-summary gg-filter-count"></span>
|
|
1351
|
+
</div>
|
|
1352
|
+
<div class="gg-filter-panel gg-filter-details-body"></div>
|
|
1353
|
+
</div>
|
|
1020
1354
|
<div class="gg-settings-panel"></div>
|
|
1021
|
-
<div class="gg-
|
|
1355
|
+
<div class="gg-sentinel-section" style="display: none;"></div>
|
|
1356
|
+
|
|
1022
1357
|
<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>
|
|
1023
1358
|
<div class="gg-toast"></div>
|
|
1024
1359
|
<iframe class="gg-editor-iframe" hidden title="open-in-editor"></iframe>
|
|
@@ -1026,8 +1361,8 @@ export function createGgPlugin(options, gg) {
|
|
|
1026
1361
|
`;
|
|
1027
1362
|
}
|
|
1028
1363
|
function applyPatternFromInput(value) {
|
|
1029
|
-
filterPattern = value;
|
|
1030
|
-
localStorage.setItem(
|
|
1364
|
+
filterPattern = value || 'gg:*';
|
|
1365
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1031
1366
|
// Sync enabledNamespaces from the new pattern
|
|
1032
1367
|
const allNamespaces = getAllCapturedNamespaces();
|
|
1033
1368
|
enabledNamespaces.clear();
|
|
@@ -1037,41 +1372,62 @@ export function createGgPlugin(options, gg) {
|
|
|
1037
1372
|
enabledNamespaces.add(ns);
|
|
1038
1373
|
}
|
|
1039
1374
|
});
|
|
1375
|
+
// Sync toolbar Show input value
|
|
1376
|
+
if ($el) {
|
|
1377
|
+
const showInput = $el.find('.gg-show-input').get(0);
|
|
1378
|
+
if (showInput && document.activeElement !== showInput) {
|
|
1379
|
+
showInput.value = filterPattern;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1040
1382
|
renderFilterUI();
|
|
1383
|
+
renderSentinelSection();
|
|
1041
1384
|
renderLogs();
|
|
1042
1385
|
}
|
|
1386
|
+
function applyKeepPatternFromInput(value) {
|
|
1387
|
+
keepPattern = value || 'gg:*';
|
|
1388
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
1389
|
+
renderKeepUI();
|
|
1390
|
+
scheduleSentinelRender();
|
|
1391
|
+
}
|
|
1043
1392
|
function wireUpFilterUI() {
|
|
1044
1393
|
if (!$el)
|
|
1045
1394
|
return;
|
|
1046
|
-
const filterBtn = $el.find('.gg-filter-btn').get(0);
|
|
1047
1395
|
const filterPanel = $el.find('.gg-filter-panel').get(0);
|
|
1048
|
-
if (!
|
|
1396
|
+
if (!filterPanel)
|
|
1049
1397
|
return;
|
|
1050
1398
|
renderFilterUI();
|
|
1051
|
-
//
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1399
|
+
// Show handle toggles the show panel
|
|
1400
|
+
const showHandle = $el.find('.gg-pipeline-show-handle').get(0);
|
|
1401
|
+
const showPanel = $el.find('.gg-show-panel').get(0);
|
|
1402
|
+
if (showHandle && showPanel) {
|
|
1403
|
+
showHandle.addEventListener('click', () => {
|
|
1404
|
+
const open = showPanel.style.display !== 'none';
|
|
1405
|
+
showPanel.style.display = open ? 'none' : '';
|
|
1406
|
+
showHandle.classList.toggle('active', !open);
|
|
1407
|
+
// Close keep panel when opening show
|
|
1408
|
+
if (!open) {
|
|
1409
|
+
const keepPanel = $el?.find('.gg-keep-panel').get(0);
|
|
1410
|
+
const keepHandle = $el?.find('.gg-pipeline-keep-handle').get(0);
|
|
1411
|
+
if (keepPanel)
|
|
1412
|
+
keepPanel.style.display = 'none';
|
|
1413
|
+
if (keepHandle)
|
|
1414
|
+
keepHandle.classList.remove('active');
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
// Wire up the Show input (blur or Enter)
|
|
1419
|
+
const showInput = $el.find('.gg-show-input').get(0);
|
|
1420
|
+
if (showInput) {
|
|
1421
|
+
showInput.addEventListener('blur', () => {
|
|
1422
|
+
applyPatternFromInput(showInput.value);
|
|
1423
|
+
});
|
|
1424
|
+
showInput.addEventListener('keydown', (e) => {
|
|
1425
|
+
if (e.key === 'Enter') {
|
|
1426
|
+
applyPatternFromInput(showInput.value);
|
|
1427
|
+
showInput.blur();
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1075
1431
|
// Wire up checkboxes
|
|
1076
1432
|
filterPanel.addEventListener('change', (e) => {
|
|
1077
1433
|
const target = e.target;
|
|
@@ -1090,8 +1446,9 @@ export function createGgPlugin(options, gg) {
|
|
|
1090
1446
|
filterPattern = `gg:*,${exclusions}`;
|
|
1091
1447
|
enabledNamespaces.clear();
|
|
1092
1448
|
}
|
|
1093
|
-
localStorage.setItem(
|
|
1449
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1094
1450
|
renderFilterUI();
|
|
1451
|
+
renderSentinelSection();
|
|
1095
1452
|
renderLogs();
|
|
1096
1453
|
return;
|
|
1097
1454
|
}
|
|
@@ -1105,6 +1462,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1105
1462
|
toggleNamespaces(otherNamespaces, target.checked);
|
|
1106
1463
|
// localStorage already saved in toggleNamespaces()
|
|
1107
1464
|
renderFilterUI();
|
|
1465
|
+
renderSentinelSection();
|
|
1108
1466
|
renderLogs();
|
|
1109
1467
|
return;
|
|
1110
1468
|
}
|
|
@@ -1117,96 +1475,426 @@ export function createGgPlugin(options, gg) {
|
|
|
1117
1475
|
toggleNamespace(namespace, target.checked);
|
|
1118
1476
|
// Re-render to update UI
|
|
1119
1477
|
renderFilterUI();
|
|
1478
|
+
renderSentinelSection();
|
|
1120
1479
|
renderLogs();
|
|
1121
1480
|
}
|
|
1122
1481
|
});
|
|
1123
1482
|
}
|
|
1483
|
+
/** Update the three pipeline node labels with current counts. */
|
|
1484
|
+
function renderPipelineUI() {
|
|
1485
|
+
if (!$el)
|
|
1486
|
+
return;
|
|
1487
|
+
const keptNs = allNamespacesSet.size;
|
|
1488
|
+
const droppedNs = droppedNamespaces.size;
|
|
1489
|
+
const totalNs = keptNs + droppedNs;
|
|
1490
|
+
const visNs = enabledNamespaces.size;
|
|
1491
|
+
// recv node: "N total loggs" (no ns count — it moves to the keep button)
|
|
1492
|
+
const recvNode = $el.find('.gg-pipeline-recv').get(0);
|
|
1493
|
+
if (recvNode) {
|
|
1494
|
+
recvNode.textContent = `${receivedTotal} total loggs`;
|
|
1495
|
+
}
|
|
1496
|
+
// keep handle: "keep N/N namespaces" (kept ns / total ns ever seen)
|
|
1497
|
+
const keepHandle = $el.find('.gg-pipeline-keep-handle').get(0);
|
|
1498
|
+
if (keepHandle) {
|
|
1499
|
+
const countStr = totalNs ? ` ${keptNs}/${totalNs} namespaces` : '';
|
|
1500
|
+
// Preserve active class — only update text content
|
|
1501
|
+
keepHandle.textContent = `keep${countStr}`;
|
|
1502
|
+
}
|
|
1503
|
+
// buf node: buffer.size / buffer.capacity (no ns count — moved to keep button)
|
|
1504
|
+
const bufNode = $el.find('.gg-pipeline-buf').get(0);
|
|
1505
|
+
if (bufNode) {
|
|
1506
|
+
const bufSize = buffer.size;
|
|
1507
|
+
const bufCap = buffer.capacity;
|
|
1508
|
+
const full = bufSize >= bufCap;
|
|
1509
|
+
bufNode.textContent = full ? `${bufSize}/${bufCap} ⚠` : `${bufSize}/${bufCap}`;
|
|
1510
|
+
bufNode.style.color = full ? '#b94' : '';
|
|
1511
|
+
}
|
|
1512
|
+
// show handle: "show N/N namespaces" (visible ns / kept ns)
|
|
1513
|
+
const showHandle = $el.find('.gg-pipeline-show-handle').get(0);
|
|
1514
|
+
if (showHandle) {
|
|
1515
|
+
const countStr = keptNs ? ` ${visNs}/${keptNs} namespaces` : '';
|
|
1516
|
+
showHandle.textContent = `show${countStr}`;
|
|
1517
|
+
}
|
|
1518
|
+
// vis node: "N loggs shown" (no ns count — moved to show button)
|
|
1519
|
+
const visNode = $el.find('.gg-pipeline-vis').get(0);
|
|
1520
|
+
if (visNode) {
|
|
1521
|
+
visNode.textContent = `${filteredIndices.length} loggs shown`;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1124
1524
|
function renderFilterUI() {
|
|
1125
1525
|
if (!$el)
|
|
1126
1526
|
return;
|
|
1527
|
+
renderPipelineUI();
|
|
1127
1528
|
const allNamespaces = getAllCapturedNamespaces();
|
|
1128
1529
|
const enabledCount = enabledNamespaces.size;
|
|
1129
1530
|
const totalCount = allNamespaces.length;
|
|
1130
|
-
//
|
|
1531
|
+
// Sync input value (may have changed via hide/undo/right-click)
|
|
1532
|
+
const showInput = $el.find('.gg-show-input').get(0);
|
|
1533
|
+
if (showInput && document.activeElement !== showInput)
|
|
1534
|
+
showInput.value = filterPattern;
|
|
1535
|
+
// Update count in summary
|
|
1131
1536
|
const filterSummary = $el.find('.gg-filter-summary').get(0);
|
|
1132
|
-
if (filterSummary)
|
|
1537
|
+
if (filterSummary)
|
|
1133
1538
|
filterSummary.textContent = `${enabledCount}/${totalCount}`;
|
|
1134
|
-
|
|
1135
|
-
// Update panel
|
|
1539
|
+
// Always render panel body — <details> open state handles visibility
|
|
1136
1540
|
const filterPanel = $el.find('.gg-filter-panel').get(0);
|
|
1137
1541
|
if (!filterPanel)
|
|
1138
1542
|
return;
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const
|
|
1144
|
-
const
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
</label>
|
|
1172
|
-
${displayedNamespaces
|
|
1173
|
-
.map((ns) => {
|
|
1174
|
-
// Check if namespace matches the current pattern
|
|
1175
|
-
const checked = namespaceMatchesPattern(ns, effectivePattern);
|
|
1176
|
-
const count = nsCounts.get(ns) || 0;
|
|
1177
|
-
return `
|
|
1178
|
-
<label class="gg-filter-checkbox">
|
|
1179
|
-
<input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}>
|
|
1180
|
-
<span>${escapeHtml(ns)} (${count})</span>
|
|
1181
|
-
</label>
|
|
1182
|
-
`;
|
|
1183
|
-
})
|
|
1184
|
-
.join('')}
|
|
1185
|
-
${otherTotalCount > 0
|
|
1186
|
-
? `
|
|
1187
|
-
<label class="gg-filter-checkbox" style="opacity: 0.7;">
|
|
1188
|
-
<input type="checkbox" class="gg-other-checkbox" ${otherChecked ? 'checked' : ''} data-other-namespaces='${JSON.stringify(otherNamespaces)}'>
|
|
1189
|
-
<span>other (${otherCount})</span>
|
|
1190
|
-
</label>
|
|
1191
|
-
`
|
|
1192
|
-
: ''}
|
|
1193
|
-
</div>
|
|
1194
|
-
`;
|
|
1195
|
-
}
|
|
1196
|
-
else if (!simple) {
|
|
1197
|
-
checkboxesHTML = `<div style="opacity: 0.6; font-size: 11px; margin: 8px 0;">⚠️ Complex pattern - edit manually (quick filters disabled)</div>`;
|
|
1198
|
-
}
|
|
1199
|
-
filterPanel.innerHTML = `
|
|
1200
|
-
<div style="margin-bottom: 8px;">
|
|
1201
|
-
<input type="text" class="gg-filter-pattern" value="${escapeHtml(filterPattern)}" placeholder="gg:*" style="width: 100%;">
|
|
1202
|
-
</div>
|
|
1203
|
-
${checkboxesHTML}
|
|
1204
|
-
`;
|
|
1543
|
+
const simple = isSimplePattern(filterPattern);
|
|
1544
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
1545
|
+
if (simple && allNamespaces.length > 0) {
|
|
1546
|
+
const allChecked = enabledCount === totalCount;
|
|
1547
|
+
const allEntries = buffer.getEntries();
|
|
1548
|
+
const nsCounts = new Map();
|
|
1549
|
+
allEntries.forEach((entry) => {
|
|
1550
|
+
nsCounts.set(entry.namespace, (nsCounts.get(entry.namespace) || 0) + 1);
|
|
1551
|
+
});
|
|
1552
|
+
const sortedAll = [...allNamespaces].sort((a, b) => (nsCounts.get(b) || 0) - (nsCounts.get(a) || 0));
|
|
1553
|
+
const displayed = sortedAll.slice(0, 5);
|
|
1554
|
+
const displayedSet = new Set(displayed);
|
|
1555
|
+
const others = allNamespaces.filter((ns) => !displayedSet.has(ns));
|
|
1556
|
+
const otherChecked = others.some((ns) => enabledNamespaces.has(ns));
|
|
1557
|
+
const otherCount = others.reduce((sum, ns) => sum + (nsCounts.get(ns) || 0), 0);
|
|
1558
|
+
filterPanel.innerHTML =
|
|
1559
|
+
`<div style="font-size: 11px; opacity: 0.6; margin-bottom: 6px;">Layer 2: controls which kept loggs are displayed.</div>` +
|
|
1560
|
+
`<div class="gg-filter-checkboxes">` +
|
|
1561
|
+
`<label class="gg-filter-checkbox" style="font-weight: bold;"><input type="checkbox" class="gg-all-checkbox" ${allChecked ? 'checked' : ''}><span>ALL</span></label>` +
|
|
1562
|
+
displayed
|
|
1563
|
+
.map((ns) => {
|
|
1564
|
+
const checked = namespaceMatchesPattern(ns, effectivePattern);
|
|
1565
|
+
return `<label class="gg-filter-checkbox"><input type="checkbox" class="gg-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}><span>${escapeHtml(ns)} (${nsCounts.get(ns) || 0})</span></label>`;
|
|
1566
|
+
})
|
|
1567
|
+
.join('') +
|
|
1568
|
+
(others.length > 0
|
|
1569
|
+
? `<label class="gg-filter-checkbox" style="opacity: 0.7;"><input type="checkbox" class="gg-other-checkbox" ${otherChecked ? 'checked' : ''} data-other-namespaces='${JSON.stringify(others)}'><span>other (${otherCount})</span></label>`
|
|
1570
|
+
: '') +
|
|
1571
|
+
`</div>`;
|
|
1572
|
+
}
|
|
1573
|
+
else if (!simple) {
|
|
1574
|
+
filterPanel.innerHTML = `<div style="opacity: 0.6; font-size: 11px;">⚠️ Complex pattern — edit directly in the input above</div>`;
|
|
1205
1575
|
}
|
|
1206
1576
|
else {
|
|
1207
|
-
|
|
1208
|
-
|
|
1577
|
+
filterPanel.innerHTML = '';
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
/** Render the Keep filter UI (count + panel body) */
|
|
1581
|
+
function renderKeepUI() {
|
|
1582
|
+
if (!$el)
|
|
1583
|
+
return;
|
|
1584
|
+
renderPipelineUI();
|
|
1585
|
+
const droppedCount = droppedNamespaces.size;
|
|
1586
|
+
const keptCount = allNamespacesSet.size;
|
|
1587
|
+
const totalCount = keptCount + droppedCount;
|
|
1588
|
+
// Sync input value
|
|
1589
|
+
const keepInput = $el.find('.gg-keep-input').get(0);
|
|
1590
|
+
if (keepInput && document.activeElement !== keepInput)
|
|
1591
|
+
keepInput.value = keepPattern;
|
|
1592
|
+
// Update count in summary (keep panel header)
|
|
1593
|
+
const keepSummary = $el.find('.gg-keep-filter-summary').get(0);
|
|
1594
|
+
if (keepSummary)
|
|
1595
|
+
keepSummary.textContent = `${keptCount}/${totalCount}`;
|
|
1596
|
+
// Always render panel body — <details> open state handles visibility
|
|
1597
|
+
const keepPanel = $el.find('.gg-keep-filter-panel').get(0);
|
|
1598
|
+
if (!keepPanel)
|
|
1599
|
+
return;
|
|
1600
|
+
const simple = isSimplePattern(keepPattern);
|
|
1601
|
+
const allKept = [...allNamespacesSet].sort();
|
|
1602
|
+
const allDropped = [...droppedNamespaces.keys()].sort();
|
|
1603
|
+
// Also extract namespaces explicitly excluded in keepPattern itself — these may
|
|
1604
|
+
// never have sent a logg so they won't be in allNamespacesSet or droppedNamespaces
|
|
1605
|
+
const patternExcluded = (keepPattern || 'gg:*')
|
|
1606
|
+
.split(',')
|
|
1607
|
+
.map((p) => p.trim())
|
|
1608
|
+
.filter((p) => p.startsWith('-') && p.length > 1)
|
|
1609
|
+
.map((p) => p.slice(1));
|
|
1610
|
+
const allNs = [...new Set([...allKept, ...allDropped, ...patternExcluded])];
|
|
1611
|
+
if (simple && allNs.length > 0) {
|
|
1612
|
+
const allChecked = droppedCount === 0;
|
|
1613
|
+
const effectiveKeep = keepPattern || 'gg:*';
|
|
1614
|
+
// Count loggs per namespace (kept + dropped combined for context)
|
|
1615
|
+
const allEntries = buffer.getEntries();
|
|
1616
|
+
const nsCounts = new Map();
|
|
1617
|
+
allEntries.forEach((entry) => {
|
|
1618
|
+
nsCounts.set(entry.namespace, (nsCounts.get(entry.namespace) || 0) + 1);
|
|
1619
|
+
});
|
|
1620
|
+
// Also add dropped counts
|
|
1621
|
+
droppedNamespaces.forEach((info, ns) => {
|
|
1622
|
+
nsCounts.set(ns, (nsCounts.get(ns) || 0) + info.total);
|
|
1623
|
+
});
|
|
1624
|
+
const sorted = allNs.sort((a, b) => (nsCounts.get(b) || 0) - (nsCounts.get(a) || 0));
|
|
1625
|
+
const displayed = sorted.slice(0, 5);
|
|
1626
|
+
const displayedSet = new Set(displayed);
|
|
1627
|
+
const others = allNs.filter((ns) => !displayedSet.has(ns));
|
|
1628
|
+
const otherKept = others.filter((ns) => namespaceMatchesPattern(ns, effectiveKeep));
|
|
1629
|
+
const otherCount = others.reduce((sum, ns) => sum + (nsCounts.get(ns) || 0), 0);
|
|
1630
|
+
keepPanel.innerHTML =
|
|
1631
|
+
`<div style="font-size: 11px; opacity: 0.6; margin-bottom: 6px;">Layer 1: controls which loggs enter the ring buffer.</div>` +
|
|
1632
|
+
`<div class="gg-filter-checkboxes">` +
|
|
1633
|
+
`<label class="gg-filter-checkbox" style="font-weight: bold;"><input type="checkbox" class="gg-keep-all-checkbox" ${allChecked ? 'checked' : ''}><span>ALL</span></label>` +
|
|
1634
|
+
displayed
|
|
1635
|
+
.map((ns) => {
|
|
1636
|
+
const checked = namespaceMatchesPattern(ns, effectiveKeep);
|
|
1637
|
+
return `<label class="gg-filter-checkbox"><input type="checkbox" class="gg-keep-ns-checkbox" data-namespace="${escapeHtml(ns)}" ${checked ? 'checked' : ''}><span>${escapeHtml(ns)} (${nsCounts.get(ns) || 0})</span></label>`;
|
|
1638
|
+
})
|
|
1639
|
+
.join('') +
|
|
1640
|
+
(others.length > 0
|
|
1641
|
+
? `<label class="gg-filter-checkbox" style="opacity: 0.7;"><input type="checkbox" class="gg-keep-other-checkbox" ${otherKept.length > 0 ? 'checked' : ''} data-other-namespaces='${JSON.stringify(others)}'><span>other (${otherCount})</span></label>`
|
|
1642
|
+
: '') +
|
|
1643
|
+
`</div>`;
|
|
1644
|
+
}
|
|
1645
|
+
else if (!simple) {
|
|
1646
|
+
keepPanel.innerHTML = `<div style="opacity: 0.6; font-size: 11px;">⚠️ Complex pattern — edit directly in the input above</div>`;
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
keepPanel.innerHTML = '';
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
/** Schedule a debounced re-render of the sentinel section (via rAF) */
|
|
1653
|
+
function scheduleSentinelRender() {
|
|
1654
|
+
if (sentinelRenderPending)
|
|
1655
|
+
return;
|
|
1656
|
+
sentinelRenderPending = true;
|
|
1657
|
+
requestAnimationFrame(() => {
|
|
1658
|
+
sentinelRenderPending = false;
|
|
1659
|
+
renderSentinelSection();
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
/** Render the dropped-namespace sentinel section above the log container */
|
|
1663
|
+
function renderSentinelSection() {
|
|
1664
|
+
if (!$el)
|
|
1665
|
+
return;
|
|
1666
|
+
const sentinelSection = $el.find('.gg-sentinel-section').get(0);
|
|
1667
|
+
if (!sentinelSection)
|
|
1668
|
+
return;
|
|
1669
|
+
if (droppedNamespaces.size === 0) {
|
|
1670
|
+
sentinelSection.style.display = 'none';
|
|
1671
|
+
sentinelSection.innerHTML = '';
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
// Sort by total dropped count descending (noisiest first)
|
|
1675
|
+
// Also respect filterPattern (Layer 2 show filter) — sentinels for hidden namespaces
|
|
1676
|
+
// are themselves hidden, consistent with how filtered log entries are hidden.
|
|
1677
|
+
const effectiveShowPattern = filterPattern || 'gg:*';
|
|
1678
|
+
const sorted = [...droppedNamespaces.values()]
|
|
1679
|
+
.filter((info) => namespaceMatchesPattern(info.namespace, effectiveShowPattern))
|
|
1680
|
+
.sort((a, b) => b.total - a.total);
|
|
1681
|
+
if (sorted.length === 0) {
|
|
1682
|
+
sentinelSection.style.display = 'none';
|
|
1683
|
+
sentinelSection.innerHTML = '';
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const total = sorted.reduce((sum, i) => sum + i.total, 0);
|
|
1687
|
+
// Header: "▼ Dropped: 3 namespaces, 47 loggs" — click to collapse
|
|
1688
|
+
const arrow = sentinelExpanded ? '▼' : '▶';
|
|
1689
|
+
const headerText = `${sorted.length} dropped namespace${sorted.length === 1 ? '' : 's'}, ${total} logg${total === 1 ? '' : 's'}`;
|
|
1690
|
+
let html = `<div class="gg-sentinel-header"><span class="gg-sentinel-toggle">${arrow}</span> ${escapeHtml(headerText)}</div>`;
|
|
1691
|
+
html += `<div class="gg-sentinel-rows${sentinelExpanded ? '' : ' collapsed'}">`;
|
|
1692
|
+
for (const info of sorted) {
|
|
1693
|
+
const ns = escapeHtml(info.namespace);
|
|
1694
|
+
const typeEntries = Object.entries(info.byType);
|
|
1695
|
+
const breakdown = typeEntries.length > 1 ? ` (${typeEntries.map(([t, n]) => `${n} ${t}`).join(', ')})` : '';
|
|
1696
|
+
const countStr = `${info.total} logg${info.total === 1 ? '' : 's'}${breakdown}`;
|
|
1697
|
+
let previewStr = '';
|
|
1698
|
+
if (info.preview.args && info.preview.args.length > 0) {
|
|
1699
|
+
const raw = info.preview.args
|
|
1700
|
+
.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a)))
|
|
1701
|
+
.join(' ');
|
|
1702
|
+
previewStr = raw.length > 80 ? raw.slice(0, 77) + '...' : raw;
|
|
1703
|
+
}
|
|
1704
|
+
html +=
|
|
1705
|
+
`<div class="gg-sentinel-row" data-namespace="${ns}">` +
|
|
1706
|
+
`<button class="gg-sentinel-keep" data-namespace="${ns}" title="Keep this namespace (start capturing its loggs)">${ICON_KEEP}</button>` +
|
|
1707
|
+
`<span class="gg-sentinel-ns">DROPPED:${ns}</span>` +
|
|
1708
|
+
`<span class="gg-sentinel-count">${escapeHtml(countStr)}</span>` +
|
|
1709
|
+
(previewStr
|
|
1710
|
+
? `<span class="gg-sentinel-preview">\u21b3 ${escapeHtml(previewStr)}</span>`
|
|
1711
|
+
: '') +
|
|
1712
|
+
`</div>`;
|
|
1209
1713
|
}
|
|
1714
|
+
html += `</div>`;
|
|
1715
|
+
sentinelSection.innerHTML = html;
|
|
1716
|
+
sentinelSection.style.display = 'block';
|
|
1717
|
+
// Update keep UI summary too
|
|
1718
|
+
renderKeepUI();
|
|
1719
|
+
}
|
|
1720
|
+
/** Wire up the Keep input + sentinel section */
|
|
1721
|
+
function wireUpKeepUI() {
|
|
1722
|
+
if (!$el)
|
|
1723
|
+
return;
|
|
1724
|
+
// Keep handle toggles the keep panel
|
|
1725
|
+
const keepHandle = $el.find('.gg-pipeline-keep-handle').get(0);
|
|
1726
|
+
const keepPanelEl = $el.find('.gg-keep-panel').get(0);
|
|
1727
|
+
if (keepHandle && keepPanelEl) {
|
|
1728
|
+
keepHandle.addEventListener('click', () => {
|
|
1729
|
+
const open = keepPanelEl.style.display !== 'none';
|
|
1730
|
+
keepPanelEl.style.display = open ? 'none' : '';
|
|
1731
|
+
keepHandle.classList.toggle('active', !open);
|
|
1732
|
+
// Close show panel when opening keep
|
|
1733
|
+
if (!open) {
|
|
1734
|
+
const showPanel = $el?.find('.gg-show-panel').get(0);
|
|
1735
|
+
const showHandle = $el?.find('.gg-pipeline-show-handle').get(0);
|
|
1736
|
+
if (showPanel)
|
|
1737
|
+
showPanel.style.display = 'none';
|
|
1738
|
+
if (showHandle)
|
|
1739
|
+
showHandle.classList.remove('active');
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
// Buf node click: replace node text with an inline input to change capacity
|
|
1744
|
+
const bufNode = $el.find('.gg-pipeline-buf').get(0);
|
|
1745
|
+
if (bufNode) {
|
|
1746
|
+
bufNode.addEventListener('click', () => {
|
|
1747
|
+
// Already editing?
|
|
1748
|
+
if (bufNode.querySelector('.gg-buf-size-input'))
|
|
1749
|
+
return;
|
|
1750
|
+
const current = buffer.capacity;
|
|
1751
|
+
const input = document.createElement('input');
|
|
1752
|
+
input.type = 'number';
|
|
1753
|
+
input.className = 'gg-buf-size-input';
|
|
1754
|
+
input.value = String(current);
|
|
1755
|
+
input.min = '100';
|
|
1756
|
+
input.max = '100000';
|
|
1757
|
+
input.title = 'Buffer capacity (Enter to apply, Escape to cancel)';
|
|
1758
|
+
bufNode.textContent = '';
|
|
1759
|
+
bufNode.appendChild(input);
|
|
1760
|
+
input.focus();
|
|
1761
|
+
input.select();
|
|
1762
|
+
const restore = () => {
|
|
1763
|
+
input.remove();
|
|
1764
|
+
renderPipelineUI(); // restores text content
|
|
1765
|
+
};
|
|
1766
|
+
const apply = () => {
|
|
1767
|
+
const val = parseInt(input.value, 10);
|
|
1768
|
+
if (!isNaN(val) && val > 0 && val !== current) {
|
|
1769
|
+
buffer.resize(val);
|
|
1770
|
+
localStorage.setItem(BUFFER_CAP_KEY, String(val));
|
|
1771
|
+
renderLogs();
|
|
1772
|
+
}
|
|
1773
|
+
restore();
|
|
1774
|
+
};
|
|
1775
|
+
input.addEventListener('blur', apply);
|
|
1776
|
+
input.addEventListener('keydown', (e) => {
|
|
1777
|
+
if (e.key === 'Enter') {
|
|
1778
|
+
e.preventDefault();
|
|
1779
|
+
input.blur();
|
|
1780
|
+
}
|
|
1781
|
+
if (e.key === 'Escape') {
|
|
1782
|
+
input.removeEventListener('blur', apply);
|
|
1783
|
+
restore();
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
// Stop click from immediately re-triggering
|
|
1787
|
+
input.addEventListener('click', (e) => e.stopPropagation());
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
const keepInput = $el.find('.gg-keep-input').get(0);
|
|
1791
|
+
if (keepInput) {
|
|
1792
|
+
keepInput.addEventListener('blur', () => {
|
|
1793
|
+
applyKeepPatternFromInput(keepInput.value);
|
|
1794
|
+
});
|
|
1795
|
+
keepInput.addEventListener('keydown', (e) => {
|
|
1796
|
+
if (e.key === 'Enter') {
|
|
1797
|
+
applyKeepPatternFromInput(keepInput.value);
|
|
1798
|
+
keepInput.blur();
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
// Wire up sentinel section: collapse toggle + [+] keep buttons
|
|
1803
|
+
const sentinelSection = $el.find('.gg-sentinel-section').get(0);
|
|
1804
|
+
if (sentinelSection) {
|
|
1805
|
+
sentinelSection.addEventListener('click', (e) => {
|
|
1806
|
+
const target = e.target;
|
|
1807
|
+
// Header click: toggle collapse
|
|
1808
|
+
if (target?.closest?.('.gg-sentinel-header')) {
|
|
1809
|
+
sentinelExpanded = !sentinelExpanded;
|
|
1810
|
+
renderSentinelSection();
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const keepBtn = target?.closest?.('.gg-sentinel-keep');
|
|
1814
|
+
if (keepBtn) {
|
|
1815
|
+
const namespace = keepBtn.getAttribute('data-namespace');
|
|
1816
|
+
if (!namespace)
|
|
1817
|
+
return;
|
|
1818
|
+
const info = droppedNamespaces.get(namespace);
|
|
1819
|
+
if (!info)
|
|
1820
|
+
return;
|
|
1821
|
+
const previousKeep = keepPattern || 'gg:*';
|
|
1822
|
+
// Remove the exact exclusion for this namespace from keepPattern.
|
|
1823
|
+
// If no exclusion exists (Case B: namespace simply not included), add it.
|
|
1824
|
+
const currentKeep = keepPattern || 'gg:*';
|
|
1825
|
+
const exclusion = `-${namespace}`;
|
|
1826
|
+
const parts = currentKeep
|
|
1827
|
+
.split(',')
|
|
1828
|
+
.map((p) => p.trim())
|
|
1829
|
+
.filter(Boolean);
|
|
1830
|
+
const hasExclusion = parts.includes(exclusion);
|
|
1831
|
+
if (hasExclusion) {
|
|
1832
|
+
keepPattern = parts.filter((p) => p !== exclusion).join(',') || 'gg:*';
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
// No explicit exclusion — add an inclusion for the exact namespace
|
|
1836
|
+
keepPattern = parts.some((p) => !p.startsWith('-'))
|
|
1837
|
+
? `${currentKeep},${namespace}`
|
|
1838
|
+
: `gg:*,${namespace}`;
|
|
1839
|
+
}
|
|
1840
|
+
keepPattern = simplifyPattern(keepPattern);
|
|
1841
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
1842
|
+
// Sync keep input
|
|
1843
|
+
if (keepInput)
|
|
1844
|
+
keepInput.value = keepPattern;
|
|
1845
|
+
// Remove from droppedNamespaces map so sentinel disappears immediately
|
|
1846
|
+
droppedNamespaces.delete(namespace);
|
|
1847
|
+
renderKeepUI();
|
|
1848
|
+
renderSentinelSection();
|
|
1849
|
+
// Show keep toast for undo / segment broadening
|
|
1850
|
+
showKeepToast(namespace, previousKeep, info);
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
// Wire up Keep panel checkboxes
|
|
1855
|
+
const keepPanel = $el.find('.gg-keep-filter-panel').get(0);
|
|
1856
|
+
if (keepPanel) {
|
|
1857
|
+
keepPanel.addEventListener('change', (e) => {
|
|
1858
|
+
const target = e.target;
|
|
1859
|
+
if (target.classList.contains('gg-keep-all-checkbox')) {
|
|
1860
|
+
const allNs = [...allNamespacesSet, ...droppedNamespaces.keys()];
|
|
1861
|
+
if (target.checked) {
|
|
1862
|
+
// Keep all: remove all exclusions
|
|
1863
|
+
keepPattern = 'gg:*';
|
|
1864
|
+
droppedNamespaces.clear();
|
|
1865
|
+
}
|
|
1866
|
+
else {
|
|
1867
|
+
// Drop all
|
|
1868
|
+
const exclusions = allNs.map((ns) => `-${ns}`).join(',');
|
|
1869
|
+
keepPattern = `gg:*,${exclusions}`;
|
|
1870
|
+
keepPattern = simplifyPattern(keepPattern) || 'gg:*';
|
|
1871
|
+
}
|
|
1872
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
1873
|
+
renderKeepUI();
|
|
1874
|
+
renderLogs();
|
|
1875
|
+
renderSentinelSection();
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (target.classList.contains('gg-keep-other-checkbox')) {
|
|
1879
|
+
const otherNs = JSON.parse(target.getAttribute('data-other-namespaces') || '[]');
|
|
1880
|
+
otherNs.forEach((ns) => toggleKeepNamespace(ns, target.checked));
|
|
1881
|
+
renderKeepUI();
|
|
1882
|
+
renderLogs();
|
|
1883
|
+
renderSentinelSection();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (target.classList.contains('gg-keep-ns-checkbox')) {
|
|
1887
|
+
const namespace = target.getAttribute('data-namespace');
|
|
1888
|
+
if (!namespace)
|
|
1889
|
+
return;
|
|
1890
|
+
toggleKeepNamespace(namespace, target.checked);
|
|
1891
|
+
renderKeepUI();
|
|
1892
|
+
renderLogs();
|
|
1893
|
+
renderSentinelSection();
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
renderKeepUI();
|
|
1210
1898
|
}
|
|
1211
1899
|
/** Render the format field + $ROOT field shared by "Copy to clipboard" and "Open as URL" */
|
|
1212
1900
|
function renderFormatSection(isUrlMode) {
|
|
@@ -1277,34 +1965,17 @@ export function createGgPlugin(options, gg) {
|
|
|
1277
1965
|
optionsSection = renderFormatSection(nsClickAction === 'open-url');
|
|
1278
1966
|
}
|
|
1279
1967
|
// Native Console section
|
|
1280
|
-
const currentDebugValue = localStorage.getItem('debug');
|
|
1281
|
-
const debugDisplay = currentDebugValue !== null ? `'${escapeHtml(currentDebugValue)}'` : 'not set';
|
|
1282
|
-
const currentFilter = filterPattern || 'gg:*';
|
|
1283
|
-
const debugMatchesFilter = currentDebugValue === currentFilter;
|
|
1284
|
-
const debugIncludesGg = currentDebugValue !== null &&
|
|
1285
|
-
(currentDebugValue.includes('gg:') || currentDebugValue === '*');
|
|
1286
1968
|
const nativeConsoleSection = `
|
|
1287
1969
|
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #ddd;">
|
|
1288
1970
|
<div class="gg-settings-label">Native Console Output</div>
|
|
1289
1971
|
<div style="font-size: 11px; opacity: 0.7; margin-bottom: 8px;">
|
|
1290
|
-
|
|
1291
|
-
For server-side: <code>DEBUG=gg:* npm run dev</code>
|
|
1292
|
-
</div>
|
|
1293
|
-
<div style="font-family: monospace; font-size: 12px; margin-bottom: 6px;">
|
|
1294
|
-
localStorage.debug = ${debugDisplay}
|
|
1295
|
-
${debugIncludesGg ? '<span style="color: green;">✅</span>' : '<span style="color: #999;">⚫ gg:* not included</span>'}
|
|
1296
|
-
</div>
|
|
1297
|
-
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
|
|
1298
|
-
<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' : ''}>
|
|
1299
|
-
${debugMatchesFilter ? 'In sync' : `Set to '${escapeHtml(currentFilter)}'`}
|
|
1300
|
-
</button>
|
|
1301
|
-
<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' : ''}>
|
|
1302
|
-
Clear
|
|
1303
|
-
</button>
|
|
1304
|
-
</div>
|
|
1305
|
-
<div style="font-size: 10px; opacity: 0.5; margin-top: 4px;">
|
|
1306
|
-
Changes take effect on next page reload.
|
|
1972
|
+
When enabled, loggs shown in the GG panel are also output to the browser's native console (filtered by the Show pattern).
|
|
1307
1973
|
</div>
|
|
1974
|
+
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px;">
|
|
1975
|
+
<input type="checkbox" class="gg-console-toggle" ${ggConsoleEnabled ? 'checked' : ''}>
|
|
1976
|
+
Native console output
|
|
1977
|
+
</label>
|
|
1978
|
+
${ggConsoleEnabled ? `<div style="font-size: 10px; opacity: 0.5; margin-top: 4px;">Disable to silence gg loggs in DevTools console.</div>` : ''}
|
|
1308
1979
|
</div>
|
|
1309
1980
|
`;
|
|
1310
1981
|
settingsPanel.innerHTML = `
|
|
@@ -1343,7 +2014,6 @@ export function createGgPlugin(options, gg) {
|
|
|
1343
2014
|
settingsBtn.addEventListener('click', () => {
|
|
1344
2015
|
settingsExpanded = !settingsExpanded;
|
|
1345
2016
|
if (settingsExpanded) {
|
|
1346
|
-
filterExpanded = false;
|
|
1347
2017
|
renderFilterUI();
|
|
1348
2018
|
renderLogs();
|
|
1349
2019
|
}
|
|
@@ -1403,15 +2073,21 @@ export function createGgPlugin(options, gg) {
|
|
|
1403
2073
|
renderSettingsUI();
|
|
1404
2074
|
}
|
|
1405
2075
|
}
|
|
1406
|
-
// Native Console:
|
|
1407
|
-
if (target.classList.contains('gg-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
2076
|
+
// Native Console: gg-console toggle
|
|
2077
|
+
if (target.classList.contains('gg-console-toggle')) {
|
|
2078
|
+
const checked = target.checked;
|
|
2079
|
+
ggConsoleEnabled = checked;
|
|
2080
|
+
localStorage.setItem(CONSOLE_KEY, String(checked));
|
|
2081
|
+
// Update the debug factory's enabled pattern immediately
|
|
2082
|
+
import('../debug/index.js').then(({ default: dbg }) => {
|
|
2083
|
+
try {
|
|
2084
|
+
const pattern = ggConsoleEnabled ? filterPattern || 'gg:*' : '';
|
|
2085
|
+
dbg.enable(pattern);
|
|
2086
|
+
}
|
|
2087
|
+
catch {
|
|
2088
|
+
// ignore
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
1415
2091
|
renderSettingsUI();
|
|
1416
2092
|
}
|
|
1417
2093
|
});
|
|
@@ -1422,7 +2098,10 @@ export function createGgPlugin(options, gg) {
|
|
|
1422
2098
|
$el.find('.gg-clear-btn').on('click', () => {
|
|
1423
2099
|
buffer.clear();
|
|
1424
2100
|
allNamespacesSet.clear();
|
|
2101
|
+
droppedNamespaces.clear();
|
|
1425
2102
|
renderLogs();
|
|
2103
|
+
renderSentinelSection();
|
|
2104
|
+
renderKeepUI();
|
|
1426
2105
|
});
|
|
1427
2106
|
$el.find('.gg-copy-btn').on('click', async () => {
|
|
1428
2107
|
const allEntries = buffer.getEntries();
|
|
@@ -1507,14 +2186,8 @@ export function createGgPlugin(options, gg) {
|
|
|
1507
2186
|
}
|
|
1508
2187
|
}
|
|
1509
2188
|
/** Show toast bar after hiding a namespace via the x button */
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
return;
|
|
1513
|
-
lastHiddenPattern = previousPattern;
|
|
1514
|
-
const toast = $el.find('.gg-toast').get(0);
|
|
1515
|
-
if (!toast)
|
|
1516
|
-
return;
|
|
1517
|
-
// Split namespace into segments with delimiters (same logic as log row rendering)
|
|
2189
|
+
/** Build clickable segment HTML for a namespace (shared by hide/drop toasts) */
|
|
2190
|
+
function buildToastNsHTML(namespace) {
|
|
1518
2191
|
const parts = namespace.split(/([:/@ \-_])/);
|
|
1519
2192
|
const segments = [];
|
|
1520
2193
|
const delimiters = [];
|
|
@@ -1527,27 +2200,31 @@ export function createGgPlugin(options, gg) {
|
|
|
1527
2200
|
delimiters.push(parts[i]);
|
|
1528
2201
|
}
|
|
1529
2202
|
}
|
|
1530
|
-
// Build clickable segment HTML
|
|
1531
2203
|
let nsHTML = '';
|
|
1532
2204
|
for (let i = 0; i < segments.length; i++) {
|
|
1533
|
-
const segment = escapeHtml(segments[i]);
|
|
1534
|
-
// Build filter pattern for this segment level
|
|
1535
2205
|
let segFilter = '';
|
|
1536
2206
|
for (let j = 0; j <= i; j++) {
|
|
1537
2207
|
segFilter += segments[j];
|
|
1538
|
-
if (j < i)
|
|
2208
|
+
if (j < i)
|
|
1539
2209
|
segFilter += delimiters[j];
|
|
1540
|
-
|
|
1541
|
-
else if (j < segments.length - 1) {
|
|
2210
|
+
else if (j < segments.length - 1)
|
|
1542
2211
|
segFilter += delimiters[j] + '*';
|
|
1543
|
-
}
|
|
1544
2212
|
}
|
|
1545
|
-
nsHTML += `<span class="gg-toast-segment" data-filter="${escapeHtml(segFilter)}">${
|
|
1546
|
-
if (i < segments.length - 1)
|
|
2213
|
+
nsHTML += `<span class="gg-toast-segment" data-filter="${escapeHtml(segFilter)}">${escapeHtml(segments[i])}</span>`;
|
|
2214
|
+
if (i < segments.length - 1)
|
|
1547
2215
|
nsHTML += `<span class="gg-toast-delim">${escapeHtml(delimiters[i])}</span>`;
|
|
1548
|
-
}
|
|
1549
2216
|
}
|
|
1550
|
-
|
|
2217
|
+
return nsHTML;
|
|
2218
|
+
}
|
|
2219
|
+
function showHideToast(namespace, previousPattern) {
|
|
2220
|
+
if (!$el)
|
|
2221
|
+
return;
|
|
2222
|
+
toastMode = 'hide';
|
|
2223
|
+
lastHiddenPattern = previousPattern;
|
|
2224
|
+
const toast = $el.find('.gg-toast').get(0);
|
|
2225
|
+
if (!toast)
|
|
2226
|
+
return;
|
|
2227
|
+
const nsHTML = buildToastNsHTML(namespace);
|
|
1551
2228
|
const showExplanation = !hasSeenToastExplanation;
|
|
1552
2229
|
toast.innerHTML =
|
|
1553
2230
|
`<button class="gg-toast-btn gg-toast-dismiss" title="Dismiss">\u00d7</button>` +
|
|
@@ -1558,13 +2235,64 @@ export function createGgPlugin(options, gg) {
|
|
|
1558
2235
|
`<button class="gg-toast-btn gg-toast-help" title="Toggle help">?</button>` +
|
|
1559
2236
|
`</span>` +
|
|
1560
2237
|
`<div class="gg-toast-explanation${showExplanation ? ' visible' : ''}">` +
|
|
1561
|
-
`Click a segment above to hide all matching namespaces (e.g. click "api" to hide
|
|
2238
|
+
`Click a segment above to hide all matching namespaces (e.g. click "api" to hide api/*). ` +
|
|
1562
2239
|
`Tip: you can also right-click any segment in the log to hide it directly.` +
|
|
1563
2240
|
`</div>`;
|
|
1564
2241
|
toast.classList.add('visible');
|
|
1565
|
-
if (showExplanation)
|
|
2242
|
+
if (showExplanation)
|
|
2243
|
+
hasSeenToastExplanation = true;
|
|
2244
|
+
}
|
|
2245
|
+
function showDropToast(namespace, previousKeep) {
|
|
2246
|
+
if (!$el)
|
|
2247
|
+
return;
|
|
2248
|
+
toastMode = 'drop';
|
|
2249
|
+
lastDroppedPattern = previousKeep;
|
|
2250
|
+
const toast = $el.find('.gg-toast').get(0);
|
|
2251
|
+
if (!toast)
|
|
2252
|
+
return;
|
|
2253
|
+
const nsHTML = buildToastNsHTML(namespace);
|
|
2254
|
+
const showExplanation = !hasSeenToastExplanation;
|
|
2255
|
+
toast.innerHTML =
|
|
2256
|
+
`<button class="gg-toast-btn gg-toast-dismiss" title="Dismiss">\u00d7</button>` +
|
|
2257
|
+
`<span class="gg-toast-label">Dropped:</span>` +
|
|
2258
|
+
`<span class="gg-toast-ns">${nsHTML}</span>` +
|
|
2259
|
+
`<span class="gg-toast-actions">` +
|
|
2260
|
+
`<button class="gg-toast-btn gg-toast-undo">Undo</button>` +
|
|
2261
|
+
`<button class="gg-toast-btn gg-toast-help" title="Toggle help">?</button>` +
|
|
2262
|
+
`</span>` +
|
|
2263
|
+
`<div class="gg-toast-explanation${showExplanation ? ' visible' : ''}">` +
|
|
2264
|
+
`Click a segment above to drop all matching namespaces from the buffer (e.g. click "api" to drop api/*).` +
|
|
2265
|
+
`</div>`;
|
|
2266
|
+
toast.classList.add('visible');
|
|
2267
|
+
if (showExplanation)
|
|
2268
|
+
hasSeenToastExplanation = true;
|
|
2269
|
+
}
|
|
2270
|
+
function showKeepToast(namespace, previousKeep, info) {
|
|
2271
|
+
if (!$el)
|
|
2272
|
+
return;
|
|
2273
|
+
toastMode = 'keep';
|
|
2274
|
+
lastKeptPattern = previousKeep;
|
|
2275
|
+
lastKeptNamespaceInfo = { ns: namespace, info };
|
|
2276
|
+
const toast = $el.find('.gg-toast').get(0);
|
|
2277
|
+
if (!toast)
|
|
2278
|
+
return;
|
|
2279
|
+
const nsHTML = buildToastNsHTML(namespace);
|
|
2280
|
+
const showExplanation = !hasSeenToastExplanation;
|
|
2281
|
+
toast.innerHTML =
|
|
2282
|
+
`<button class="gg-toast-btn gg-toast-dismiss" title="Dismiss">\u00d7</button>` +
|
|
2283
|
+
`<span class="gg-toast-label">Kept:</span>` +
|
|
2284
|
+
`<span class="gg-toast-ns">${nsHTML}</span>` +
|
|
2285
|
+
`<span class="gg-toast-actions">` +
|
|
2286
|
+
`<button class="gg-toast-btn gg-toast-undo">Undo</button>` +
|
|
2287
|
+
`<button class="gg-toast-btn gg-toast-help" title="Toggle help">?</button>` +
|
|
2288
|
+
`</span>` +
|
|
2289
|
+
`<div class="gg-toast-explanation${showExplanation ? ' visible' : ''}">` +
|
|
2290
|
+
`Click a segment above to keep all matching namespaces (e.g. click "api" to keep api/*). ` +
|
|
2291
|
+
`Only new loggs from this point forward will be captured.` +
|
|
2292
|
+
`</div>`;
|
|
2293
|
+
toast.classList.add('visible');
|
|
2294
|
+
if (showExplanation)
|
|
1566
2295
|
hasSeenToastExplanation = true;
|
|
1567
|
-
}
|
|
1568
2296
|
}
|
|
1569
2297
|
/** Dismiss the toast bar */
|
|
1570
2298
|
function dismissToast() {
|
|
@@ -1575,25 +2303,64 @@ export function createGgPlugin(options, gg) {
|
|
|
1575
2303
|
toast.classList.remove('visible');
|
|
1576
2304
|
}
|
|
1577
2305
|
lastHiddenPattern = null;
|
|
2306
|
+
lastDroppedPattern = null;
|
|
2307
|
+
lastKeptPattern = null;
|
|
2308
|
+
lastKeptNamespaceInfo = null;
|
|
1578
2309
|
}
|
|
1579
|
-
/** Undo the last namespace hide */
|
|
2310
|
+
/** Undo the last namespace hide, drop, or keep */
|
|
1580
2311
|
function undoHide() {
|
|
1581
|
-
if (!$el
|
|
2312
|
+
if (!$el)
|
|
1582
2313
|
return;
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
2314
|
+
if (toastMode === 'keep' && lastKeptPattern !== null) {
|
|
2315
|
+
// Restore keepPattern
|
|
2316
|
+
keepPattern = lastKeptPattern;
|
|
2317
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
2318
|
+
const keepInput = $el.find('.gg-keep-input').get(0);
|
|
2319
|
+
if (keepInput)
|
|
2320
|
+
keepInput.value = keepPattern;
|
|
2321
|
+
// Re-insert the sentinel entry so it reappears
|
|
2322
|
+
if (lastKeptNamespaceInfo) {
|
|
2323
|
+
droppedNamespaces.set(lastKeptNamespaceInfo.ns, lastKeptNamespaceInfo.info);
|
|
2324
|
+
}
|
|
2325
|
+
dismissToast();
|
|
2326
|
+
renderKeepUI();
|
|
2327
|
+
renderSentinelSection();
|
|
2328
|
+
renderLogs();
|
|
2329
|
+
}
|
|
2330
|
+
else if (toastMode === 'drop' && lastDroppedPattern !== null) {
|
|
2331
|
+
// Restore keepPattern
|
|
2332
|
+
keepPattern = lastDroppedPattern;
|
|
2333
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
2334
|
+
const keepInput = $el.find('.gg-keep-input').get(0);
|
|
2335
|
+
if (keepInput)
|
|
2336
|
+
keepInput.value = keepPattern;
|
|
2337
|
+
// Restore enabledNamespaces from current show filter
|
|
2338
|
+
enabledNamespaces.clear();
|
|
2339
|
+
const effectiveShow = filterPattern || 'gg:*';
|
|
2340
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
2341
|
+
if (namespaceMatchesPattern(ns, effectiveShow))
|
|
2342
|
+
enabledNamespaces.add(ns);
|
|
2343
|
+
});
|
|
2344
|
+
dismissToast();
|
|
2345
|
+
renderKeepUI();
|
|
2346
|
+
renderFilterUI();
|
|
2347
|
+
renderLogs();
|
|
2348
|
+
}
|
|
2349
|
+
else if (toastMode === 'hide' && lastHiddenPattern !== null) {
|
|
2350
|
+
// Restore filterPattern
|
|
2351
|
+
filterPattern = lastHiddenPattern;
|
|
2352
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
2353
|
+
enabledNamespaces.clear();
|
|
2354
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
2355
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
2356
|
+
if (namespaceMatchesPattern(ns, effectivePattern))
|
|
2357
|
+
enabledNamespaces.add(ns);
|
|
2358
|
+
});
|
|
2359
|
+
dismissToast();
|
|
2360
|
+
renderFilterUI();
|
|
2361
|
+
renderSentinelSection();
|
|
2362
|
+
renderLogs();
|
|
2363
|
+
}
|
|
1597
2364
|
}
|
|
1598
2365
|
/** Wire up toast event handlers (called once after init) */
|
|
1599
2366
|
function wireUpToast() {
|
|
@@ -1622,41 +2389,101 @@ export function createGgPlugin(options, gg) {
|
|
|
1622
2389
|
}
|
|
1623
2390
|
return;
|
|
1624
2391
|
}
|
|
1625
|
-
// Segment click: add exclusion
|
|
2392
|
+
// Segment click: add exclusion to the appropriate layer
|
|
1626
2393
|
if (target.classList?.contains('gg-toast-segment')) {
|
|
1627
2394
|
const filter = target.getAttribute('data-filter');
|
|
1628
2395
|
if (!filter)
|
|
1629
2396
|
return;
|
|
1630
|
-
// Add exclusion pattern (same logic as right-click segment)
|
|
1631
|
-
const currentPattern = filterPattern || 'gg:*';
|
|
1632
2397
|
const exclusion = `-${filter}`;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
//
|
|
1636
|
-
|
|
2398
|
+
if (toastMode === 'keep') {
|
|
2399
|
+
// Broaden the keep: remove all exclusions whose prefix matches `filter`,
|
|
2400
|
+
// then add `filter` as an inclusion if pattern has no wildcard base.
|
|
2401
|
+
const currentKeep = keepPattern || 'gg:*';
|
|
2402
|
+
const keepParts = currentKeep
|
|
2403
|
+
.split(',')
|
|
2404
|
+
.map((p) => p.trim())
|
|
2405
|
+
.filter(Boolean);
|
|
2406
|
+
// Remove any exclusion that is a sub-pattern of filter (starts with -filter prefix)
|
|
2407
|
+
const narrowed = keepParts.filter((p) => {
|
|
2408
|
+
if (!p.startsWith('-'))
|
|
2409
|
+
return true;
|
|
2410
|
+
const excl = p.slice(1).replace(/\*$/, '');
|
|
2411
|
+
const filterBase = filter.replace(/\*$/, '');
|
|
2412
|
+
return !excl.startsWith(filterBase) && !filterBase.startsWith(excl);
|
|
2413
|
+
});
|
|
2414
|
+
const hasWildcardBase = narrowed.some((p) => p === '*' || p === 'gg:*');
|
|
2415
|
+
if (!hasWildcardBase &&
|
|
2416
|
+
!narrowed.some((p) => !p.startsWith('-') && namespaceMatchesPattern(filter.replace(/\*$/, 'x'), p))) {
|
|
2417
|
+
narrowed.push(filter);
|
|
2418
|
+
}
|
|
2419
|
+
keepPattern = simplifyPattern(narrowed.join(',') || 'gg:*');
|
|
2420
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
2421
|
+
const keepInput = $el?.find('.gg-keep-input').get(0);
|
|
2422
|
+
if (keepInput)
|
|
2423
|
+
keepInput.value = keepPattern;
|
|
2424
|
+
// Remove all dropped namespaces that now match the new keepPattern
|
|
2425
|
+
for (const ns of [...droppedNamespaces.keys()]) {
|
|
2426
|
+
if (namespaceMatchesPattern(ns, keepPattern)) {
|
|
2427
|
+
droppedNamespaces.delete(ns);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
dismissToast();
|
|
2431
|
+
renderKeepUI();
|
|
2432
|
+
renderSentinelSection();
|
|
2433
|
+
renderLogs();
|
|
2434
|
+
}
|
|
2435
|
+
else if (toastMode === 'drop') {
|
|
2436
|
+
// Add exclusion to keepPattern (Layer 1)
|
|
2437
|
+
const currentKeep = keepPattern || 'gg:*';
|
|
2438
|
+
const keepParts = currentKeep.split(',').map((p) => p.trim());
|
|
2439
|
+
if (!keepParts.includes(exclusion)) {
|
|
2440
|
+
keepPattern = keepParts.some((p) => !p.startsWith('-'))
|
|
2441
|
+
? `${currentKeep},${exclusion}`
|
|
2442
|
+
: `gg:*,${exclusion}`;
|
|
2443
|
+
keepPattern = simplifyPattern(keepPattern);
|
|
2444
|
+
}
|
|
2445
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
2446
|
+
const keepInput = $el?.find('.gg-keep-input').get(0);
|
|
2447
|
+
if (keepInput)
|
|
2448
|
+
keepInput.value = keepPattern;
|
|
2449
|
+
// Remove newly-dropped namespace from visible list
|
|
2450
|
+
// (the segment pattern may match multiple namespaces)
|
|
2451
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
2452
|
+
if (!namespaceMatchesPattern(ns, keepPattern)) {
|
|
2453
|
+
enabledNamespaces.delete(ns);
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
dismissToast();
|
|
2457
|
+
renderKeepUI();
|
|
2458
|
+
renderFilterUI();
|
|
2459
|
+
renderLogs();
|
|
2460
|
+
scheduleSentinelRender();
|
|
1637
2461
|
}
|
|
1638
2462
|
else {
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
2463
|
+
// toastMode === 'hide': add exclusion to filterPattern (Layer 2)
|
|
2464
|
+
const currentPattern = filterPattern || 'gg:*';
|
|
2465
|
+
const parts = currentPattern.split(',').map((p) => p.trim());
|
|
2466
|
+
if (parts.includes(exclusion)) {
|
|
2467
|
+
filterPattern = parts.filter((p) => p !== exclusion).join(',') || 'gg:*';
|
|
1642
2468
|
}
|
|
1643
2469
|
else {
|
|
1644
|
-
filterPattern =
|
|
2470
|
+
filterPattern = parts.some((p) => !p.startsWith('-'))
|
|
2471
|
+
? `${currentPattern},${exclusion}`
|
|
2472
|
+
: `gg:*,${exclusion}`;
|
|
1645
2473
|
}
|
|
2474
|
+
filterPattern = simplifyPattern(filterPattern);
|
|
2475
|
+
enabledNamespaces.clear();
|
|
2476
|
+
const effectivePattern = filterPattern || 'gg:*';
|
|
2477
|
+
getAllCapturedNamespaces().forEach((ns) => {
|
|
2478
|
+
if (namespaceMatchesPattern(ns, effectivePattern))
|
|
2479
|
+
enabledNamespaces.add(ns);
|
|
2480
|
+
});
|
|
2481
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
2482
|
+
dismissToast();
|
|
2483
|
+
renderFilterUI();
|
|
2484
|
+
renderSentinelSection();
|
|
2485
|
+
renderLogs();
|
|
1646
2486
|
}
|
|
1647
|
-
filterPattern = simplifyPattern(filterPattern);
|
|
1648
|
-
// Sync enabledNamespaces
|
|
1649
|
-
enabledNamespaces.clear();
|
|
1650
|
-
const effectivePattern = filterPattern || 'gg:*';
|
|
1651
|
-
getAllCapturedNamespaces().forEach((ns) => {
|
|
1652
|
-
if (namespaceMatchesPattern(ns, effectivePattern)) {
|
|
1653
|
-
enabledNamespaces.add(ns);
|
|
1654
|
-
}
|
|
1655
|
-
});
|
|
1656
|
-
localStorage.setItem(FILTER_KEY, filterPattern);
|
|
1657
|
-
dismissToast();
|
|
1658
|
-
renderFilterUI();
|
|
1659
|
-
renderLogs();
|
|
1660
2487
|
return;
|
|
1661
2488
|
}
|
|
1662
2489
|
});
|
|
@@ -1671,13 +2498,25 @@ export function createGgPlugin(options, gg) {
|
|
|
1671
2498
|
return;
|
|
1672
2499
|
containerEl.addEventListener('click', (e) => {
|
|
1673
2500
|
const target = e.target;
|
|
1674
|
-
// Handle reset filter button (shown when all logs filtered out)
|
|
2501
|
+
// Handle reset filter button (shown when all logs filtered out by gg-show)
|
|
1675
2502
|
if (target?.classList?.contains('gg-reset-filter-btn')) {
|
|
1676
2503
|
filterPattern = 'gg:*';
|
|
1677
2504
|
enabledNamespaces.clear();
|
|
1678
2505
|
getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
|
|
1679
|
-
localStorage.setItem(
|
|
2506
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1680
2507
|
renderFilterUI();
|
|
2508
|
+
renderSentinelSection();
|
|
2509
|
+
renderLogs();
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
// Handle keep-all button (shown when gg-keep is restrictive and buffer is empty)
|
|
2513
|
+
if (target?.classList?.contains('gg-keep-all-btn')) {
|
|
2514
|
+
keepPattern = 'gg:*';
|
|
2515
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
2516
|
+
const keepInput = $el?.find('.gg-keep-input').get(0);
|
|
2517
|
+
if (keepInput)
|
|
2518
|
+
keepInput.value = keepPattern;
|
|
2519
|
+
renderKeepUI();
|
|
1681
2520
|
renderLogs();
|
|
1682
2521
|
return;
|
|
1683
2522
|
}
|
|
@@ -1748,8 +2587,9 @@ export function createGgPlugin(options, gg) {
|
|
|
1748
2587
|
.filter((ns) => namespaceMatchesPattern(ns, filter))
|
|
1749
2588
|
.forEach((ns) => enabledNamespaces.add(ns));
|
|
1750
2589
|
}
|
|
1751
|
-
localStorage.setItem(
|
|
2590
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1752
2591
|
renderFilterUI();
|
|
2592
|
+
renderSentinelSection();
|
|
1753
2593
|
renderLogs();
|
|
1754
2594
|
return;
|
|
1755
2595
|
}
|
|
@@ -1758,41 +2598,65 @@ export function createGgPlugin(options, gg) {
|
|
|
1758
2598
|
handleNamespaceClick(target);
|
|
1759
2599
|
return;
|
|
1760
2600
|
}
|
|
2601
|
+
// Handle clicking drop button for namespace (Layer 1: gg-keep)
|
|
2602
|
+
const dropBtn = target?.closest?.('.gg-ns-drop');
|
|
2603
|
+
if (dropBtn) {
|
|
2604
|
+
const namespace = dropBtn.getAttribute('data-namespace');
|
|
2605
|
+
if (!namespace)
|
|
2606
|
+
return;
|
|
2607
|
+
const currentKeep = keepPattern || 'gg:*';
|
|
2608
|
+
const exclusion = `-${namespace}`;
|
|
2609
|
+
const parts = currentKeep.split(',').map((p) => p.trim());
|
|
2610
|
+
if (!parts.includes(exclusion)) {
|
|
2611
|
+
const previousKeep = keepPattern;
|
|
2612
|
+
keepPattern = `${currentKeep},${exclusion}`;
|
|
2613
|
+
keepPattern = simplifyPattern(keepPattern);
|
|
2614
|
+
localStorage.setItem(KEEP_KEY, keepPattern);
|
|
2615
|
+
// Sync keep input
|
|
2616
|
+
const keepInput = $el?.find('.gg-keep-input').get(0);
|
|
2617
|
+
if (keepInput)
|
|
2618
|
+
keepInput.value = keepPattern;
|
|
2619
|
+
// Remove from visible loggs (Layer 1 drop hides from display too)
|
|
2620
|
+
enabledNamespaces.delete(namespace);
|
|
2621
|
+
renderKeepUI();
|
|
2622
|
+
renderFilterUI();
|
|
2623
|
+
renderLogs();
|
|
2624
|
+
scheduleSentinelRender();
|
|
2625
|
+
showDropToast(namespace, previousKeep);
|
|
2626
|
+
}
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
1761
2629
|
// Handle clicking hide button for namespace
|
|
1762
|
-
|
|
1763
|
-
|
|
2630
|
+
const hideBtn = target?.closest?.('.gg-ns-hide');
|
|
2631
|
+
if (hideBtn) {
|
|
2632
|
+
const namespace = hideBtn.getAttribute('data-namespace');
|
|
1764
2633
|
if (!namespace)
|
|
1765
2634
|
return;
|
|
1766
2635
|
// Save current pattern for undo before hiding
|
|
1767
2636
|
const previousPattern = filterPattern;
|
|
1768
2637
|
toggleNamespace(namespace, false);
|
|
1769
|
-
localStorage.setItem(
|
|
2638
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1770
2639
|
renderFilterUI();
|
|
2640
|
+
renderSentinelSection();
|
|
1771
2641
|
renderLogs();
|
|
1772
2642
|
// Show toast with undo option
|
|
1773
2643
|
showHideToast(namespace, previousPattern);
|
|
1774
2644
|
return;
|
|
1775
2645
|
}
|
|
1776
|
-
// Clicking background (container or grid, not a log
|
|
1777
|
-
if (
|
|
1778
|
-
filterPattern !== 'gg:*' &&
|
|
2646
|
+
// Clicking background (container or grid, not a log entry) restores show filter
|
|
2647
|
+
if (filterPattern !== 'gg:*' &&
|
|
1779
2648
|
(target === containerEl || target?.classList?.contains('gg-log-grid'))) {
|
|
1780
2649
|
filterPattern = 'gg:*';
|
|
1781
2650
|
enabledNamespaces.clear();
|
|
1782
2651
|
getAllCapturedNamespaces().forEach((ns) => enabledNamespaces.add(ns));
|
|
1783
|
-
localStorage.setItem(
|
|
2652
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1784
2653
|
renderFilterUI();
|
|
2654
|
+
renderSentinelSection();
|
|
1785
2655
|
renderLogs();
|
|
1786
2656
|
}
|
|
1787
2657
|
});
|
|
1788
|
-
|
|
1789
|
-
function
|
|
1790
|
-
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
1791
|
-
if (!tip)
|
|
1792
|
-
return;
|
|
1793
|
-
tip.textContent = text;
|
|
1794
|
-
tip.style.display = 'block';
|
|
1795
|
-
const targetRect = target.getBoundingClientRect();
|
|
2658
|
+
/** Clamp and apply tooltip position below (or above) a target element. */
|
|
2659
|
+
function positionTooltip(tip, targetRect) {
|
|
1796
2660
|
let left = targetRect.left;
|
|
1797
2661
|
let top = targetRect.bottom + 4;
|
|
1798
2662
|
const tipRect = tip.getBoundingClientRect();
|
|
@@ -1806,6 +2670,15 @@ export function createGgPlugin(options, gg) {
|
|
|
1806
2670
|
}
|
|
1807
2671
|
tip.style.left = `${left}px`;
|
|
1808
2672
|
tip.style.top = `${top}px`;
|
|
2673
|
+
}
|
|
2674
|
+
// Helper: show confirmation tooltip near target element
|
|
2675
|
+
function showConfirmationTooltip(containerEl, target, text) {
|
|
2676
|
+
const tip = containerEl.querySelector('.gg-hover-tooltip');
|
|
2677
|
+
if (!tip)
|
|
2678
|
+
return;
|
|
2679
|
+
tip.textContent = text;
|
|
2680
|
+
tip.style.display = 'block';
|
|
2681
|
+
positionTooltip(tip, target.getBoundingClientRect());
|
|
1809
2682
|
setTimeout(() => {
|
|
1810
2683
|
tip.style.display = 'none';
|
|
1811
2684
|
}, 1500);
|
|
@@ -1847,8 +2720,9 @@ export function createGgPlugin(options, gg) {
|
|
|
1847
2720
|
enabledNamespaces.add(ns);
|
|
1848
2721
|
}
|
|
1849
2722
|
});
|
|
1850
|
-
localStorage.setItem(
|
|
2723
|
+
localStorage.setItem(SHOW_KEY, filterPattern);
|
|
1851
2724
|
renderFilterUI();
|
|
2725
|
+
renderSentinelSection();
|
|
1852
2726
|
renderLogs();
|
|
1853
2727
|
return;
|
|
1854
2728
|
}
|
|
@@ -1916,23 +2790,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1916
2790
|
pre.textContent = JSON.stringify(arg, null, 2);
|
|
1917
2791
|
tip.appendChild(pre);
|
|
1918
2792
|
tip.style.display = 'block';
|
|
1919
|
-
|
|
1920
|
-
const targetRect = target.getBoundingClientRect();
|
|
1921
|
-
let left = targetRect.left;
|
|
1922
|
-
let top = targetRect.bottom + 4;
|
|
1923
|
-
// Keep tooltip within viewport
|
|
1924
|
-
const tipRect = tip.getBoundingClientRect();
|
|
1925
|
-
if (left + tipRect.width > window.innerWidth) {
|
|
1926
|
-
left = window.innerWidth - tipRect.width - 8;
|
|
1927
|
-
}
|
|
1928
|
-
if (left < 4)
|
|
1929
|
-
left = 4;
|
|
1930
|
-
// If tooltip would go below viewport, show above instead
|
|
1931
|
-
if (top + tipRect.height > window.innerHeight) {
|
|
1932
|
-
top = targetRect.top - tipRect.height - 4;
|
|
1933
|
-
}
|
|
1934
|
-
tip.style.left = `${left}px`;
|
|
1935
|
-
tip.style.top = `${top}px`;
|
|
2793
|
+
positionTooltip(tip, target.getBoundingClientRect());
|
|
1936
2794
|
});
|
|
1937
2795
|
containerEl.addEventListener('mouseout', (e) => {
|
|
1938
2796
|
const target = e.target?.closest?.('.gg-expand');
|
|
@@ -1972,22 +2830,7 @@ export function createGgPlugin(options, gg) {
|
|
|
1972
2830
|
}
|
|
1973
2831
|
tip.textContent = actionText;
|
|
1974
2832
|
tip.style.display = 'block';
|
|
1975
|
-
|
|
1976
|
-
const targetRect = target.getBoundingClientRect();
|
|
1977
|
-
let left = targetRect.left;
|
|
1978
|
-
let top = targetRect.bottom + 4;
|
|
1979
|
-
// Keep tooltip within viewport
|
|
1980
|
-
const tipRect = tip.getBoundingClientRect();
|
|
1981
|
-
if (left + tipRect.width > window.innerWidth) {
|
|
1982
|
-
left = window.innerWidth - tipRect.width - 8;
|
|
1983
|
-
}
|
|
1984
|
-
if (left < 4)
|
|
1985
|
-
left = 4;
|
|
1986
|
-
if (top + tipRect.height > window.innerHeight) {
|
|
1987
|
-
top = targetRect.top - tipRect.height - 4;
|
|
1988
|
-
}
|
|
1989
|
-
tip.style.left = `${left}px`;
|
|
1990
|
-
tip.style.top = `${top}px`;
|
|
2833
|
+
positionTooltip(tip, target.getBoundingClientRect());
|
|
1991
2834
|
});
|
|
1992
2835
|
containerEl.addEventListener('mouseout', (e) => {
|
|
1993
2836
|
const target = e.target;
|
|
@@ -2178,31 +3021,13 @@ export function createGgPlugin(options, gg) {
|
|
|
2178
3021
|
return (`<div class="gg-log-entry${levelClass}" data-entry="${index}"${vindexAttr}>` +
|
|
2179
3022
|
`<div class="gg-log-header">` +
|
|
2180
3023
|
`<div class="gg-log-diff" style="color: ${color};"${fileAttr}${lineAttr}${colAttr}>${diff}</div>` +
|
|
2181
|
-
`<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"
|
|
3024
|
+
`<div class="gg-log-ns" style="color: ${color};" data-namespace="${escapeHtml(entry.namespace)}"><span class="gg-ns-text">${nsHTML}</span><button class="gg-ns-drop" data-namespace="${escapeHtml(entry.namespace)}" title="Drop this namespace (stop buffering its loggs)">${ICON_DROP}</button><button class="gg-ns-hide" data-namespace="${escapeHtml(entry.namespace)}" title="Hide this namespace">${ICON_HIDE}</button></div>` +
|
|
2182
3025
|
`<div class="gg-log-handle"></div>` +
|
|
2183
3026
|
`</div>` +
|
|
2184
3027
|
`<div class="gg-log-content"${hasSrcExpr ? ` data-src="${escapeHtml(entry.src)}"` : ''}>${exprAboveForPrimitives}${argsHTML}${stackHTML}</div>` +
|
|
2185
3028
|
detailsHTML +
|
|
2186
3029
|
`</div>`);
|
|
2187
3030
|
}
|
|
2188
|
-
/** Update the copy-button count text */
|
|
2189
|
-
function updateTruncationBanner() {
|
|
2190
|
-
if (!$el)
|
|
2191
|
-
return;
|
|
2192
|
-
const banner = $el.find('.gg-truncation-banner').get(0);
|
|
2193
|
-
if (!banner)
|
|
2194
|
-
return;
|
|
2195
|
-
const evicted = buffer.evicted;
|
|
2196
|
-
if (evicted > 0) {
|
|
2197
|
-
const total = buffer.totalPushed;
|
|
2198
|
-
const retained = buffer.size;
|
|
2199
|
-
banner.innerHTML = `⚠ Showing ${retained.toLocaleString()} of ${total.toLocaleString()} messages — ${evicted.toLocaleString()} truncated. Increase <code style="font-family:monospace;background:rgba(255,255,255,0.15);padding:0 3px;border-radius:3px;">maxEntries</code> to retain more.`;
|
|
2200
|
-
banner.style.display = 'flex';
|
|
2201
|
-
}
|
|
2202
|
-
else {
|
|
2203
|
-
banner.style.display = 'none';
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
3031
|
function updateCopyCount() {
|
|
2207
3032
|
if (!$el)
|
|
2208
3033
|
return;
|
|
@@ -2323,7 +3148,8 @@ export function createGgPlugin(options, gg) {
|
|
|
2323
3148
|
// Mount the virtualizer (attaches scroll/resize observers)
|
|
2324
3149
|
const cleanup = virtualizer._didMount();
|
|
2325
3150
|
// Store cleanup for when we tear down (TODO: call on destroy)
|
|
2326
|
-
containerDom.__ggVirtualCleanup =
|
|
3151
|
+
containerDom.__ggVirtualCleanup =
|
|
3152
|
+
cleanup;
|
|
2327
3153
|
// Initial render, then scroll to bottom if requested
|
|
2328
3154
|
virtualizer._willUpdate();
|
|
2329
3155
|
renderVirtualItems();
|
|
@@ -2354,6 +3180,7 @@ export function createGgPlugin(options, gg) {
|
|
|
2354
3180
|
const hasVisible = newEntries.some((e) => enabledNamespaces.has(e.namespace));
|
|
2355
3181
|
if (!hasVisible && buffer.evicted === 0) {
|
|
2356
3182
|
updateCopyCount();
|
|
3183
|
+
renderPipelineUI();
|
|
2357
3184
|
return;
|
|
2358
3185
|
}
|
|
2359
3186
|
// Rebuild filteredIndices from scratch. This is O(buffer.size) with a
|
|
@@ -2361,7 +3188,7 @@ export function createGgPlugin(options, gg) {
|
|
|
2361
3188
|
// when the buffer wraps and old logical indices shift.
|
|
2362
3189
|
rebuildFilteredIndices();
|
|
2363
3190
|
updateCopyCount();
|
|
2364
|
-
|
|
3191
|
+
renderPipelineUI();
|
|
2365
3192
|
// Check if user is near bottom before we update the virtualizer
|
|
2366
3193
|
const nearBottom = containerDom.scrollHeight - containerDom.scrollTop - containerDom.clientHeight < 50;
|
|
2367
3194
|
userNearBottom = nearBottom;
|
|
@@ -2398,33 +3225,46 @@ export function createGgPlugin(options, gg) {
|
|
|
2398
3225
|
expandedStacks.clear();
|
|
2399
3226
|
// Rebuild filtered indices from scratch
|
|
2400
3227
|
rebuildFilteredIndices();
|
|
3228
|
+
renderPipelineUI();
|
|
2401
3229
|
updateCopyCount();
|
|
2402
|
-
updateTruncationBanner();
|
|
2403
3230
|
if (filteredIndices.length === 0) {
|
|
2404
3231
|
// Tear down virtualizer
|
|
2405
3232
|
if (virtualizer) {
|
|
2406
3233
|
const containerDom = logContainer.get(0);
|
|
2407
3234
|
if (containerDom) {
|
|
2408
|
-
const cleanup = containerDom
|
|
3235
|
+
const cleanup = containerDom
|
|
3236
|
+
.__ggVirtualCleanup;
|
|
2409
3237
|
if (cleanup)
|
|
2410
3238
|
cleanup();
|
|
2411
3239
|
}
|
|
2412
3240
|
virtualizer = null;
|
|
2413
3241
|
}
|
|
2414
3242
|
const hasFilteredLogs = buffer.size > 0;
|
|
2415
|
-
const
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
3243
|
+
const keepIsRestrictive = (keepPattern || 'gg:*') !== 'gg:*' && (keepPattern || 'gg:*') !== '*';
|
|
3244
|
+
let message;
|
|
3245
|
+
let actionButton;
|
|
3246
|
+
if (hasFilteredLogs) {
|
|
3247
|
+
message = `All ${buffer.size} logs filtered out.`;
|
|
3248
|
+
actionButton =
|
|
3249
|
+
'<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>';
|
|
3250
|
+
}
|
|
3251
|
+
else if (keepIsRestrictive) {
|
|
3252
|
+
message = 'No loggs kept.';
|
|
3253
|
+
actionButton =
|
|
3254
|
+
`<button class="gg-keep-all-btn" style="margin-top: 12px; padding: 10px 20px; cursor: pointer; border: 1px solid #4CAF50; background: #4CAF50; color: white; border-radius: 6px; font-size: 13px; font-weight: 500; transition: background 0.2s;">Keep All</button>` +
|
|
3255
|
+
`<div style="margin-top: 10px; font-size: 11px; opacity: 0.7;">gg-keep: ${escapeHtml(keepPattern || 'gg:*')}</div>`;
|
|
3256
|
+
}
|
|
3257
|
+
else {
|
|
3258
|
+
message = 'No loggs captured yet. Call gg() to see output here.';
|
|
3259
|
+
actionButton = '';
|
|
3260
|
+
}
|
|
3261
|
+
logContainer.html(`<div style="padding: 20px; text-align: center; opacity: 0.5;">${message}<div>${actionButton}</div></div>`);
|
|
2422
3262
|
return;
|
|
2423
3263
|
}
|
|
2424
3264
|
// Build the virtual scroll DOM structure:
|
|
2425
3265
|
// - .gg-virtual-spacer: sized to total virtual height (provides scrollbar)
|
|
2426
3266
|
// - .gg-log-grid: positioned absolutely, translated to visible offset, holds only visible entries
|
|
2427
|
-
const gridClasses = `gg-log-grid${
|
|
3267
|
+
const gridClasses = `gg-log-grid${showExpressions ? ' gg-show-expr' : ''}`;
|
|
2428
3268
|
logContainer.html(`<div class="gg-virtual-spacer">` +
|
|
2429
3269
|
`<div class="${gridClasses}" style="position: absolute; top: 0; left: 0; width: 100%; grid-template-columns: ${gridColumns()};"></div>` +
|
|
2430
3270
|
`</div>` +
|