[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
270
domain/sftpFollowTerminalCwd.ts
Normal file
270
domain/sftpFollowTerminalCwd.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
/** 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;
|
||||
};
|
||||
Reference in New Issue
Block a user