Redo connection manager: Stacks, More options, drag & drop, WinSCP import

Folders become Stacks (connections.json v2, no v1 migration): collapsible
stack-first list, open-stack-in-tabs (first reuses a pristine tab), drag
connections between stacks. More... reveals a per-connection Local
directory applied to the other panel when local and untouched. Import
reads a WinSCP.ini export incl. deobfuscated passwords.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-13 18:28:30 +02:00
co-authored by Claude Fable 5
parent 9a4420fdc2
commit e2f3737656
13 changed files with 533 additions and 125 deletions
+24 -4
View File
@@ -8,6 +8,7 @@ import { SftpProvider } from "./fs/sftp";
import type { ConnectionStore } from "./store/connections";
import type { SettingsStore } from "./store/settings";
import { loadSshConfigHosts } from "./store/ssh-config";
import { importWinScpIni } from "./store/winscp-import";
import type { TransferEngine } from "./transfers/engine";
import { removeRecursive } from "./transfers/walk";
@@ -152,20 +153,39 @@ export function createAppRPC({
return {};
}),
upsertFolder: ({ folder }) =>
upsertStack: ({ stack }) =>
wrap(async () => {
const id = await store.upsertFolder(folder);
const id = await store.upsertStack(stack);
events.connectionsChanged();
return { id };
}),
deleteFolder: ({ id }) =>
deleteStack: ({ id }) =>
wrap(async () => {
await store.deleteFolder(id);
await store.deleteStack(id);
events.connectionsChanged();
return {};
}),
importWinScp: () =>
wrap(async () => {
const { homedir } = await import("node:os");
const picked = await Utils.openFileDialog({
startingFolder: homedir(),
allowedFileTypes: "*",
canChooseFiles: true,
canChooseDirectory: false,
allowsMultipleSelection: false,
});
const file = picked.find((p) => p.trim().length > 0);
// dialog cancelled — all-zero result, UI shows nothing
if (!file) return { connections: 0, stacks: 0, skipped: 0 };
const text = await (await import("node:fs/promises")).readFile(file, "utf8");
const result = await importWinScpIni(text, store);
events.connectionsChanged();
return result;
}),
getSshConfigHosts: () => wrap(async () => ({ hosts: loadSshConfigHosts() })),
// ---- settings ----