|
|
|
@@ -2,7 +2,8 @@
|
|
|
|
|
* SftpSidePanel - SFTP file browser rendered as a resizable side panel
|
|
|
|
|
*
|
|
|
|
|
* Reuses SftpView's components (SftpPaneView, SftpContextProvider, etc.)
|
|
|
|
|
* to provide a unified SFTP experience. Renders a single pane (left side only).
|
|
|
|
|
* to provide a unified SFTP experience. Renders remote (left) + local (right)
|
|
|
|
|
* so transfers work beside the terminal without opening the full SFTP page.
|
|
|
|
|
*
|
|
|
|
|
* IMPORTANT: Does NOT use the global activeTabStore to avoid conflicts with
|
|
|
|
|
* the main SftpView tab. Instead manages pane visibility internally.
|
|
|
|
@@ -91,6 +92,7 @@ import {
|
|
|
|
|
resolveSftpSidePanelTrackedSourceStatusUpdate,
|
|
|
|
|
shouldAcceptPendingSftpUpload,
|
|
|
|
|
shouldDeferSftpSidePanelAutoConnectForSession,
|
|
|
|
|
shouldEnsureSftpSidePanelCompanionLocal,
|
|
|
|
|
shouldRebindSftpSidePanelSourceSession,
|
|
|
|
|
shouldSkipSftpSidePanelAutoConnect,
|
|
|
|
|
} from "./sftp/sftpSidePanelAutoConnect";
|
|
|
|
@@ -331,7 +333,34 @@ const SftpSidePanelInner: React.FC<SftpSidePanelProps> = ({
|
|
|
|
|
const sftpRef = useRef(sftp);
|
|
|
|
|
sftpRef.current = sftp;
|
|
|
|
|
|
|
|
|
|
const { getConnectionCacheKey, leftPane } = sftp;
|
|
|
|
|
const { getConnectionCacheKey, leftPane, rightPane } = sftp;
|
|
|
|
|
|
|
|
|
|
const ensureCompanionLocal = useCallback(() => {
|
|
|
|
|
const s = sftpRef.current;
|
|
|
|
|
if (
|
|
|
|
|
!shouldEnsureSftpSidePanelCompanionLocal({
|
|
|
|
|
remoteConnection: s.leftPane.connection,
|
|
|
|
|
companionConnection: s.rightPane.connection,
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const rightConn = s.rightPane.connection;
|
|
|
|
|
if (rightConn) {
|
|
|
|
|
void s.disconnect("right").then(() => {
|
|
|
|
|
if (
|
|
|
|
|
shouldEnsureSftpSidePanelCompanionLocal({
|
|
|
|
|
remoteConnection: sftpRef.current.leftPane.connection,
|
|
|
|
|
companionConnection: sftpRef.current.rightPane.connection,
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
sftpRef.current.connect("right", "local");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
s.connect("right", "local");
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
/** Per-task locks so resume-all can prepare multiple transfers sequentially. */
|
|
|
|
@@ -632,6 +661,7 @@ const SftpSidePanelInner: React.FC<SftpSidePanelProps> = ({
|
|
|
|
|
activeSessionId,
|
|
|
|
|
activeSessionStatus,
|
|
|
|
|
});
|
|
|
|
|
ensureCompanionLocal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Defer advancing the session cursor while interactive work blocks rebind,
|
|
|
|
@@ -691,6 +721,7 @@ const SftpSidePanelInner: React.FC<SftpSidePanelProps> = ({
|
|
|
|
|
path,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
ensureCompanionLocal();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -784,7 +815,8 @@ const SftpSidePanelInner: React.FC<SftpSidePanelProps> = ({
|
|
|
|
|
tabConnectionKeyMapRef.current.set(tabId, connectionKey);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}, [activeHost, activeSessionId, initialLocation, interactiveWorkActive, sessions]);
|
|
|
|
|
ensureCompanionLocal();
|
|
|
|
|
}, [activeHost, activeSessionId, ensureCompanionLocal, initialLocation, interactiveWorkActive, sessions]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!activeHost || !isVisible) return;
|
|
|
|
@@ -799,6 +831,19 @@ const SftpSidePanelInner: React.FC<SftpSidePanelProps> = ({
|
|
|
|
|
};
|
|
|
|
|
}, [activeHost, activeSessionId, interactiveWorkActive, isVisible, runAutoConnect]);
|
|
|
|
|
|
|
|
|
|
// Remote connect() is async; open the local companion once the left pane is live.
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isVisible) return;
|
|
|
|
|
ensureCompanionLocal();
|
|
|
|
|
}, [
|
|
|
|
|
ensureCompanionLocal,
|
|
|
|
|
isVisible,
|
|
|
|
|
leftPane.connection?.isLocal,
|
|
|
|
|
leftPane.connection?.status,
|
|
|
|
|
rightPane.connection?.isLocal,
|
|
|
|
|
rightPane.connection?.status,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (activeSessionId) return;
|
|
|
|
|
const trackedSessionId = lastSourceSessionIdRef.current;
|
|
|
|
@@ -991,7 +1036,6 @@ const SftpSidePanelInner: React.FC<SftpSidePanelProps> = ({
|
|
|
|
|
behaviorRef={behaviorRef}
|
|
|
|
|
autoSyncRef={autoSyncRef}
|
|
|
|
|
connectedHostObjRef={connectedHostObjRef}
|
|
|
|
|
connectedKeyRef={connectedKeyRef}
|
|
|
|
|
onInteractiveWorkChange={setInteractiveWorkActive}
|
|
|
|
|
listSftp={listSftp}
|
|
|
|
|
mkdirLocal={mkdirLocal}
|
|
|
|
@@ -1039,7 +1083,6 @@ type SftpSidePanelInteractiveBodyProps = {
|
|
|
|
|
behaviorRef: MutableRefObject<"open" | "transfer">;
|
|
|
|
|
autoSyncRef: MutableRefObject<boolean>;
|
|
|
|
|
connectedHostObjRef: MutableRefObject<Host | null>;
|
|
|
|
|
connectedKeyRef: MutableRefObject<string | null>;
|
|
|
|
|
onInteractiveWorkChange: (active: boolean) => void;
|
|
|
|
|
listSftp: ReturnType<typeof useSftpBackend>["listSftp"];
|
|
|
|
|
mkdirLocal: ReturnType<typeof useSftpBackend>["mkdirLocal"];
|
|
|
|
@@ -1079,7 +1122,6 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
behaviorRef,
|
|
|
|
|
autoSyncRef,
|
|
|
|
|
connectedHostObjRef,
|
|
|
|
|
connectedKeyRef,
|
|
|
|
|
onInteractiveWorkChange,
|
|
|
|
|
listSftp,
|
|
|
|
|
mkdirLocal,
|
|
|
|
@@ -1095,6 +1137,7 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
const dialogActionScopeIdRef = useRef(`sftp-side-panel:${crypto.randomUUID()}`);
|
|
|
|
|
const terminalBackend = useTerminalBackend();
|
|
|
|
|
const [hasPaneFocus, setHasPaneFocus] = useState(false);
|
|
|
|
|
const [focusedSide, setFocusedSide] = useState<"left" | "right">("left");
|
|
|
|
|
const [pendingFollowOverride, setPendingFollowOverride] = useState<{
|
|
|
|
|
hostId: string;
|
|
|
|
|
value: boolean;
|
|
|
|
@@ -1112,24 +1155,26 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
const getOpenerForFileRef = useRef(getOpenerForFile);
|
|
|
|
|
getOpenerForFileRef.current = getOpenerForFile;
|
|
|
|
|
|
|
|
|
|
const handleToggleHiddenFiles = useCallback((paneId: string) => {
|
|
|
|
|
const pane = sftpRef.current.leftTabs.tabs.find((tab) => tab.id === paneId);
|
|
|
|
|
const handleToggleHiddenFiles = useCallback((side: "left" | "right", paneId: string) => {
|
|
|
|
|
const tabs = side === "left" ? sftpRef.current.leftTabs.tabs : sftpRef.current.rightTabs.tabs;
|
|
|
|
|
const pane = tabs.find((tab) => tab.id === paneId);
|
|
|
|
|
if (!pane) return;
|
|
|
|
|
sftpRef.current.setShowHiddenFiles("left", paneId, !pane.showHiddenFiles);
|
|
|
|
|
sftpRef.current.setShowHiddenFiles(side, paneId, !pane.showHiddenFiles);
|
|
|
|
|
}, [sftpRef]);
|
|
|
|
|
|
|
|
|
|
const syncFocusedSelection = useCallback((tabId: string | null) => {
|
|
|
|
|
const syncFocusedSelection = useCallback((side: "left" | "right", tabId: string | null) => {
|
|
|
|
|
if (tabId) {
|
|
|
|
|
keepOnlyPaneSelections(sftpRef.current, { side: "left", tabId });
|
|
|
|
|
keepOnlyPaneSelections(sftpRef.current, { side, tabId });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
keepOnlyPaneSelections(sftpRef.current, null);
|
|
|
|
|
}, [sftpRef]);
|
|
|
|
|
|
|
|
|
|
const handlePaneFocus = useCallback(() => {
|
|
|
|
|
sftpFocusStore.setFocusedSide("left");
|
|
|
|
|
const handlePaneFocus = useCallback((side: "left" | "right") => {
|
|
|
|
|
sftpFocusStore.setFocusedSide(side);
|
|
|
|
|
setFocusedSide(side);
|
|
|
|
|
setHasPaneFocus(true);
|
|
|
|
|
syncFocusedSelection(sftpRef.current.getActiveTabId("left"));
|
|
|
|
|
syncFocusedSelection(side, sftpRef.current.getActiveTabId(side));
|
|
|
|
|
}, [sftpRef, syncFocusedSelection]);
|
|
|
|
|
|
|
|
|
|
// NOTE: We intentionally do NOT sync to activeTabStore here.
|
|
|
|
@@ -1148,12 +1193,15 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (panelRootRef.current?.contains(target)) {
|
|
|
|
|
sftpFocusStore.setFocusedSide("left");
|
|
|
|
|
const sideAttr = elementTarget?.closest("[data-sftp-side]")?.getAttribute("data-sftp-side");
|
|
|
|
|
const side: "left" | "right" = sideAttr === "right" ? "right" : "left";
|
|
|
|
|
sftpFocusStore.setFocusedSide(side);
|
|
|
|
|
setFocusedSide(side);
|
|
|
|
|
setHasPaneFocus(true);
|
|
|
|
|
syncFocusedSelection(sftpRef.current.getActiveTabId("left"));
|
|
|
|
|
syncFocusedSelection(side, sftpRef.current.getActiveTabId(side));
|
|
|
|
|
} else {
|
|
|
|
|
setHasPaneFocus(false);
|
|
|
|
|
syncFocusedSelection(null);
|
|
|
|
|
syncFocusedSelection("left", null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
@@ -1203,6 +1251,7 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
leftPanes,
|
|
|
|
|
rightPanes,
|
|
|
|
|
showHostPickerLeft,
|
|
|
|
|
showHostPickerRight,
|
|
|
|
|
hostSearchLeft,
|
|
|
|
@@ -1693,27 +1742,81 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
|
|
|
|
|
const MAX_VISIBLE_TRANSFERS = 5;
|
|
|
|
|
const visibleTransfers = useMemo(() => {
|
|
|
|
|
const connection = sftp.leftPane.connection;
|
|
|
|
|
if (!connection) return [];
|
|
|
|
|
// Filter transfers to those relevant to the active connection's host,
|
|
|
|
|
const leftConnection = sftp.leftPane.connection;
|
|
|
|
|
const rightConnection = sftp.rightPane.connection;
|
|
|
|
|
if (!leftConnection && !rightConnection) return [];
|
|
|
|
|
const panes = [leftConnection, rightConnection];
|
|
|
|
|
const remotes = panes.filter((connection): connection is NonNullable<typeof connection> => (
|
|
|
|
|
!!connection && !connection.isLocal
|
|
|
|
|
));
|
|
|
|
|
// Filter transfers to those relevant to the active connections' hosts,
|
|
|
|
|
// so workspace focus switches don't show transfers from other hosts.
|
|
|
|
|
const filtered = sftp.transfers.filter((t) => {
|
|
|
|
|
if (t.parentTaskId) return false; // Child tasks rendered by SftpTransferQueue
|
|
|
|
|
if (connection.isLocal) {
|
|
|
|
|
return t.sourceConnectionId === connection.id || t.targetConnectionId === connection.id;
|
|
|
|
|
const filtered = sftp.transfers.filter((task) => {
|
|
|
|
|
if (task.parentTaskId) return false; // Child tasks rendered by SftpTransferQueue
|
|
|
|
|
const matchesActiveRemoteSource = remotes.some((remote) => (
|
|
|
|
|
task.sourceConnectionId === remote.id
|
|
|
|
|
|| (task.sourceHostId != null && task.sourceHostId === remote.hostId)
|
|
|
|
|
));
|
|
|
|
|
for (const connection of panes) {
|
|
|
|
|
if (!connection) continue;
|
|
|
|
|
if (connection.isLocal) {
|
|
|
|
|
if (task.sourceConnectionId === connection.id) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// Downloads may use the shared "local" sentinel or the companion local
|
|
|
|
|
// pane UUID. Require an active remote source either way so another
|
|
|
|
|
// host's history does not leak after the left pane switches hosts.
|
|
|
|
|
if (
|
|
|
|
|
(task.targetConnectionId === "local" || task.targetConnectionId === connection.id)
|
|
|
|
|
&& matchesActiveRemoteSource
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
task.targetHostId === connection.hostId
|
|
|
|
|
|| task.sourceConnectionId === connection.id
|
|
|
|
|
|| task.targetConnectionId === connection.id
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return t.targetHostId === connection.hostId || t.sourceConnectionId === connection.id || t.targetConnectionId === connection.id;
|
|
|
|
|
return false;
|
|
|
|
|
});
|
|
|
|
|
return [...filtered].reverse().slice(0, MAX_VISIBLE_TRANSFERS);
|
|
|
|
|
}, [sftp.transfers, sftp.leftPane.connection]);
|
|
|
|
|
}, [sftp.transfers, sftp.leftPane.connection, sftp.rightPane.connection]);
|
|
|
|
|
|
|
|
|
|
const findRemoteTransferTargetTab = useCallback((task: TransferTask) => {
|
|
|
|
|
const state = sftpRef.current;
|
|
|
|
|
for (const side of ["left", "right"] as const) {
|
|
|
|
|
const tabs = side === "left" ? state.leftTabs.tabs : state.rightTabs.tabs;
|
|
|
|
|
const pane = tabs.find((tab) => tab.connection?.id === task.targetConnectionId);
|
|
|
|
|
if (pane?.connection && !pane.connection.isLocal) {
|
|
|
|
|
return { side, tabId: pane.id };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}, [sftpRef]);
|
|
|
|
|
|
|
|
|
|
// Dual-pane downloads store the local pane connection UUID, not the "local" sentinel.
|
|
|
|
|
const isLocalFilesystemTransferTarget = useCallback((task: TransferTask) => {
|
|
|
|
|
if (task.targetConnectionId === "local") return true;
|
|
|
|
|
const state = sftpRef.current;
|
|
|
|
|
for (const side of ["left", "right"] as const) {
|
|
|
|
|
const tabs = side === "left" ? state.leftTabs.tabs : state.rightTabs.tabs;
|
|
|
|
|
const pane = tabs.find((tab) => tab.connection?.id === task.targetConnectionId);
|
|
|
|
|
if (pane?.connection?.isLocal) return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}, [sftpRef]);
|
|
|
|
|
|
|
|
|
|
const handleRevealTransferTarget = useCallback(
|
|
|
|
|
async (task: TransferTask) => {
|
|
|
|
|
if (!isConcreteTransferTargetPath(task)) return;
|
|
|
|
|
const connection = sftpRef.current.leftPane.connection;
|
|
|
|
|
const revealPath = task.isDirectory ? task.targetPath : getParentPath(task.targetPath);
|
|
|
|
|
|
|
|
|
|
if (task.targetConnectionId === "local") {
|
|
|
|
|
if (isLocalFilesystemTransferTarget(task)) {
|
|
|
|
|
try {
|
|
|
|
|
const result = await openPath(revealPath);
|
|
|
|
|
if (result.success) return;
|
|
|
|
@@ -1724,38 +1827,26 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!connection || connection.isLocal) return;
|
|
|
|
|
|
|
|
|
|
await sftpRef.current.navigateTo("left", revealPath, { force: true });
|
|
|
|
|
const targetTab = findRemoteTransferTargetTab(task);
|
|
|
|
|
if (!targetTab) return;
|
|
|
|
|
await sftpRef.current.navigateTo(targetTab.side, revealPath, {
|
|
|
|
|
force: true,
|
|
|
|
|
tabId: targetTab.tabId,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[openPath, sftpRef, t],
|
|
|
|
|
[findRemoteTransferTargetTab, isLocalFilesystemTransferTarget, openPath, sftpRef, t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const canRevealTransferTarget = useCallback(
|
|
|
|
|
(task: TransferTask) => {
|
|
|
|
|
if (task.status !== "completed") return false;
|
|
|
|
|
if (!isConcreteTransferTargetPath(task)) return false;
|
|
|
|
|
if (task.targetConnectionId === "local") {
|
|
|
|
|
if (isLocalFilesystemTransferTarget(task)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (task.direction !== "upload" && task.direction !== "remote-to-remote") return false;
|
|
|
|
|
|
|
|
|
|
const connection = sftp.leftPane.connection;
|
|
|
|
|
if (!connection || connection.isLocal) return false;
|
|
|
|
|
|
|
|
|
|
if (task.targetHostId) {
|
|
|
|
|
if (connection.hostId !== task.targetHostId) return false;
|
|
|
|
|
// If the transfer recorded a full endpoint key, use it to
|
|
|
|
|
// distinguish same-hostId uploads with different session overrides.
|
|
|
|
|
if (task.targetConnectionKey) {
|
|
|
|
|
return connectedKeyRef.current === task.targetConnectionKey;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return connection.id === task.targetConnectionId;
|
|
|
|
|
return !!findRemoteTransferTargetTab(task);
|
|
|
|
|
},
|
|
|
|
|
[connectedKeyRef, sftp.leftPane.connection],
|
|
|
|
|
[findRemoteTransferTargetTab, isLocalFilesystemTransferTarget],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const canCopyTransferTargetPath = useCallback(
|
|
|
|
@@ -1776,8 +1867,9 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
[t],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Determine the active pane to render (without using global activeTabStore)
|
|
|
|
|
// Determine the active panes to render (without using global activeTabStore)
|
|
|
|
|
const activeLeftPaneId = sftp.leftTabs.activeTabId;
|
|
|
|
|
const activeRightPaneId = sftp.rightTabs.activeTabId;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<SftpContextProvider
|
|
|
|
@@ -1794,7 +1886,6 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
ref={panelRootRef}
|
|
|
|
|
className="h-full flex flex-col bg-background overflow-hidden"
|
|
|
|
|
data-section="terminal-sftp-panel"
|
|
|
|
|
onClick={handlePaneFocus}
|
|
|
|
|
>
|
|
|
|
|
{showWorkspaceHostHeader && displayHost && (
|
|
|
|
|
<div
|
|
|
|
@@ -1827,35 +1918,71 @@ const SftpSidePanelInteractiveBody: React.FC<SftpSidePanelInteractiveBodyProps>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{/* File browser pane - render only the active pane */}
|
|
|
|
|
<div className="relative flex-1 min-h-0">
|
|
|
|
|
{leftPanes.map((pane, idx) => {
|
|
|
|
|
// Manage visibility locally instead of via activeTabStore
|
|
|
|
|
const isActive = activeLeftPaneId
|
|
|
|
|
? pane.id === activeLeftPaneId
|
|
|
|
|
: idx === 0;
|
|
|
|
|
if (!isActive) return null;
|
|
|
|
|
{/* Remote (left) + local (right): stack when panel is narrow, side-by-side when wide.
|
|
|
|
|
Outer establishes @container; inner uses @min-* (queries an ancestor, not self). */}
|
|
|
|
|
<div className="@container relative flex-1 min-h-0">
|
|
|
|
|
<div className="h-full min-h-0 flex flex-col @min-[420px]:flex-row">
|
|
|
|
|
<div
|
|
|
|
|
data-sftp-side="left"
|
|
|
|
|
className="relative flex-1 min-h-0 border-b @min-[420px]:border-b-0 @min-[420px]:border-r border-border/70"
|
|
|
|
|
onClick={() => handlePaneFocus("left")}
|
|
|
|
|
>
|
|
|
|
|
{leftPanes.map((pane, idx) => {
|
|
|
|
|
const isActive = activeLeftPaneId
|
|
|
|
|
? pane.id === activeLeftPaneId
|
|
|
|
|
: idx === 0;
|
|
|
|
|
if (!isActive) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={pane.id} className="absolute inset-0 z-10">
|
|
|
|
|
<SftpPaneView
|
|
|
|
|
side="left"
|
|
|
|
|
pane={pane}
|
|
|
|
|
dialogActionScopeId={dialogActionScopeIdRef.current}
|
|
|
|
|
isPaneFocused={hasPaneFocus}
|
|
|
|
|
sftpDefaultViewMode={sftpDefaultViewMode}
|
|
|
|
|
showHeader
|
|
|
|
|
showEmptyHeader
|
|
|
|
|
forceActive
|
|
|
|
|
onToggleShowHiddenFiles={() => handleToggleHiddenFiles(pane.id)}
|
|
|
|
|
onGoToTerminalCwd={onGetTerminalCwd ? handleGoToTerminalCwd : undefined}
|
|
|
|
|
onLocatePathInTerminal={canLocatePathInTerminal ? handleLocatePathInTerminal : undefined}
|
|
|
|
|
followTerminalCwd={canFollowTerminalCwd ? effectiveFollowTerminalCwd : undefined}
|
|
|
|
|
onToggleFollowTerminalCwd={canFollowTerminalCwd ? handleToggleFollowTerminalCwd : undefined}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
return (
|
|
|
|
|
<div key={pane.id} className="absolute inset-0 z-10">
|
|
|
|
|
<SftpPaneView
|
|
|
|
|
side="left"
|
|
|
|
|
pane={pane}
|
|
|
|
|
dialogActionScopeId={dialogActionScopeIdRef.current}
|
|
|
|
|
isPaneFocused={hasPaneFocus && focusedSide === "left"}
|
|
|
|
|
sftpDefaultViewMode={sftpDefaultViewMode}
|
|
|
|
|
showHeader
|
|
|
|
|
showEmptyHeader
|
|
|
|
|
forceActive
|
|
|
|
|
onToggleShowHiddenFiles={() => handleToggleHiddenFiles("left", pane.id)}
|
|
|
|
|
onGoToTerminalCwd={onGetTerminalCwd ? handleGoToTerminalCwd : undefined}
|
|
|
|
|
onLocatePathInTerminal={canLocatePathInTerminal ? handleLocatePathInTerminal : undefined}
|
|
|
|
|
followTerminalCwd={canFollowTerminalCwd ? effectiveFollowTerminalCwd : undefined}
|
|
|
|
|
onToggleFollowTerminalCwd={canFollowTerminalCwd ? handleToggleFollowTerminalCwd : undefined}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
data-sftp-side="right"
|
|
|
|
|
className="relative flex-1 min-h-0"
|
|
|
|
|
onClick={() => handlePaneFocus("right")}
|
|
|
|
|
>
|
|
|
|
|
{rightPanes.map((pane, idx) => {
|
|
|
|
|
const isActive = activeRightPaneId
|
|
|
|
|
? pane.id === activeRightPaneId
|
|
|
|
|
: idx === 0;
|
|
|
|
|
if (!isActive) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={pane.id} className="absolute inset-0 z-10">
|
|
|
|
|
<SftpPaneView
|
|
|
|
|
side="right"
|
|
|
|
|
pane={pane}
|
|
|
|
|
dialogActionScopeId={dialogActionScopeIdRef.current}
|
|
|
|
|
isPaneFocused={hasPaneFocus && focusedSide === "right"}
|
|
|
|
|
sftpDefaultViewMode={sftpDefaultViewMode}
|
|
|
|
|
showHeader
|
|
|
|
|
showEmptyHeader
|
|
|
|
|
forceActive
|
|
|
|
|
onToggleShowHiddenFiles={() => handleToggleHiddenFiles("right", pane.id)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<SftpTransferQueue
|
|
|
|
|
sftp={sftp}
|
|
|
|
|