diff --git a/src/renderer/index.html b/src/renderer/index.html
index 29bcbd7..c9ef5be 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -145,6 +145,9 @@
+
diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts
index f505064..c9b420e 100644
--- a/src/renderer/renderer.ts
+++ b/src/renderer/renderer.ts
@@ -340,6 +340,7 @@ interface AppState {
// "Mute" patterns: rows whose text contains any of these are dimmed IN PLACE
// (kept for context, not removed) via a render-time class. Persisted globally.
mutePatterns: string[];
+ mutedSize: 'normal' | 'small' | 'tiny'; // opt-in smaller font for muted lines (mute menu)
// Traceback
tracebackResult: any | null;
tracebackSort: 'time' | 'score';
@@ -419,6 +420,7 @@ const state: AppState = {
hiddenLevels: new Set
(JSON.parse(localStorage.getItem('logan-hidden-levels') || '[]')),
levelBarFilterActive: false,
mutePatterns: JSON.parse(localStorage.getItem('logan-mute-patterns') || '[]'),
+ mutedSize: (localStorage.getItem('logan-muted-size') as 'normal' | 'small' | 'tiny') || 'normal',
tracebackResult: null,
tracebackSort: 'time' as 'time' | 'score',
tracebackFilterCat: 'all',
@@ -2876,6 +2878,18 @@ function clearMutePatterns(): void {
renderVisibleLines();
}
+// Opt-in smaller font for muted lines (default 'normal' = as is). Applied via a body class
+// so it works for already- and later-rendered rows without a re-render. Persisted.
+function applyMutedSize(): void {
+ document.body.classList.toggle('muted-size-small', state.mutedSize === 'small');
+ document.body.classList.toggle('muted-size-tiny', state.mutedSize === 'tiny');
+}
+function setMutedSize(size: 'normal' | 'small' | 'tiny'): void {
+ state.mutedSize = size;
+ localStorage.setItem('logan-muted-size', size);
+ applyMutedSize();
+}
+
function createLineElementPooled(line: LogLine): HTMLDivElement {
const div = lineElementPool.acquire();
div.dataset.lineNumber = String(line.lineNumber);
@@ -4740,6 +4754,12 @@ function handleContextMenu(event: MouseEvent): void {
menu.remove();
});
menu.appendChild(clearMutes);
+ // Muted line SIZE — opt-in smaller font so noise recedes (default As is). ✓ marks current.
+ for (const [label, val] of [['As is', 'normal'], ['Small', 'small'], ['Tiny', 'tiny']] as const) {
+ const it = menuItem(state.mutedSize === val ? '✓' : ' ', `Muted size: ${label}`);
+ it.addEventListener('click', () => { setMutedSize(val); menu.remove(); });
+ menu.appendChild(it);
+ }
menu.appendChild(menuSeparator());
}
@@ -5689,6 +5709,44 @@ function findSubfolder(entries: LocalFolderFile[], subdirPath: string): LocalFol
return null;
}
+// Reveal the active file in the folder tree: expand the folder + the ancestor subdirs on
+// its path, re-render, then scroll it into view + flash it. The active row already carries
+// the `.active` class (see renderFolderEntries); this just makes it visible + findable.
+function revealActiveFileInTree(): void {
+ if (!state.filePath) { showToast('No active file to reveal'); return; }
+ const target = state.filePath;
+ let matched = false;
+ for (const folder of state.folders) {
+ const base = folder.path.replace(/[\\/]+$/, '');
+ if (target !== base && !target.startsWith(base + '/') && !target.startsWith(base + '\\')) continue;
+ matched = true;
+ folder.collapsed = false;
+ // Un-collapse each ancestor directory of the file (those present in the loaded tree).
+ const rel = target.slice(base.length).replace(/^[\\/]+/, '');
+ const parts = rel.split(/[\\/]/);
+ parts.pop(); // drop the filename
+ let acc = base;
+ for (const part of parts) {
+ acc = acc + '/' + part;
+ const sub = findSubfolder(folder.files, acc) || findSubfolder(folder.files, acc.replace(/\//g, '\\'));
+ if (sub) sub.collapsed = false;
+ }
+ break;
+ }
+ if (!matched) { showToast('Active file isn’t under a folder in the tree — add its folder first'); return; }
+ renderFolderTree();
+ requestAnimationFrame(() => {
+ const el = elements.foldersList.querySelector('.folder-file.active') as HTMLElement | null;
+ if (el) {
+ el.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ el.classList.add('reveal-flash');
+ setTimeout(() => el.classList.remove('reveal-flash'), 1400);
+ } else {
+ showToast('Located its folder, but the file row isn’t loaded yet — expand that folder');
+ }
+ });
+}
+
function renderFolderTree(): void {
if (state.folders.length === 0) {
elements.foldersList.innerHTML = 'No folders open
';
@@ -22586,6 +22644,7 @@ function setupHelpTooltips(): void {
function init(): void {
// Detect platform and setup window controls
setupWindowControls();
+ applyMutedSize(); // restore the saved muted-line size (default: as is)
// Load user settings from localStorage
loadSettings();
@@ -23050,6 +23109,7 @@ function init(): void {
elements.btnLiveSshRefresh.addEventListener('click', () => refreshSshHosts());
elements.btnLiveSshManage.addEventListener('click', () => showSshProfileManager());
elements.btnOpenSshFolder.addEventListener('click', () => openSshFolder());
+ document.getElementById('btn-reveal-active-file')?.addEventListener('click', () => revealActiveFileInTree());
// Unified live connection event listeners (register once)
setupLiveEventListeners();
diff --git a/src/renderer/styles.css b/src/renderer/styles.css
index 734b27b..3e8ad89 100644
--- a/src/renderer/styles.css
+++ b/src/renderer/styles.css
@@ -1550,6 +1550,12 @@ body.platform-darwin .titlebar {
background: var(--accent-color);
color: white;
}
+/* Brief flash when "Reveal active file" scrolls to it, so the eye catches it. */
+.folder-file.reveal-flash { animation: folder-reveal-flash 1.3s ease-out; }
+@keyframes folder-reveal-flash {
+ 0%, 30% { box-shadow: inset 0 0 0 2px var(--accent-color, #5b8def); }
+ 100% { box-shadow: inset 0 0 0 2px transparent; }
+}
.folder-file-icon {
width: 14px;
@@ -2643,17 +2649,19 @@ button.is-cancelable:hover, .btn.is-cancelable:hover { filter: brightness(1.12);
to bring it back to full opacity so it's still readable on demand. */
.log-line.muted {
opacity: 0.28;
- /* Smaller text so muted "noise" lines read as lighter/shorter. The ROW height stays
- var(--line-height) — the virtual scroller needs a uniform per-line height — so this
- shrinks the text within the row, not the row itself. Full size + opacity on hover. */
- font-size: 0.78em;
transition: opacity 0.12s ease, font-size 0.1s ease;
}
-.log-line.muted .line-number { font-size: 0.85em; }
.log-line.muted:hover {
opacity: 1;
font-size: 1em;
}
+/* Opt-in muted-line SIZE (mute menu → "Muted size"). Default = As is (no class on body).
+ Shrinks the text within the fixed 20px row (the virtual scroller needs a uniform row
+ height, so this does NOT reduce the row height). Full size on hover regardless. */
+.muted-size-small .log-line.muted { font-size: 0.82em; }
+.muted-size-small .log-line.muted .line-number { font-size: 0.88em; }
+.muted-size-tiny .log-line.muted { font-size: 0.66em; }
+.muted-size-tiny .log-line.muted .line-number { font-size: 0.78em; }
.line-number {
min-width: 60px;