Skip to content

Commit dd6624b

Browse files
ozgesolidkeyclaude
andcommitted
Add Recent Folders button; make Recent Files button clearer
- Recent Files: chevron inside the open-file button replaced with a standalone clock-icon button that's always visible and clearly distinct - Recent Folders: same clock-icon button added next to the Add Folder button in the Folders panel, with its own popup (same style as files) - Recent folders are persisted to ~/.logan/recent-folders.json (cap 15), tracked whenever a folder is picked via the open-folder dialog - Clicking a recent folder re-opens it into the folder panel without going through the OS file picker Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b165452 commit dd6624b

6 files changed

Lines changed: 162 additions & 24 deletions

File tree

src/main/index.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,37 @@ function addToRecentFiles(filePath: string): void {
257257
saveRecentFiles(list);
258258
}
259259

260+
// Recent folders
261+
const RECENT_FOLDERS_CAP = 15;
262+
263+
function getRecentFoldersPath(): string {
264+
return path.join(os.homedir(), '.logan', 'recent-folders.json');
265+
}
266+
267+
function loadRecentFolders(): Array<{ path: string; lastOpened: number }> {
268+
try {
269+
const data = fs.readFileSync(getRecentFoldersPath(), 'utf-8');
270+
const parsed = JSON.parse(data);
271+
if (Array.isArray(parsed)) return parsed;
272+
} catch { /* missing or invalid */ }
273+
return [];
274+
}
275+
276+
function saveRecentFolders(list: Array<{ path: string; lastOpened: number }>): void {
277+
try {
278+
const dir = path.join(os.homedir(), '.logan');
279+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
280+
fs.writeFileSync(getRecentFoldersPath(), JSON.stringify(list, null, 2));
281+
} catch { /* ignore */ }
282+
}
283+
284+
function addToRecentFolders(folderPath: string): void {
285+
const list = loadRecentFolders().filter(e => e.path !== folderPath);
286+
list.unshift({ path: folderPath, lastOpened: Date.now() });
287+
if (list.length > RECENT_FOLDERS_CAP) list.length = RECENT_FOLDERS_CAP;
288+
saveRecentFolders(list);
289+
}
290+
260291
// Activity history logging
261292
const ACTIVITY_HISTORY_CAP = 500;
262293
const ACTIVITY_HISTORY_TRIM_TO = 400;
@@ -1872,7 +1903,9 @@ ipcMain.handle(IPC.OPEN_FOLDER_DIALOG, async () => {
18721903
const result = await showOpenDialog({
18731904
properties: ['openDirectory'],
18741905
});
1875-
return result.canceled ? null : result.filePaths[0];
1906+
if (result.canceled || !result.filePaths[0]) return null;
1907+
addToRecentFolders(result.filePaths[0]);
1908+
return result.filePaths[0];
18761909
});
18771910

18781911
// Text extensions used by folder search (ripgrep glob filters)
@@ -5730,6 +5763,20 @@ ipcMain.handle('recent-files-clear', async () => {
57305763
return { success: true };
57315764
});
57325765

