Skip to content

Commit f71b626

Browse files
ozgesolidkeyclaude
andcommitted
Add recent files dropdown and bump version to 0.7.0
- Recent files stored in ~/.logan/recent-files.json (capped at 20) - Updated on every successful file open with dedupe - New chevron button next to Open File button shows dropdown - Each entry shows filename, directory, and relative time - "Clear history" button to wipe the list - Filters out files that no longer exist on disk Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 58dc1c7 commit f71b626

7 files changed

Lines changed: 218 additions & 4 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "logan",
3-
"version": "0.6.1",
3+
"version": "0.7.0",
44
"description": "AI-powered log file viewer and analyzer — handles 14M+ lines with virtual scrolling, MCP agent integration, live serial/logcat/SSH connections, pattern correlation, diff view, and built-in terminal",
55
"keywords": [
66
"log-analyzer",

src/main/index.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,37 @@ function saveLocalFileData(filePath: string, data: LocalFileData): void {
226226
// Track whether current file uses local storage or fallback
227227
let currentFileUsesLocalStorage = false;
228228

229+
// Recent files (global, for quick re-open)
230+
const RECENT_FILES_CAP = 20;
231+
232+
function getRecentFilesPath(): string {
233+
return path.join(os.homedir(), '.logan', 'recent-files.json');
234+
}
235+
236+
function loadRecentFiles(): Array<{ path: string; lastOpened: number }> {
237+
try {
238+
const data = fs.readFileSync(getRecentFilesPath(), 'utf-8');
239+
const parsed = JSON.parse(data);
240+
if (Array.isArray(parsed)) return parsed;
241+
} catch { /* missing or invalid */ }
242+
return [];
243+
}
244+
245+
function saveRecentFiles(list: Array<{ path: string; lastOpened: number }>): void {
246+
try {
247+
const dir = path.join(os.homedir(), '.logan');
248+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
249+
fs.writeFileSync(getRecentFilesPath(), JSON.stringify(list, null, 2));
250+
} catch { /* ignore */ }
251+
}
252+
253+
function addToRecentFiles(filePath: string): void {
254+
const list = loadRecentFiles().filter(e => e.path !== filePath);
255+
list.unshift({ path: filePath, lastOpened: Date.now() });
256+
if (list.length > RECENT_FILES_CAP) list.length = RECENT_FILES_CAP;
257+
saveRecentFiles(list);
258+
}
259+
229260
// Activity history logging
230261
const ACTIVITY_HISTORY_CAP = 500;
231262
const ACTIVITY_HISTORY_TRIM_TO = 400;
@@ -2269,6 +2300,7 @@ ipcMain.handle(IPC.OPEN_FILE, async (_, filePath: string) => {
22692300
saveLocalFileData(persistPath, localData);
22702301
}
22712302
logActivity(persistPath, 'file_opened', { filePath: persistPath });
2303+
addToRecentFiles(persistPath);
22722304

22732305
// Check for split metadata in file header (preferred)
22742306
const splitMeta = fileHandler.getSplitMetadata();
@@ -5563,6 +5595,21 @@ ipcMain.handle('agent-save-config', async (_event, config: any) => {
55635595
return { success: true };
55645596
});
55655597

5598+
// === Recent Files ===
5599+
5600+
ipcMain.handle('recent-files-list', async () => {
5601+
// Filter out files that no longer exist
5602+
const list = loadRecentFiles().filter(e => {
5603+
try { return fs.existsSync(e.path); } catch { return false; }
5604+
});
5605+
return { success: true, files: list };
5606+
});
5607+
5608+
ipcMain.handle('recent-files-clear', async () => {
5609+
saveRecentFiles([]);
5610+
return { success: true };
5611+
});
5612+
55665613
ipcMain.handle('agent-browse-script', async () => {
55675614
const result = await dialog.showOpenDialog({
55685615
title: 'Select Agent Script',

src/preload/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,12 @@ const api = {
494494
browseAgentScript: (): Promise<string | null> =>
495495
ipcRenderer.invoke('agent-browse-script'),
496496

497+
// Recent files
498+
listRecentFiles: (): Promise<{ success: boolean; files?: Array<{ path: string; lastOpened: number }> }> =>
499+
ipcRenderer.invoke('recent-files-list'),
500+
clearRecentFiles: (): Promise<{ success: boolean }> =>
501+
ipcRenderer.invoke('recent-files-clear'),
502+
497503
// Agent annotations
498504
addAnnotation: (annotation: any): Promise<{ success: boolean }> =>
499505
ipcRenderer.invoke('annotation-add', annotation),

src/renderer/index.html

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,13 @@
2727
<!-- Toolbar -->
2828
<div class="toolbar">
2929
<div class="toolbar-left">
30-
<button id="btn-open-file" class="toolbar-btn" title="Open file" data-help="Open a log file for analysis.&#10;Supports text logs, JSON logs, CSV, and large files (100K+ lines).&#10;Drag & drop also works.">
31-
<span class="icon">&#128194;</span> Open File
32-
</button>
30+
<div class="open-file-wrapper">
31+
<button id="btn-open-file" class="toolbar-btn" title="Open file" data-help="Open a log file for analysis.&#10;Supports text logs, JSON logs, CSV, and large files (100K+ lines).&#10;Drag & drop also works.">
32+
<span class="icon">&#128194;</span> Open File
33+
</button>
34+
<button id="btn-recent-files" class="toolbar-btn small recent-files-chevron" title="Recent files">&#9662;</button>
35+
<div id="recent-files-popup" class="recent-files-popup hidden"></div>
36+
</div>
3337
<div class="separator"></div>
3438
<div class="search-container">
3539
<input type="text" id="search-input" placeholder="Search..." class="toolbar-input">

src/renderer/renderer.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,8 @@ const SLOW_SCROLL_FRAME_COUNT = 10; // Number of slow frames before warning
684684
const elements = {
685685
logo: document.getElementById('logo') as HTMLImageElement,
686686
btnOpenFile: document.getElementById('btn-open-file') as HTMLButtonElement,
687+
btnRecentFiles: document.getElementById('btn-recent-files') as HTMLButtonElement,
688+
recentFilesPopup: document.getElementById('recent-files-popup') as HTMLDivElement,
687689
btnOpenWelcome: document.getElementById('btn-open-welcome') as HTMLButtonElement,
688690
btnSearch: document.getElementById('btn-search') as HTMLButtonElement,
689691
btnPrevResult: document.getElementById('btn-prev-result') as HTMLButtonElement,
@@ -4059,6 +4061,61 @@ async function saveToNotes(): Promise<void> {
40594061
}
40604062

40614063
// File Operations
4064+
// ─── Recent Files Dropdown ──────────────────────────────────────────────
4065+
4066+
async function toggleRecentFilesPopup(): Promise<void> {
4067+
const popup = elements.recentFilesPopup;
4068+
if (!popup.classList.contains('hidden')) {
4069+
popup.classList.add('hidden');
4070+
return;
4071+
}
4072+
4073+
const result = await window.api.listRecentFiles();
4074+
const files = result.files || [];
4075+
4076+
let html = `<div class="recent-files-header">Recent Files</div>`;
4077+
if (files.length === 0) {
4078+
html += `<div class="recent-files-empty">No recent files</div>`;
4079+
} else {
4080+
html += `<div class="recent-files-list">`;
4081+
for (const f of files) {
4082+
const name = f.path.split('/').pop() || f.path;
4083+
const dir = f.path.substring(0, f.path.length - name.length - 1);
4084+
const ageMs = Date.now() - f.lastOpened;
4085+
let ageText: string;
4086+
if (ageMs < 60000) ageText = 'just now';
4087+
else if (ageMs < 3600000) ageText = `${Math.floor(ageMs / 60000)}m ago`;
4088+
else if (ageMs < 86400000) ageText = `${Math.floor(ageMs / 3600000)}h ago`;
4089+
else ageText = `${Math.floor(ageMs / 86400000)}d ago`;
4090+
html += `
4091+
<div class="recent-file-item" data-path="${escapeHtml(f.path)}" title="${escapeHtml(f.path)}">
4092+
<span class="recent-file-name">${escapeHtml(name)}</span>
4093+
<span class="recent-file-meta">${escapeHtml(dir)} · ${ageText}</span>
4094+
</div>`;
4095+
}
4096+
html += `</div>`;
4097+
html += `<div class="recent-files-footer"><button class="recent-files-clear-btn">Clear history</button></div>`;
4098+
}
4099+
4100+
popup.innerHTML = html;
4101+
popup.classList.remove('hidden');
4102+
4103+
// Wire up clicks
4104+
popup.querySelectorAll('.recent-file-item').forEach(item => {
4105+
item.addEventListener('click', async () => {
4106+
const filePath = (item as HTMLElement).dataset.path;
4107+
if (filePath) {
4108+
popup.classList.add('hidden');
4109+
await loadFile(filePath);
4110+
}
4111+
});
4112+
});
4113+
popup.querySelector('.recent-files-clear-btn')?.addEventListener('click', async () => {
4114+
await window.api.clearRecentFiles();
4115+
popup.classList.add('hidden');
4116+
});
4117+
}
4118+
40624119
async function openFile(): Promise<void> {
40634120
const filePath = await window.api.openFileDialog();
40644121
if (!filePath) return;
@@ -12548,6 +12605,17 @@ function init(): void {
1254812605

1254912606
// File operations
1255012607
elements.btnOpenFile.addEventListener('click', openFile);
12608+
elements.btnRecentFiles.addEventListener('click', (e) => {
12609+
e.stopPropagation();
12610+
toggleRecentFilesPopup();
12611+
});
12612+
document.addEventListener('click', (e) => {
12613+
if (!elements.recentFilesPopup.classList.contains('hidden') &&
12614+
!elements.recentFilesPopup.contains(e.target as Node) &&
12615+
!elements.btnRecentFiles.contains(e.target as Node)) {
12616+
elements.recentFilesPopup.classList.add('hidden');
12617+
}
12618+
});
1255112619
elements.btnOpenWelcome.addEventListener('click', openFile);
1255212620

1255312621
// Folder operations

src/renderer/styles.css

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,93 @@ body.platform-darwin .titlebar {
288288
position: relative;
289289
}
290290

291+
/* Recent Files dropdown */
292+
.open-file-wrapper { position: relative; display: inline-flex; align-items: center; }
293+
.recent-files-chevron { padding: 4px 6px; margin-left: -2px; font-size: 10px; }
294+
295+
.recent-files-popup {
296+
position: absolute;
297+
top: 100%;
298+
left: 0;
299+
background-color: var(--bg-secondary);
300+
border: 1px solid var(--border-color);
301+
border-radius: 4px;
302+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
303+
z-index: 1001;
304+
min-width: 360px;
305+
max-width: 500px;
306+
max-height: 400px;
307+
display: flex;
308+
flex-direction: column;
309+
margin-top: 4px;
310+
}
311+
.recent-files-popup.hidden { display: none; }
312+
313+
.recent-files-header {
314+
padding: 8px 12px;
315+
border-bottom: 1px solid var(--border-color);
316+
font-size: 11px;
317+
font-weight: 600;
318+
color: var(--text-secondary);
319+
text-transform: uppercase;
320+
}
321+
322+
.recent-files-list {
323+
flex: 1;
324+
overflow-y: auto;
325+
padding: 4px 0;
326+
}
327+
328+
.recent-file-item {
329+
display: flex;
330+
flex-direction: column;
331+
padding: 6px 12px;
332+
cursor: pointer;
333+
font-size: 12px;
334+
border-left: 2px solid transparent;
335+
}
336+
.recent-file-item:hover {
337+
background-color: var(--bg-tertiary);
338+
border-left-color: var(--accent-color);
339+
}
340+
.recent-file-name {
341+
color: var(--text-primary);
342+
font-weight: 500;
343+
white-space: nowrap;
344+
overflow: hidden;
345+
text-overflow: ellipsis;
346+
}
347+
.recent-file-meta {
348+
color: var(--text-muted);
349+
font-size: 10px;
350+
white-space: nowrap;
351+
overflow: hidden;
352+
text-overflow: ellipsis;
353+
margin-top: 1px;
354+
}
355+
356+
.recent-files-footer {
357+
padding: 6px 12px;
358+
border-top: 1px solid var(--border-color);
359+
display: flex;
360+
justify-content: flex-end;
361+
}
362+
.recent-files-clear-btn {
363+
background: none;
364+
border: none;
365+
color: var(--text-muted);
366+
cursor: pointer;
367+
font-size: 11px;
368+
padding: 2px 6px;
369+
}
370+
.recent-files-clear-btn:hover { color: var(--error-color, #e88080); }
371+
.recent-files-empty {
372+
padding: 16px 12px;
373+
text-align: center;
374+
font-size: 11px;
375+
color: var(--text-muted);
376+
}
377+
291378
.search-options-popup {
292379
position: fixed;
293380
background-color: var(--bg-secondary);

src/renderer/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,8 @@ interface Api {
459459
}>;
460460
saveAgentConfig: (config: { type: 'claude-code' | 'builtin' | 'custom' | 'local-llm'; scriptPath?: string; model?: string; llmEndpoint?: string; llmModel?: string }) => Promise<{ success: boolean }>;
461461
browseAgentScript: () => Promise<string | null>;
462+
listRecentFiles: () => Promise<{ success: boolean; files?: Array<{ path: string; lastOpened: number }> }>;
463+
clearRecentFiles: () => Promise<{ success: boolean }>;
462464

463465
// Agent annotations
464466
addAnnotation: (annotation: { id: string; lineNumber: number; text: string; agentName: string; timestamp: number; severity?: 'info' | 'warning' | 'error' }) => Promise<{ success: boolean }>;

0 commit comments

Comments
 (0)