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
271 lines
9.5 KiB
TypeScript
271 lines
9.5 KiB
TypeScript
/** Pure state and decision helpers for keeping an SFTP pane aligned with a terminal. */
|
|
export type SftpFollowTerminalCwdBlock = {
|
|
connectionId: string;
|
|
terminalCwd: string;
|
|
};
|
|
|
|
export type SftpFollowTerminalCwdContext = {
|
|
followEnabled: boolean;
|
|
isVisible: boolean;
|
|
terminalCwd?: string | null;
|
|
currentPath?: string | null;
|
|
connectionId?: string | null;
|
|
hasActiveWork: boolean;
|
|
isConnected: boolean;
|
|
/** Skip auto-follow while this terminal cwd cannot be reached on SFTP. */
|
|
blockedFollow?: SftpFollowTerminalCwdBlock | null;
|
|
/** Skip auto-follow after this terminal cwd was already handled for the connection. */
|
|
handledFollow?: SftpFollowTerminalCwdBlock | null;
|
|
};
|
|
|
|
export type SftpFollowTerminalCwdSyncResultContext = {
|
|
syncGeneration: number;
|
|
currentGeneration: number;
|
|
followEnabled: boolean;
|
|
canFollow: boolean;
|
|
expectedSessionId?: string | null;
|
|
liveSessionId?: string | null;
|
|
expectedConnectionId?: string | null;
|
|
liveConnectionId?: string | null;
|
|
paneConnectionId?: string | null;
|
|
expectedTerminalCwd?: string | null;
|
|
liveTerminalCwd?: string | null;
|
|
requireLiveTerminalCwd?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Whether a change of the live `activeTerminalCwd` input must invalidate the
|
|
* handled/blocked follow bookkeeping.
|
|
*
|
|
* Hidden surfaces receive `activeTerminalCwd={null}`
|
|
* (terminalLayerSidePanelSlots) and get the live value back when the tab
|
|
* becomes visible again. That visibility-induced `null` is synthetic and must
|
|
* not invalidate: invalidating on it would drop `handledFollowRef` and make
|
|
* the regular follow sync navigate the user's browsed pane back to the
|
|
* terminal cwd (clearing the filename filter) on every terminal-tab switch.
|
|
*
|
|
* While the surface is visible, though, a `null` is a real transition: the
|
|
* linked terminal session moved to (or closed on) a session whose cwd cache is
|
|
* empty. Passing it through would leave the generation valid, and the
|
|
* first-open eligibility checks do not bind to the terminal session identity,
|
|
* so an in-flight probe of the previous session could navigate the visible
|
|
* pane. Any live value change while visible invalidates — including to `null`.
|
|
*/
|
|
export const shouldInvalidateFollowBookkeepingOnCwdChange = ({
|
|
nextCwd,
|
|
lastCwd,
|
|
isVisible,
|
|
}: {
|
|
nextCwd: string | null;
|
|
lastCwd: string | null;
|
|
isVisible: boolean;
|
|
}): boolean => {
|
|
if (nextCwd === lastCwd) return false;
|
|
// Only the hidden synthetic `null` pass-through is ignored; a caller that
|
|
// still forwards live values while hidden keeps the concrete-change rule.
|
|
if (!isVisible) return nextCwd !== null;
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Whether the one-shot "first open" terminal cwd sync must re-arm.
|
|
*
|
|
* Hiding the surface while the owner side panel stays open (switching terminal
|
|
* tabs, or focusing another side-panel tool in the same tab) must NOT re-arm
|
|
* the sync: re-running it would navigate the pane away from the user's browsed
|
|
* directory back to the terminal cwd and clear the filename filter on every
|
|
* switch. A changed terminal cwd is still followed by the regular follow sync
|
|
* through its handled/blocked bookkeeping. The sync re-arms only when the
|
|
* owning panel actually closed (fresh open resyncs, #2335) or the linked
|
|
* connection replaced itself (Start over).
|
|
*/
|
|
export const shouldResetInitialFollowTerminalCwdSync = ({
|
|
isVisible,
|
|
ownerPanelOpen,
|
|
connectionId,
|
|
trackedConnectionId,
|
|
}: {
|
|
isVisible: boolean;
|
|
/** Side panel still open for this terminal tab (another tool/tab may have focus). */
|
|
ownerPanelOpen: boolean;
|
|
connectionId: string | null;
|
|
trackedConnectionId: string | null;
|
|
}): boolean => {
|
|
if (connectionId !== trackedConnectionId) return true;
|
|
return !isVisible && !ownerPanelOpen;
|
|
};
|
|
|
|
/**
|
|
* Whether a hide transition with the owning panel still open (terminal-tab
|
|
* switch, or focusing another side-panel tool in the same tab) must latch the
|
|
* pending first-open fresh-CWD probe as interrupted.
|
|
*
|
|
* When the surface is hidden while a probe is still in flight, the probe's
|
|
* eligibility is only re-checked live when it resolves. If the tab becomes
|
|
* visible again before then, the live checks pass again (generation unchanged,
|
|
* visible, no active work) and the stale probe would navigate the pane back to
|
|
* the terminal cwd, clearing the user's browsed directory and filename filter.
|
|
* The hide transition must latch the attempt as interrupted while keeping its
|
|
* one-shot slot consumed (no re-arm on return — see
|
|
* {@link shouldReleaseInitialFollowSyncAttempt}).
|
|
*/
|
|
export const shouldLatchInitialFollowInterruption = ({
|
|
isVisible,
|
|
ownerPanelOpen,
|
|
}: {
|
|
isVisible: boolean;
|
|
ownerPanelOpen: boolean;
|
|
}): boolean => !isVisible && ownerPanelOpen;
|
|
|
|
/**
|
|
* Whether an unfinished one-shot "first open" sync attempt may release its
|
|
* consumed slot so a retry can re-arm.
|
|
*
|
|
* If the fresh-CWD probe resolves while the surface is hidden but the owning
|
|
* panel is still open (terminal-tab switch), the attempt must stay consumed:
|
|
* releasing it would re-run the first-open sync when the tab becomes visible
|
|
* again and navigate the pane away from the directory the user browsed to
|
|
* while the original probe was pending (clearing its filename filter). The
|
|
* attempt is released once the surface is visible again or the owning panel
|
|
* closed (fresh open resyncs, #2335).
|
|
*/
|
|
export const shouldReleaseInitialFollowSyncAttempt = ({
|
|
isVisible,
|
|
ownerPanelOpen,
|
|
}: {
|
|
isVisible: boolean;
|
|
ownerPanelOpen: boolean;
|
|
}): boolean => isVisible || !ownerPanelOpen;
|
|
|
|
export const resolveHostFollowTerminalCwd = (
|
|
hostFollowTerminalCwd: boolean | undefined,
|
|
globalFollowTerminalCwd: boolean,
|
|
): boolean => hostFollowTerminalCwd ?? globalFollowTerminalCwd;
|
|
|
|
export const resolveSftpFollowTerminalCwdTargetHost = <T>(
|
|
visibleHost: T | null | undefined,
|
|
fallbackHost: T | null | undefined,
|
|
): T | null => visibleHost ?? fallbackHost ?? null;
|
|
|
|
export const mergeLatestFollowTerminalCwdHostSetting = <
|
|
T extends { id?: string; sftpFollowTerminalCwd?: boolean },
|
|
>(
|
|
displayHost: T | null | undefined,
|
|
latestHost: T | null | undefined,
|
|
pendingFollowOverride?: boolean,
|
|
): T | null => {
|
|
if (!displayHost) return latestHost ?? null;
|
|
if (!latestHost || latestHost.id !== displayHost.id) return displayHost;
|
|
|
|
return {
|
|
...latestHost,
|
|
...displayHost,
|
|
sftpFollowTerminalCwd:
|
|
latestHost.sftpFollowTerminalCwd !== undefined
|
|
? latestHost.sftpFollowTerminalCwd
|
|
: pendingFollowOverride,
|
|
};
|
|
};
|
|
|
|
/** Clear a follow block once the user reaches the blocked cwd through any navigation. */
|
|
export const shouldClearBlockedFollowOnReach = (
|
|
blockedFollow: SftpFollowTerminalCwdBlock | null | undefined,
|
|
connectionId: string | null | undefined,
|
|
currentPath: string | null | undefined,
|
|
loading: boolean,
|
|
): boolean => {
|
|
if (loading || !blockedFollow || !connectionId || !currentPath) return false;
|
|
return (
|
|
blockedFollow.connectionId === connectionId
|
|
&& blockedFollow.terminalCwd === currentPath
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Whether a first-open or in-flight follow still belongs to the terminal that
|
|
* started it. A missing origin id is not a skipped guard: the live focused
|
|
* terminal must stay absent too, otherwise the probe belongs to a later pane.
|
|
*/
|
|
export const isFollowOriginStillCurrent = ({
|
|
expectedOriginId,
|
|
liveOriginId,
|
|
}: {
|
|
expectedOriginId: string | null | undefined;
|
|
liveOriginId?: string | null;
|
|
}): boolean => {
|
|
if (liveOriginId === undefined) return true;
|
|
if (expectedOriginId == null) return liveOriginId == null;
|
|
return liveOriginId === expectedOriginId;
|
|
};
|
|
|
|
/** Whether an async follow result still belongs to the current terminal/connection state. */
|
|
export const shouldApplyFollowTerminalCwdSyncResult = ({
|
|
syncGeneration,
|
|
currentGeneration,
|
|
followEnabled,
|
|
canFollow,
|
|
expectedSessionId,
|
|
liveSessionId,
|
|
expectedConnectionId,
|
|
liveConnectionId,
|
|
paneConnectionId,
|
|
expectedTerminalCwd,
|
|
liveTerminalCwd,
|
|
requireLiveTerminalCwd = false,
|
|
}: SftpFollowTerminalCwdSyncResultContext): boolean => {
|
|
if (syncGeneration !== currentGeneration || !followEnabled || !canFollow) {
|
|
return false;
|
|
}
|
|
if (expectedSessionId !== undefined) {
|
|
if (!isFollowOriginStillCurrent({
|
|
expectedOriginId: expectedSessionId,
|
|
liveOriginId: liveSessionId,
|
|
})) return false;
|
|
}
|
|
if (expectedConnectionId !== undefined) {
|
|
if (!expectedConnectionId) return false;
|
|
if (liveConnectionId !== undefined && liveConnectionId !== expectedConnectionId) return false;
|
|
if (paneConnectionId !== undefined && paneConnectionId !== expectedConnectionId) return false;
|
|
}
|
|
if (expectedTerminalCwd !== undefined) {
|
|
if (requireLiveTerminalCwd && !liveTerminalCwd) return false;
|
|
if (liveTerminalCwd && liveTerminalCwd !== expectedTerminalCwd) return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
/** Whether SFTP should auto-navigate to match the linked terminal cwd. */
|
|
export const shouldFollowTerminalCwdNavigate = ({
|
|
followEnabled,
|
|
isVisible,
|
|
terminalCwd,
|
|
currentPath,
|
|
connectionId,
|
|
hasActiveWork,
|
|
isConnected,
|
|
blockedFollow,
|
|
handledFollow,
|
|
}: SftpFollowTerminalCwdContext): boolean => {
|
|
if (!followEnabled || !isVisible || !isConnected) return false;
|
|
if (hasActiveWork) return false;
|
|
if (!terminalCwd || terminalCwd.trim().length === 0) return false;
|
|
if (
|
|
handledFollow
|
|
&& connectionId
|
|
&& handledFollow.connectionId === connectionId
|
|
&& handledFollow.terminalCwd === terminalCwd
|
|
) {
|
|
return false;
|
|
}
|
|
if (
|
|
blockedFollow
|
|
&& connectionId
|
|
&& blockedFollow.connectionId === connectionId
|
|
&& blockedFollow.terminalCwd === terminalCwd
|
|
) {
|
|
return false;
|
|
}
|
|
if (!currentPath || currentPath === terminalCwd) return false;
|
|
return true;
|
|
};
|