Files
NetMesh/domain/sftpTransferNavigation.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

151 lines
5.3 KiB
TypeScript

import type { TransferTask } from "./models";
export type SftpTransferNavigationTarget = {
kind: "local-path" | "local-copy-panel" | "remote-host";
hostId?: string;
/** When true, open the source path instead of the target path. */
useSourcePath: boolean;
};
/**
* Decide which endpoint the global transfer center should open.
*
* Resume needs a remote host available for re-auth/reconnect. Opening the
* destination folder after completion should open the target instead.
*
* Important: dual-pane downloads store a real local connection UUID in
* `targetConnectionId`, not the sentinel `"local"`. Prefer direction + host
* ids over connection-id string equality alone.
*
* Uploads always target a remote host even when `targetHostId` is missing
* (older rows / drag-drop without a vault id) — never treat a remote path as
* a local `openPath` target.
*/
export function resolveSftpTransferNavigationTarget(
task: Pick<
TransferTask,
| "direction"
| "sourceHostId"
| "targetHostId"
| "sourceConnectionId"
| "targetConnectionId"
| "sourcePath"
| "targetPath"
| "isDirectory"
>,
forResume: boolean,
): SftpTransferNavigationTarget {
const sourceIsRemote = Boolean(task.sourceHostId)
|| (task.direction === "download" || task.direction === "remote-to-remote");
const targetIsRemote = Boolean(task.targetHostId)
|| task.direction === "upload"
|| task.direction === "remote-to-remote";
const isLocalCopy = task.direction === "local-copy"
|| (
task.direction !== "upload"
&& task.direction !== "download"
&& task.direction !== "remote-to-remote"
&& !task.sourceHostId
&& !task.targetHostId
);
if (forResume) {
if (isLocalCopy) {
return { kind: "local-copy-panel", useSourcePath: false };
}
// Prefer the remote source when present (downloads, remote-to-remote).
// Uploads only have a remote target.
if (sourceIsRemote && task.direction !== "upload") {
return { kind: "remote-host", hostId: task.sourceHostId, useSourcePath: true };
}
return { kind: "remote-host", hostId: task.targetHostId, useSourcePath: false };
}
// Open destination folder: local filesystem for downloads/local-copy;
// remote SFTP panel for uploads and server-to-server targets.
if (task.direction === "upload" || task.direction === "remote-to-remote" || targetIsRemote) {
return { kind: "remote-host", hostId: task.targetHostId, useSourcePath: false };
}
return { kind: "local-path", useSourcePath: false };
}
export function resolveSftpTransferNavigationPath(
task: Pick<TransferTask, "sourcePath" | "targetPath" | "isDirectory">,
useSourcePath: boolean,
): string {
const rawPath = useSourcePath ? task.sourcePath : task.targetPath;
if (task.isDirectory) return rawPath;
return rawPath.replace(/[\\/][^\\/]+$/, "") || "/";
}
/** Host label used when `hostId` is missing and we fall back to name matching. */
export function resolveSftpTransferNavigationHostLabel(
task: Pick<TransferTask, "sourceHostLabel" | "targetHostLabel">,
useSourcePath: boolean,
): string | undefined {
return useSourcePath ? task.sourceHostLabel : task.targetHostLabel;
}
/**
* Top-level tabs that can host the SFTP side panel (session or workspace ids).
* Vault / full-page SFTP / editor tabs are not valid SFTP side-panel scopes.
*/
export function isTransferNavigationTerminalTabId(tabId: string | null | undefined): boolean {
if (!tabId) return false;
if (tabId === "vault" || tabId === "sftp") return false;
if (tabId.startsWith("editor:")) return false;
return true;
}
export type TransferNavigationHostLike = {
id: string;
label?: string;
hostname?: string;
isLocal?: boolean;
};
/**
* Resolve a vault/session host for open-folder / resume routing.
* Prefer id, then label/hostname, then optional live SFTP panel hosts
* (covers drag-drop uploads that never persisted a vault host id).
*/
export function pickHostForTransferNavigation(params: {
hostId?: string;
hostLabel?: string;
vaultHosts: readonly TransferNavigationHostLike[];
/** Currently open SFTP hosts (active tab first). */
liveHosts?: readonly TransferNavigationHostLike[];
/** When true and no id/label match, use the first non-local live host. */
allowLiveUploadFallback?: boolean;
}): TransferNavigationHostLike | null {
const needle = (params.hostLabel || "").trim().toLowerCase();
const matchIn = (hosts: readonly TransferNavigationHostLike[]) => {
if (params.hostId) {
const byId = hosts.find((host) => host.id === params.hostId);
if (byId) return byId;
}
if (needle && needle !== "local") {
const byLabel = hosts.find((host) => {
const label = (host.label || "").trim().toLowerCase();
const hostname = (host.hostname || "").trim().toLowerCase();
return label === needle || hostname === needle;
});
if (byLabel) return byLabel;
}
return null;
};
const fromVault = matchIn(params.vaultHosts);
if (fromVault) return fromVault;
const live = params.liveHosts ?? [];
const fromLive = matchIn(live);
if (fromLive) return fromLive;
if (params.allowLiveUploadFallback) {
const openRemote = live.find((host) => host && host.isLocal !== true && host.id && host.id !== "local");
if (openRemote) return openRemote;
}
return null;
}