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
261 lines
9.0 KiB
TypeScript
261 lines
9.0 KiB
TypeScript
import React, { useCallback, useMemo, useState } from "react";
|
|
import type { MutableRefObject } from "react";
|
|
import type { Host } from "../../../types";
|
|
import type { SftpStateApi } from "../../../application/state/useSftpState";
|
|
import { editorTabStore } from "../../../application/state/editorTabStore";
|
|
import type { EditorTab, EditorTabId } from "../../../application/state/editorTabStore";
|
|
import { releaseEditorTabSaveCoordinator, saveEditorTab } from "../../../application/state/editorTabSave";
|
|
import { promptUnsavedChanges } from "../../editor/UnsavedChangesDialog";
|
|
import {
|
|
getSftpTabDuplicateRequest,
|
|
type SftpTabDuplicateMode,
|
|
} from "../sftpTabDuplication";
|
|
|
|
interface UseSftpViewTabsParams {
|
|
sftp: SftpStateApi;
|
|
sftpRef: MutableRefObject<SftpStateApi>;
|
|
hosts?: Host[];
|
|
}
|
|
|
|
interface UseSftpViewTabsResult {
|
|
leftPanes: SftpStateApi["leftPane"][];
|
|
rightPanes: SftpStateApi["rightPane"][];
|
|
leftTabsInfo: { id: string; label: string; isLocal: boolean; hostId: string | null; canDuplicate: boolean }[];
|
|
rightTabsInfo: { id: string; label: string; isLocal: boolean; hostId: string | null; canDuplicate: boolean }[];
|
|
showHostPickerLeft: boolean;
|
|
showHostPickerRight: boolean;
|
|
hostSearchLeft: string;
|
|
hostSearchRight: string;
|
|
setShowHostPickerLeft: React.Dispatch<React.SetStateAction<boolean>>;
|
|
setShowHostPickerRight: React.Dispatch<React.SetStateAction<boolean>>;
|
|
setHostSearchLeft: React.Dispatch<React.SetStateAction<string>>;
|
|
setHostSearchRight: React.Dispatch<React.SetStateAction<string>>;
|
|
handleAddTabLeft: () => string;
|
|
handleAddTabRight: () => string;
|
|
handleCloseTabLeft: (tabId: string) => Promise<void>;
|
|
handleCloseTabRight: (tabId: string) => Promise<void>;
|
|
handleSelectTabLeft: (tabId: string) => void;
|
|
handleSelectTabRight: (tabId: string) => void;
|
|
handleReorderTabsLeft: (draggedId: string, targetId: string, position: "before" | "after") => void;
|
|
handleReorderTabsRight: (draggedId: string, targetId: string, position: "before" | "after") => void;
|
|
handleMoveTabFromLeftToRight: (tabId: string) => void;
|
|
handleMoveTabFromRightToLeft: (tabId: string) => void;
|
|
handleDuplicateTabLeft: (tabId: string, mode: SftpTabDuplicateMode) => Promise<string | null>;
|
|
handleDuplicateTabRight: (tabId: string, mode: SftpTabDuplicateMode) => Promise<string | null>;
|
|
handleHostSelectLeft: (
|
|
host: Host | "local",
|
|
options?: Parameters<SftpStateApi["connect"]>[2],
|
|
) => void;
|
|
handleHostSelectRight: (
|
|
host: Host | "local",
|
|
options?: Parameters<SftpStateApi["connect"]>[2],
|
|
) => void;
|
|
}
|
|
|
|
export const useSftpViewTabs = ({ sftp, sftpRef, hosts = [] }: UseSftpViewTabsParams): UseSftpViewTabsResult => {
|
|
const [showHostPickerLeft, setShowHostPickerLeft] = useState(false);
|
|
const [showHostPickerRight, setShowHostPickerRight] = useState(false);
|
|
const [hostSearchLeft, setHostSearchLeft] = useState("");
|
|
const [hostSearchRight, setHostSearchRight] = useState("");
|
|
const hostsRef = React.useRef(hosts);
|
|
hostsRef.current = hosts;
|
|
|
|
const handleAddTabLeft = useCallback(() => {
|
|
const tabId = sftpRef.current.addTab("left");
|
|
setShowHostPickerLeft(true);
|
|
return tabId;
|
|
}, [sftpRef]);
|
|
|
|
const handleAddTabRight = useCallback(() => {
|
|
const tabId = sftpRef.current.addTab("right");
|
|
setShowHostPickerRight(true);
|
|
return tabId;
|
|
}, [sftpRef]);
|
|
|
|
const confirmCloseEditorTabsByOwner = useCallback(async (owner: {
|
|
sessionId?: string;
|
|
sftpTabId?: string;
|
|
}): Promise<boolean> => {
|
|
const choice = (tab: EditorTab) => promptUnsavedChanges(tab.fileName);
|
|
const saveTab = async (id: EditorTabId) => {
|
|
const ok = await saveEditorTab(id);
|
|
const tab = editorTabStore.getTab(id);
|
|
if (!ok || (tab && tab.content !== tab.baselineContent)) {
|
|
throw new Error(tab?.saveError ?? "Save failed");
|
|
}
|
|
};
|
|
return editorTabStore.confirmCloseByOwner(
|
|
owner,
|
|
choice,
|
|
saveTab,
|
|
releaseEditorTabSaveCoordinator,
|
|
);
|
|
}, []);
|
|
|
|
const handleCloseSftpTab = useCallback(async (side: "left" | "right", tabId: string) => {
|
|
const sideTabs = side === "left" ? sftpRef.current.leftTabs : sftpRef.current.rightTabs;
|
|
const pane = sideTabs.tabs.find((tab) => tab.id === tabId);
|
|
if (pane?.connection?.id || pane) {
|
|
const ok = await confirmCloseEditorTabsByOwner({
|
|
sessionId: pane?.connection?.id,
|
|
sftpTabId: tabId,
|
|
});
|
|
if (!ok) return;
|
|
}
|
|
await sftpRef.current.closeTab(side, tabId);
|
|
}, [confirmCloseEditorTabsByOwner, sftpRef]);
|
|
|
|
const handleCloseTabLeft = useCallback((tabId: string) => (
|
|
handleCloseSftpTab("left", tabId)
|
|
), [handleCloseSftpTab]);
|
|
|
|
const handleCloseTabRight = useCallback((tabId: string) => (
|
|
handleCloseSftpTab("right", tabId)
|
|
), [handleCloseSftpTab]);
|
|
|
|
const handleSelectTabLeft = useCallback((tabId: string) => {
|
|
sftpRef.current.selectTab("left", tabId);
|
|
}, [sftpRef]);
|
|
|
|
const handleSelectTabRight = useCallback((tabId: string) => {
|
|
sftpRef.current.selectTab("right", tabId);
|
|
}, [sftpRef]);
|
|
|
|
const leftPanes = useMemo(
|
|
() => (sftp.leftTabs.tabs.length > 0 ? sftp.leftTabs.tabs : [sftp.leftPane]),
|
|
[sftp.leftTabs.tabs, sftp.leftPane],
|
|
);
|
|
const rightPanes = useMemo(
|
|
() => (sftp.rightTabs.tabs.length > 0 ? sftp.rightTabs.tabs : [sftp.rightPane]),
|
|
[sftp.rightTabs.tabs, sftp.rightPane],
|
|
);
|
|
|
|
const handleReorderTabsLeft = useCallback(
|
|
(draggedId: string, targetId: string, position: "before" | "after") => {
|
|
sftpRef.current.reorderTabs("left", draggedId, targetId, position);
|
|
},
|
|
[sftpRef],
|
|
);
|
|
|
|
const handleReorderTabsRight = useCallback(
|
|
(draggedId: string, targetId: string, position: "before" | "after") => {
|
|
sftpRef.current.reorderTabs("right", draggedId, targetId, position);
|
|
},
|
|
[sftpRef],
|
|
);
|
|
|
|
const handleMoveTabFromLeftToRight = useCallback((tabId: string) => {
|
|
sftpRef.current.moveTabToOtherSide("left", tabId);
|
|
}, [sftpRef]);
|
|
|
|
const handleMoveTabFromRightToLeft = useCallback((tabId: string) => {
|
|
sftpRef.current.moveTabToOtherSide("right", tabId);
|
|
}, [sftpRef]);
|
|
|
|
const handleDuplicateTab = useCallback(
|
|
async (side: "left" | "right", tabId: string, mode: SftpTabDuplicateMode) => {
|
|
const sideTabs = side === "left" ? sftpRef.current.leftTabs : sftpRef.current.rightTabs;
|
|
const pane = sideTabs.tabs.find((tab) => tab.id === tabId);
|
|
const request = getSftpTabDuplicateRequest(pane, mode);
|
|
if (!request) return null;
|
|
|
|
const host = request.kind === "local"
|
|
? "local"
|
|
: hostsRef.current.find((item) => item.id === request.hostId);
|
|
if (!host) return null;
|
|
|
|
let duplicatedTabId: string | null = null;
|
|
await sftpRef.current.connect(side, host, {
|
|
forceNewTab: true,
|
|
ignoreSharedCache: mode === "defaultPath",
|
|
initialPath: request.path,
|
|
onTabCreated: (createdTabId) => {
|
|
duplicatedTabId = createdTabId;
|
|
},
|
|
});
|
|
|
|
return duplicatedTabId;
|
|
},
|
|
[sftpRef],
|
|
);
|
|
|
|
const handleDuplicateTabLeft = useCallback(
|
|
(tabId: string, mode: SftpTabDuplicateMode) => handleDuplicateTab("left", tabId, mode),
|
|
[handleDuplicateTab],
|
|
);
|
|
|
|
const handleDuplicateTabRight = useCallback(
|
|
(tabId: string, mode: SftpTabDuplicateMode) => handleDuplicateTab("right", tabId, mode),
|
|
[handleDuplicateTab],
|
|
);
|
|
|
|
const handleHostSelectLeft = useCallback((
|
|
host: Host | "local",
|
|
options?: Parameters<SftpStateApi["connect"]>[2],
|
|
) => {
|
|
sftpRef.current.connect("left", host, options);
|
|
setShowHostPickerLeft(false);
|
|
}, [sftpRef]);
|
|
|
|
const handleHostSelectRight = useCallback((
|
|
host: Host | "local",
|
|
options?: Parameters<SftpStateApi["connect"]>[2],
|
|
) => {
|
|
sftpRef.current.connect("right", host, options);
|
|
setShowHostPickerRight(false);
|
|
}, [sftpRef]);
|
|
|
|
const leftTabsInfo = useMemo(
|
|
() =>
|
|
sftp.leftTabs.tabs.map((pane) => ({
|
|
id: pane.id,
|
|
label: pane.connection?.hostLabel || "New Tab",
|
|
isLocal: pane.connection?.isLocal || false,
|
|
hostId: pane.connection?.hostId || null,
|
|
canDuplicate: pane.connection?.status === "connected",
|
|
})),
|
|
[sftp.leftTabs.tabs],
|
|
);
|
|
|
|
const rightTabsInfo = useMemo(
|
|
() =>
|
|
sftp.rightTabs.tabs.map((pane) => ({
|
|
id: pane.id,
|
|
label: pane.connection?.hostLabel || "New Tab",
|
|
isLocal: pane.connection?.isLocal || false,
|
|
hostId: pane.connection?.hostId || null,
|
|
canDuplicate: pane.connection?.status === "connected",
|
|
})),
|
|
[sftp.rightTabs.tabs],
|
|
);
|
|
|
|
return {
|
|
leftPanes,
|
|
rightPanes,
|
|
leftTabsInfo,
|
|
rightTabsInfo,
|
|
showHostPickerLeft,
|
|
showHostPickerRight,
|
|
hostSearchLeft,
|
|
hostSearchRight,
|
|
setShowHostPickerLeft,
|
|
setShowHostPickerRight,
|
|
setHostSearchLeft,
|
|
setHostSearchRight,
|
|
handleAddTabLeft,
|
|
handleAddTabRight,
|
|
handleCloseTabLeft,
|
|
handleCloseTabRight,
|
|
handleSelectTabLeft,
|
|
handleSelectTabRight,
|
|
handleReorderTabsLeft,
|
|
handleReorderTabsRight,
|
|
handleMoveTabFromLeftToRight,
|
|
handleMoveTabFromRightToLeft,
|
|
handleDuplicateTabLeft,
|
|
handleDuplicateTabRight,
|
|
handleHostSelectLeft,
|
|
handleHostSelectRight,
|
|
};
|
|
};
|