5766+
// === Recent Folders ===
5767+
5768+
ipcMain.handle('recent-folders-list', async () => {
5769+
const list = loadRecentFolders().filter(e => {
5770+
try { return fs.existsSync(e.path); } catch { return false; }
5771+
});
5772+
return { success: true, folders: list };
5773+
});
5774+
5775+
ipcMain.handle('recent-folders-clear', async () => {
5776+
saveRecentFolders([]);
5777+
return { success: true };
5778+
});
5779+
57335780
ipcMain.handle('agent-browse-script', async () => {
57345781
const result = await dialog.showOpenDialog({
57355782
title: 'Select Agent Script',

src/preload/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,12 @@ const api = {
503503
clearRecentFiles: (): Promise<{ success: boolean }> =>
504504
ipcRenderer.invoke('recent-files-clear'),
505505

506+
// Recent folders
507+
listRecentFolders: (): Promise<{ success: boolean; folders?: Array<{ path: string; lastOpened: number }> }> =>
508+
ipcRenderer.invoke('recent-folders-list'),
509+
clearRecentFolders: (): Promise<{ success: boolean }> =>
510+
ipcRenderer.invoke('recent-folders-clear'),
511+
506512
// Agent annotations
507513
addAnnotation: (annotation: any): Promise<{ success: boolean }> =>
508514
ipcRenderer.invoke('annotation-add', annotation),

src/renderer/index.html

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
<div id="tab-bar" class="tab-bar">
3030
<div class="tab-bar-left">
3131
<div class="open-file-wrapper">
32-
<button id="btn-open-file" class="tab-bar-btn" title="Open file (Ctrl+O) | Recent (right-click)">
32+
<button id="btn-open-file" class="tab-bar-btn" title="Open file (Ctrl+O)">
3333
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
34-
<span id="btn-recent-files" class="open-file-chevron" title="Recent files">&#9662;</span>
34+
</button>
35+
<button id="btn-recent-files" class="tab-bar-btn recent-btn" title="Recent files">
36+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
3537
</button>
3638
<div id="recent-files-popup" class="recent-files-popup hidden"></div>
3739
</div>
@@ -125,9 +127,15 @@
125127
<!-- Folders Panel -->
126128
<div class="panel-view" id="panel-folders" data-panel="folders">
127129
<div class="section-header-inline">
128-
<button id="btn-add-folder" class="section-btn" title="Open local folder">
129-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>
130-
</button>
130+
<div class="open-folder-wrapper">
131+
<button id="btn-add-folder" class="section-btn" title="Open local folder">
132+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/></svg>
133+
</button>
134+
<button id="btn-recent-folders" class="section-btn recent-btn" title="Recent folders">
135+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
136+
</button>
137+
<div id="recent-folders-popup" class="recent-files-popup hidden"></div>
138+
</div>
131139
<button id="btn-open-ssh-folder" class="section-btn" title="Browse SSH remote files (requires active SSH connection in Live panel)">
132140
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="2"/><path d="M7 8l4 4-4 4"/><line x1="13" y1="16" x2="17" y2="16"/></svg>
133141
</button>

src/renderer/renderer.ts

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ const SLOW_SCROLL_FRAME_COUNT = 10; // Number of slow frames before warning
690690
const elements = {
691691
logo: document.getElementById('logo') as HTMLImageElement,
692692
btnOpenFile: document.getElementById('btn-open-file') as HTMLButtonElement,
693-
btnRecentFiles: document.getElementById('btn-recent-files') as HTMLSpanElement,
693+
btnRecentFiles: document.getElementById('btn-recent-files') as HTMLButtonElement,
694694
recentFilesPopup: document.getElementById('recent-files-popup') as HTMLDivElement,
695695
btnOpenWelcome: document.getElementById('btn-open-welcome') as HTMLButtonElement,
696696
btnSearch: document.getElementById('btn-search') as HTMLButtonElement,
@@ -725,6 +725,8 @@ const elements = {
725725
markdownPreview: document.getElementById('markdown-preview') as HTMLDivElement,
726726
foldersList: document.getElementById('folders-list') as HTMLDivElement,
727727
btnAddFolder: document.getElementById('btn-add-folder') as HTMLButtonElement,
728+
btnRecentFolders: document.getElementById('btn-recent-folders') as HTMLButtonElement,
729+
recentFoldersPopup: document.getElementById('recent-folders-popup') as HTMLDivElement,
728730
btnRefreshFolders: document.getElementById('btn-refresh-folders') as HTMLButtonElement, // May be null — per-folder refresh buttons used instead
729731
folderSearchInput: document.getElementById('folder-search-input') as HTMLInputElement,
730732
btnFolderSearch: document.getElementById('btn-folder-search') as HTMLButtonElement,
@@ -4237,6 +4239,74 @@ async function openFile(): Promise<void> {
42374239
await loadFile(filePath);
42384240
}
42394241

4242+
async function toggleRecentFoldersPopup(): Promise<void> {
4243+
const popup = elements.recentFoldersPopup;
4244+
if (!popup.classList.contains('hidden')) {
4245+
popup.classList.add('hidden');
4246+
return;
4247+
}
4248+
4249+
const result = await window.api.listRecentFolders();
4250+
const folders = result.folders || [];
4251+
4252+
let html = `<div class="recent-files-header">Recent Folders</div>`;
4253+
if (folders.length === 0) {
4254+
html += `<div class="recent-files-empty">No recent folders</div>`;
4255+
} else {
4256+
html += `<div class="recent-files-list">`;
4257+
for (const f of folders) {
4258+
const parts = f.path.replace(/\\/g, '/').split('/');
4259+
const name = parts.pop() || f.path;
4260+
const parent = parts.join('/') || '/';
4261+
const ageMs = Date.now() - f.lastOpened;
4262+
let ageText: string;
4263+
if (ageMs < 60000) ageText = 'just now';
4264+
else if (ageMs < 3600000) ageText = `${Math.floor(ageMs / 60000)}m ago`;
4265+
else if (ageMs < 86400000) ageText = `${Math.floor(ageMs / 3600000)}h ago`;
4266+
else ageText = `${Math.floor(ageMs / 86400000)}d ago`;
4267+
html += `
4268+
<div class="recent-file-item" data-path="${escapeHtml(f.path)}" title="${escapeHtml(f.path)}">
4269+
<span class="recent-file-name">${escapeHtml(name)}</span>
4270+
<span class="recent-file-meta">${escapeHtml(parent)} · ${ageText}</span>
4271+
</div>`;
4272+
}
4273+
html += `</div>`;
4274+
html += `<div class="recent-files-footer"><button class="recent-files-clear-btn">Clear history</button></div>`;
4275+
}
4276+
4277+
popup.innerHTML = html;
4278+
4279+
const btnRect = elements.btnRecentFolders.getBoundingClientRect();
4280+
popup.style.top = `${btnRect.bottom + 4}px`;
4281+
popup.style.left = `${btnRect.left}px`;
4282+
popup.classList.remove('hidden');
4283+
4284+
popup.querySelectorAll('.recent-file-item').forEach(item => {
4285+
item.addEventListener('click', async () => {
4286+
const folderPath = (item as HTMLElement).dataset.path;
4287+
if (!folderPath) return;
4288+
popup.classList.add('hidden');
4289+
if (state.folders.some(f => f.path === folderPath)) return;
4290+
const res = await window.api.readFolder(folderPath);
4291+
if (res.success && res.files) {
4292+
const folderName = folderPath.replace(/\\/g, '/').split('/').pop() || folderPath;
4293+
state.folders.push({
4294+
path: folderPath,
4295+
name: folderName,
4296+
files: mapFolderEntries(res.files),
4297+
collapsed: false,
4298+
});
4299+
renderFolderTree();
4300+
updateFolderSearchState();
4301+
}
4302+
});
4303+
});
4304+
popup.querySelector('.recent-files-clear-btn')?.addEventListener('click', async () => {
4305+
await window.api.clearRecentFolders();
4306+
popup.classList.add('hidden');
4307+
});
4308+
}
4309+
42404310
// === Folder Operations ===
42414311

42424312
function mapFolderEntries(entries: any[]): LocalFolderFile[] {
@@ -13437,18 +13507,13 @@ function init(): void {
1343713507
elements.logo.style.cursor = 'pointer';
1343813508

1343913509
// File operations
13440-
elements.btnOpenFile.addEventListener('click', (e) => {
13441-
// Don't open file dialog if the recent files chevron was clicked
13442-
if ((e.target as HTMLElement).id === 'btn-recent-files') return;
13443-
openFile();
13444-
});
13445-
elements.btnRecentFiles.addEventListener('click', (e) => {
13446-
e.stopPropagation();
13510+
elements.btnOpenFile.addEventListener('click', openFile);
13511+
elements.btnOpenFile.addEventListener('contextmenu', (e) => {
1344713512
e.preventDefault();
1344813513
toggleRecentFilesPopup();
1344913514
});
13450-
elements.btnOpenFile.addEventListener('contextmenu', (e) => {
13451-
e.preventDefault();
13515+
elements.btnRecentFiles.addEventListener('click', (e) => {
13516+
e.stopPropagation();
1345213517
toggleRecentFilesPopup();
1345313518
});
1345413519
document.addEventListener('click', (e) => {
@@ -13457,11 +13522,20 @@ function init(): void {
1345713522
!elements.btnRecentFiles.contains(e.target as Node)) {
1345813523
elements.recentFilesPopup.classList.add('hidden');
1345913524
}
13525+
if (!elements.recentFoldersPopup.classList.contains('hidden') &&
13526+
!elements.recentFoldersPopup.contains(e.target as Node) &&
13527+
!elements.btnRecentFolders.contains(e.target as Node)) {
13528+
elements.recentFoldersPopup.classList.add('hidden');
13529+
}
1346013530
});
1346113531
elements.btnOpenWelcome.addEventListener('click', openFile);
1346213532

1346313533
// Folder operations
1346413534
elements.btnAddFolder.addEventListener('click', openFolder);
13535+
elements.btnRecentFolders.addEventListener('click', (e) => {
13536+
e.stopPropagation();
13537+
toggleRecentFoldersPopup();
13538+
});
1346513539
elements.btnRefreshFolders?.addEventListener('click', refreshFolders);
1346613540

1346713541
// Folder search

src/renderer/styles.css

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -285,15 +285,16 @@ body.platform-darwin .titlebar {
285285
position: relative;
286286
}
287287

288-
/* Recent Files dropdown */
289-
.open-file-wrapper { position: relative; display: inline-flex; align-items: center; }
290-
.open-file-chevron {
291-
margin-left: 4px;
292-
font-size: 10px;
293-
opacity: 0.6;
294-
cursor: pointer;
288+
/* Recent Files / Folders dropdowns */
289+
.open-file-wrapper,
290+
.open-folder-wrapper { position: relative; display: inline-flex; align-items: center; }
291+
292+
/* Clock icon button that opens recent list */
293+
.recent-btn {
294+
opacity: 0.5;
295+
padding: 0 4px !important;
295296
}
296-
.open-file-chevron:hover { opacity: 1; }
297+
.recent-btn:hover { opacity: 1 !important; }
297298

298299
.recent-files-popup {
299300
position: fixed;

src/renderer/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,8 @@ interface Api {
462462
browseAgentScript: () => Promise<string | null>;
463463
listRecentFiles: () => Promise<{ success: boolean; files?: Array<{ path: string; lastOpened: number }> }>;
464464
clearRecentFiles: () => Promise<{ success: boolean }>;
465+
listRecentFolders: () => Promise<{ success: boolean; folders?: Array<{ path: string; lastOpened: number }> }>;
466+
clearRecentFolders: () => Promise<{ success: boolean }>;
465467

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

0 commit comments

Comments
 (0)