Skip to content

Commit ab287f1

Browse files
ozgesolidkeyclaude
andcommitted
Lazy SFTP tree browsing and on-demand remote file open (v0.8.0)
SSH Remote Browse: - Subdirectories load lazily via SFTP when expanded (not pre-fetched) - Loading indicator (hourglass) while fetching remote dir contents - Remote folders now have a refresh button (re-lists via SFTP) - Remote subdir toggle fetches contents on first expand, caches after SSH Connect Modal: - Standalone modal separate from Live streaming panel - Shows saved profiles + SSH config hosts in clickable list - Manual host/port/username/identity form with remote path input - Connect & Browse establishes SSH and adds remote folder to panel UI: - Major redesign: no toolbar, tab bar + right sidebar layout - VS Code-style floating search panel (Ctrl+F) - Speech balloon annotations on right side - All buttons fixed (null crash from removed element) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 36f8f5c commit ab287f1

File tree

2 files changed

+44
-16
lines changed

2 files changed

+44
-16
lines changed

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.7.3",
3+
"version": "0.8.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/renderer/renderer.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4346,19 +4346,29 @@ async function refreshFolders(): Promise<void> {
43464346

43474347
async function refreshSingleFolder(folderPath: string): Promise<void> {
43484348
const folder = state.folders.find(f => f.path === folderPath);
4349-
if (!folder || folder.isRemote) return;
4349+
if (!folder) return;
43504350

43514351
// Find the refresh button in the DOM and show spinning state
43524352
const groupEl = elements.foldersList.querySelector(`.folder-group[data-path="${CSS.escape(folderPath)}"]`);
43534353
const btn = groupEl?.querySelector('.folder-refresh') as HTMLButtonElement | null;
43544354
if (btn) { btn.disabled = true; btn.textContent = '\u27F3'; }
43554355

43564356
try {
4357-
const result = await window.api.readFolder(folder.path);
4358-
if (result.success && result.files) {
4359-
const newFiles = mapFolderEntries(result.files);
4360-
preserveCollapseState(newFiles, folder.files);
4361-
folder.files = newFiles;
4357+
if (folder.isRemote) {
4358+
// Remote folder — re-list via SFTP
4359+
const result = await window.api.sshListRemoteDir(folder.path);
4360+
if (result.success && result.files) {
4361+
const newFiles = mapFolderEntries(result.files);
4362+
preserveCollapseState(newFiles, folder.files);
4363+
folder.files = newFiles;
4364+
}
4365+
} else {
4366+
const result = await window.api.readFolder(folder.path);
4367+
if (result.success && result.files) {
4368+
const newFiles = mapFolderEntries(result.files);
4369+
preserveCollapseState(newFiles, folder.files);
4370+
folder.files = newFiles;
4371+
}
43624372
}
43634373
renderFolderTree();
43644374
} finally {
@@ -4374,15 +4384,17 @@ function formatFileSize(bytes: number): string {
43744384

43754385
function renderFolderEntries(entries: LocalFolderFile[], depth: number, isRemote: boolean): string {
43764386
return entries.map(entry => {
4377-
if (entry.isDirectory && entry.children) {
4387+
if (entry.isDirectory) {
4388+
const hasChildren = entry.children && entry.children.length > 0;
4389+
const isLoading = (entry as any)._loading;
43784390
return `
4379-
<div class="folder-subdir ${entry.collapsed ? 'collapsed' : ''}" data-subdir-path="${escapeHtml(entry.path)}">
4391+
<div class="folder-subdir ${entry.collapsed ? 'collapsed' : ''}" data-subdir-path="${escapeHtml(entry.path)}" ${isRemote ? 'data-remote-dir="true"' : ''}>
43804392
<div class="folder-subdir-header" style="padding-left: ${8 + depth * 14}px">
4381-
<span class="folder-toggle">&#9660;</span>
4393+
<span class="folder-toggle">${isLoading ? '&#8987;' : '&#9660;'}</span>
43824394
<span class="folder-subdir-name" title="${escapeHtml(entry.path)}">${escapeHtml(entry.name)}</span>
43834395
</div>
43844396
<div class="folder-subdir-files">
4385-
${renderFolderEntries(entry.children, depth + 1, isRemote)}
4397+
${hasChildren ? renderFolderEntries(entry.children!, depth + 1, isRemote) : (!entry.collapsed && isRemote && !isLoading ? '<div class="folder-loading" style="padding-left:' + (8 + (depth+1) * 14) + 'px;font-size:11px;color:var(--text-muted);">Loading...</div>' : '')}
43864398
</div>
43874399
</div>`;
43884400
}
@@ -4425,7 +4437,7 @@ function renderFolderTree(): void {
44254437
<div class="folder-header">
44264438
<span class="folder-toggle">&#9660;</span>
44274439
<span class="folder-name" title="${escapeHtml(folder.path)}">${folder.isRemote ? '<span class="ssh-badge">SSH</span> ' : ''}${escapeHtml(folder.name)}</span>
4428-
${!folder.isRemote ? '<button class="folder-refresh" title="Refresh folder">&#8635;</button>' : ''}
4440+
<button class="folder-refresh" title="Refresh folder">&#8635;</button>
44294441
<button class="folder-close" title="Remove folder">&times;</button>
44304442
</div>
44314443
<div class="folder-files">
@@ -4463,21 +4475,37 @@ function renderFolderTree(): void {
44634475
});
44644476
});
44654477

4466-
// Subfolder toggle events
4478+
// Subfolder toggle events (with lazy remote loading)
44674479
elements.foldersList.querySelectorAll('.folder-subdir-header').forEach((header) => {
4468-
header.addEventListener('click', (e) => {
4480+
header.addEventListener('click', async (e) => {
44694481
e.stopPropagation();
44704482
const subdirEl = header.closest('.folder-subdir') as HTMLElement;
44714483
const subdirPath = subdirEl?.dataset.subdirPath;
4484+
const isRemoteDir = subdirEl?.dataset.remoteDir === 'true';
44724485
const folderGroupPath = (subdirEl?.closest('.folder-group') as HTMLElement)?.dataset.path;
44734486
if (!subdirPath || !folderGroupPath) return;
44744487
const folder = state.folders.find(f => f.path === folderGroupPath);
44754488
if (!folder) return;
44764489
const node = findSubfolder(folder.files, subdirPath);
4477-
if (node) {
4478-
node.collapsed = !node.collapsed;
4490+
if (!node) return;
4491+
4492+
// Toggle collapse
4493+
node.collapsed = !node.collapsed;
4494+
4495+
// Lazy-load remote subdirectory contents if expanding and no children yet
4496+
if (!node.collapsed && isRemoteDir && (!node.children || node.children.length === 0)) {
4497+
(node as any)._loading = true;
44794498
renderFolderTree();
4499+
try {
4500+
const result = await window.api.sshListRemoteDir(subdirPath);
4501+
if (result.success && result.files) {
4502+
node.children = mapFolderEntries(result.files);
4503+
}
4504+
} catch { /* ignore */ }
4505+
(node as any)._loading = false;
44804506
}
4507+
4508+
renderFolderTree();
44814509
});
44824510
});
44834511

0 commit comments

Comments
 (0)