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:
@@ -26,6 +26,8 @@ export class PanelState {
|
||||
readonly slowLoad = signal(false);
|
||||
/** Bumping this discards the result of any in-flight connect (cancel/supersede). */
|
||||
connectToken = 0;
|
||||
/** True once the user navigated or connected — a "pristine" panel is still at local home. */
|
||||
touched = false;
|
||||
|
||||
/** Input on this panel is ignored while it's connecting or loading. */
|
||||
readonly busy = computed(() => this.connecting() || this.loading());
|
||||
@@ -73,6 +75,7 @@ export class PanelState {
|
||||
constructor(private deps: PanelDeps) {}
|
||||
|
||||
async load(path: string | null): Promise<void> {
|
||||
if (path !== null) this.touched = true;
|
||||
this.loading.set(true);
|
||||
const slowTimer = setTimeout(() => this.slowLoad.set(true), 300);
|
||||
try {
|
||||
@@ -311,16 +314,29 @@ export class PanelsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** One new tab per connection, its left panel connecting (right stays local). */
|
||||
openInNewTabs(connections: SavedConnection[]): void {
|
||||
for (const connection of connections) {
|
||||
this.newWorkspace();
|
||||
/**
|
||||
* Open a stack: one tab per connection, left panel connecting (right stays
|
||||
* local, jumping to the connection's localDir when set). The first
|
||||
* connection reuses the current tab if it's pristine (local/local, never
|
||||
* navigated) instead of leaving a useless Local tab behind.
|
||||
*/
|
||||
openStack(connections: SavedConnection[]): void {
|
||||
for (const [index, connection] of connections.entries()) {
|
||||
if (index > 0 || !this.workspaceIsPristine(this.activeWorkspace())) {
|
||||
this.newWorkspace();
|
||||
}
|
||||
// connectPanel resolves the panel synchronously (before awaiting), so
|
||||
// firing these back-to-back binds each connection to its own tab
|
||||
void this.connectPanel('left', connection, connection.name);
|
||||
void this.connectPanel('left', connection, connection.name, connection.localDir);
|
||||
}
|
||||
}
|
||||
|
||||
private workspaceIsPristine(workspace: Workspace): boolean {
|
||||
return [workspace.left, workspace.right].every(
|
||||
(p) => p.sessionId() === 'local' && !p.touched && !p.connecting(),
|
||||
);
|
||||
}
|
||||
|
||||
/** Reorder a tab (drag & drop). Order is purely visual; ids stay stable. */
|
||||
moveWorkspace(id: number, toIndex: number): void {
|
||||
this.workspaces.update((list) => {
|
||||
@@ -361,9 +377,19 @@ export class PanelsService {
|
||||
|
||||
// ---- session binding ----
|
||||
|
||||
/** Connect a panel to a server; replaces (and disconnects) its previous remote session. */
|
||||
async connectPanel(side: Side, config: ConnectionConfig, title: string): Promise<boolean> {
|
||||
const panel = this.get(side);
|
||||
/**
|
||||
* Connect a panel to a server; replaces (and disconnects) its previous remote
|
||||
* session. When the connection has a localDir and the workspace's other panel
|
||||
* is local and untouched, that panel jumps to the localDir.
|
||||
*/
|
||||
async connectPanel(
|
||||
side: Side,
|
||||
config: ConnectionConfig,
|
||||
title: string,
|
||||
localDir?: string | null,
|
||||
): Promise<boolean> {
|
||||
const workspace = this.activeWorkspace();
|
||||
const panel = workspace[side];
|
||||
const token = ++panel.connectToken;
|
||||
panel.connecting.set(true);
|
||||
panel.connectingTo.set(title);
|
||||
@@ -385,6 +411,11 @@ export class PanelsService {
|
||||
panel.sessionId.set(res.sessionId);
|
||||
panel.fsKey.set(res.fsKey);
|
||||
panel.title.set(title);
|
||||
panel.touched = true;
|
||||
const other = workspace[side === 'left' ? 'right' : 'left'];
|
||||
if (localDir && other.sessionId() === 'local' && !other.touched) {
|
||||
void other.load(localDir);
|
||||
}
|
||||
await panel.load(config.initialPath ?? res.homeDir);
|
||||
return true;
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user