Skip to content

Commit 2b2268b

Browse files
ozgesolidkeyclaude
andcommitted
Fix SSH remote directory expansion: connection scoping and error display
- SSH_LIST_REMOTE_DIR now accepts connectionId and prefers the specific connection over "first active SSH conn" — prevents wrong connection being used when Live panel has a separate SSH stream running - connectionId stored in folder-group DOM element so expand handler can pass it through without searching state - _loaded flag tracks whether a dir was fetched (distinguishes empty dir from never-fetched); _loadError stores the actual error message - Subdir expand shows: "Loading…" while fetching, "Failed to load" (with error in title) on failure, "Empty" for empty directories instead of the misleading permanent "Loading..." that appeared before - Errors no longer silently swallowed — logged to console and shown in tree Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f18bdab commit 2b2268b

4 files changed

Lines changed: 46 additions & 21 deletions

File tree

src/main/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,18 +1837,22 @@ ipcMain.handle(IPC.CONNECTION_UPDATE, async (_, id: string, fields: Partial<Save
18371837
}
18381838
});
18391839

1840-
ipcMain.handle(IPC.SSH_LIST_REMOTE_DIR, async (_, remotePath: string) => {
1840+
ipcMain.handle(IPC.SSH_LIST_REMOTE_DIR, async (_, remotePath: string, connectionId?: string) => {
18411841
try {
1842-
// Find an active SSH connection to use for SFTP
18431842
let sshConn: LiveConnection | undefined;
1844-
for (const conn of liveConnections.values()) {
1845-
if (conn.source === 'ssh' && conn.connected) {
1846-
sshConn = conn;
1847-
break;
1843+
// Prefer the specific connection if given
1844+
if (connectionId) {
1845+
const c = liveConnections.get(connectionId);
1846+
if (c && c.connected) sshConn = c;
1847+
}
1848+
// Fall back to any active SSH connection
1849+
if (!sshConn) {
1850+
for (const conn of liveConnections.values()) {
1851+
if (conn.source === 'ssh' && conn.connected) { sshConn = conn; break; }
18481852
}
18491853
}
18501854
if (!sshConn) {
1851-
return { success: false, error: 'No active SSH connection for SFTP' };
1855+
return { success: false, error: 'No active SSH connection. Reconnect via SSH Remote Browser.' };
18521856
}
18531857
const files = await (sshConn.handler as any).listRemoteDir(remotePath);
18541858
return { success: true, files };

src/preload/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,8 @@ const api = {
559559
sshTestConnection: (config: { host: string; port: number; username: string; identityFile?: string; password?: string }): Promise<{ success: boolean; error?: string }> =>
560560
ipcRenderer.invoke('ssh-test-connection', config),
561561

562-
sshListRemoteDir: (remotePath: string): Promise<{ success: boolean; files?: any[]; error?: string }> =>
563-
ipcRenderer.invoke(IPC.SSH_LIST_REMOTE_DIR, remotePath),
562+
sshListRemoteDir: (remotePath: string, connectionId?: string): Promise<{ success: boolean; files?: any[]; error?: string }> =>
563+
ipcRenderer.invoke(IPC.SSH_LIST_REMOTE_DIR, remotePath, connectionId),
564564

565565
sshDownloadFile: (remotePath: string): Promise<{ success: boolean; localPath?: string; error?: string }> =>
566566
ipcRenderer.invoke(IPC.SSH_DOWNLOAD_FILE, remotePath),

src/renderer/renderer.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4445,15 +4445,26 @@ function renderFolderEntries(entries: LocalFolderFile[], depth: number, isRemote
44454445
if (entry.isDirectory) {
44464446
const hasChildren = entry.children && entry.children.length > 0;
44474447
const isLoading = (entry as any)._loading;
4448+
const loadError = (entry as any)._loadError as string | undefined;
4449+
let innerHtml = '';
4450+
if (hasChildren) {
4451+
innerHtml = renderFolderEntries(entry.children!, depth + 1, isRemote);
4452+
} else if (!entry.collapsed && isRemote) {
4453+
if (isLoading) {
4454+
innerHtml = `<div class="folder-loading" style="padding-left:${8 + (depth+1) * 14}px">Loading…</div>`;
4455+
} else if (loadError) {
4456+
innerHtml = `<div class="folder-loading" style="padding-left:${8 + (depth+1) * 14}px;color:var(--error-color,#e88080);" title="${escapeHtml(loadError)}">Failed to load</div>`;
4457+
} else if ((entry as any)._loaded) {
4458+
innerHtml = `<div class="folder-loading" style="padding-left:${8 + (depth+1) * 14}px">Empty</div>`;
4459+
}
4460+
}
44484461
return `
44494462
<div class="folder-subdir ${entry.collapsed ? 'collapsed' : ''}" data-subdir-path="${escapeHtml(entry.path)}" ${isRemote ? 'data-remote-dir="true"' : ''}>
44504463
<div class="folder-subdir-header" style="padding-left: ${8 + depth * 14}px">
44514464
<span class="folder-toggle">${isLoading ? '&#8987;' : '&#9660;'}</span>
44524465
<span class="folder-subdir-name" title="${escapeHtml(entry.path)}">${escapeHtml(entry.name)}</span>
44534466
</div>
4454-
<div class="folder-subdir-files">
4455-
${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>' : '')}
4456-
</div>
4467+
<div class="folder-subdir-files">${innerHtml}</div>
44574468
</div>`;
44584469
}
44594470
const icon = entry.fileType === 'image' ? '\uD83D\uDDBC' : entry.fileType === 'video' ? '\u25B6' : '';
@@ -4491,7 +4502,7 @@ function renderFolderTree(): void {
44914502
elements.foldersList.innerHTML = state.folders
44924503
.map(
44934504
(folder) => `
4494-
<div class="folder-group ${folder.collapsed ? 'collapsed' : ''}${folder.isRemote ? ' remote' : ''}" data-path="${escapeHtml(folder.path)}" ${folder.isRemote ? 'data-remote="true"' : ''}>
4505+
<div class="folder-group ${folder.collapsed ? 'collapsed' : ''}${folder.isRemote ? ' remote' : ''}" data-path="${escapeHtml(folder.path)}" ${folder.isRemote ? 'data-remote="true"' : ''} ${folder.connectionId ? `data-connection-id="${escapeHtml(folder.connectionId)}"` : ''}>
44954506
<div class="folder-header">
44964507
<span class="folder-toggle">&#9660;</span>
44974508
<span class="folder-name" title="${escapeHtml(folder.path)}">${folder.isRemote ? '<span class="ssh-badge">SSH</span> ' : ''}${escapeHtml(folder.name)}</span>
@@ -4552,7 +4563,9 @@ function renderFolderTree(): void {
45524563
const subdirEl = header.closest('.folder-subdir') as HTMLElement;
45534564
const subdirPath = subdirEl?.dataset.subdirPath;
45544565
const isRemoteDir = subdirEl?.dataset.remoteDir === 'true';
4555-
const folderGroupPath = (subdirEl?.closest('.folder-group') as HTMLElement)?.dataset.path;
4566+
const folderGroupEl = subdirEl?.closest('.folder-group') as HTMLElement;
4567+
const folderGroupPath = folderGroupEl?.dataset.path;
4568+
const connectionId = folderGroupEl?.dataset.connectionId;
45564569
if (!subdirPath || !folderGroupPath) return;
45574570
const folder = state.folders.find(f => f.path === folderGroupPath);
45584571
if (!folder) return;
@@ -4562,16 +4575,24 @@ function renderFolderTree(): void {
45624575
// Toggle collapse
45634576
node.collapsed = !node.collapsed;
45644577

4565-
// Lazy-load remote subdirectory contents if expanding and no children yet
4566-
if (!node.collapsed && isRemoteDir && (!node.children || node.children.length === 0)) {
4578+
// Lazy-load remote subdirectory contents if expanding and not yet loaded
4579+
if (!node.collapsed && isRemoteDir && !(node as any)._loaded && (!node.children || node.children.length === 0)) {
45674580
(node as any)._loading = true;
4581+
(node as any)._loadError = undefined;
45684582
renderFolderTree();
45694583
try {
4570-
const result = await window.api.sshListRemoteDir(subdirPath);
4571-
if (result.success && result.files) {
4572-
node.children = mapFolderEntries(result.files);
4584+
const result = await window.api.sshListRemoteDir(subdirPath, connectionId);
4585+
if (result.success) {
4586+
node.children = mapFolderEntries(result.files || []);
4587+
(node as any)._loaded = true;
4588+
} else {
4589+
(node as any)._loadError = result.error || 'Failed to list directory';
4590+
console.error('sshListRemoteDir failed:', result.error);
45734591
}
4574-
} catch { /* ignore */ }
4592+
} catch (err) {
4593+
(node as any)._loadError = String(err);
4594+
console.error('sshListRemoteDir error:', err);
4595+
}
45754596
(node as any)._loading = false;
45764597
}
45774598

src/renderer/types.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ interface Api {
487487
sshSaveProfile: (profile: SshProfile) => Promise<{ success: boolean; error?: string }>;
488488
sshDeleteProfile: (id: string) => Promise<{ success: boolean; error?: string }>;
489489
sshTestConnection: (config: { host: string; port: number; username: string; identityFile?: string; password?: string }) => Promise<{ success: boolean; error?: string }>;
490-
sshListRemoteDir: (remotePath: string) => Promise<{ success: boolean; files?: FolderFile[]; error?: string }>;
490+
sshListRemoteDir: (remotePath: string, connectionId?: string) => Promise<{ success: boolean; files?: FolderFile[]; error?: string }>;
491491
sshDownloadFile: (remotePath: string) => Promise<{ success: boolean; localPath?: string; error?: string }>;
492492

493493
// Unified live connection management

0 commit comments

Comments
 (0)