fix(sessions): keep missing-worktree relocation manual

Remove automatic moves on session activation, terminal failures, and archive restoration while preserving manual moves and worktree deletion.

Replace directory listing probes with a stat-only endpoint using Node built-ins, including an isolated module-load regression test for packaged desktop.

Validation: focused session, worktree, filesystem, localization, and bridge tests; workspace type-check and lint; web and VS Code builds. Desktop startup and behavior verified by the maintainer.
This commit is contained in:
Bohdan Triapitsyn
2026-09-07 02:10:06 +03:00
parent eb6f7b0904
commit 3132d1361a
32 changed files with 211 additions and 763 deletions
@@ -17,7 +17,7 @@ import { Icon } from "@/components/icon/Icon";
import type { IconName } from '@/components/icon/icons';
import { useDeviceInfo } from '@/lib/device';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { isTerminalCwdMissingError, terminalSnapshotSize } from '@/lib/terminalApi';
import { terminalSnapshotSize } from '@/lib/terminalApi';
import { extractTerminalPreviewUrl, isTerminalPreviewUrlAvailable } from '@/lib/terminalPreview';
import { useI18n } from '@/lib/i18n';
import { PROJECT_ACTION_ICONS } from '@/lib/projectActions';
@@ -40,14 +40,6 @@ const resolveTabIconName = (iconKey: string | null): IconName => {
export const TerminalView: React.FC<TerminalViewProps> = ({ visible, directory }) => {
const { t } = useI18n();
const { terminal, runtime } = useRuntimeAPIs();
// The server rejects a working directory that no longer exists (a worktree
// deleted outside OpenChamber). The session is what is stranded, not the
// terminal: relocating it to its project changes the effective directory,
// and this view then starts a terminal there on its own.
const recoverCurrentSessionDirectory = React.useCallback(() => {
const sessionId = useSessionUIStore.getState().currentSessionId;
if (sessionId) void useSessionUIStore.getState().recoverMissingSessionDirectory(sessionId);
}, []);
const { currentTheme } = useThemeSystem();
const terminalAppearanceRef = React.useRef<{ themeMode: 'light' | 'dark'; terminalBackground: string; terminalForeground: string }>({ themeMode: 'dark', terminalBackground: '', terminalForeground: '' });
terminalAppearanceRef.current = { themeMode: currentTheme.metadata.variant === 'light' ? 'light' : 'dark', terminalBackground: currentTheme.colors.surface.background, terminalForeground: currentTheme.colors.syntax.base.foreground };
@@ -546,7 +538,6 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible, directory }
// this tab stopped owning the request; use current store
// ownership so a rejected create cannot leave it spinning.
if (directoryRef.current !== directory || activeTabIdRef.current !== tabId) return;
if (isTerminalCwdMissingError(error)) recoverCurrentSessionDirectory();
setConnectionError(
error instanceof Error
? error.message
@@ -590,7 +581,6 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible, directory }
setTabSessionId,
startStream,
disconnectStream,
recoverCurrentSessionDirectory,
t,
terminal,
terminalLoginShell,
@@ -654,7 +644,6 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible, directory }
|| directoryRef.current !== terminalDirectory
|| activeTabIdRef.current !== tabId
) return;
if (isTerminalCwdMissingError(error)) recoverCurrentSessionDirectory();
setConnectionError(
error instanceof Error ? error.message : t('terminalView.error.restartFailed')
);
@@ -665,7 +654,7 @@ export const TerminalView: React.FC<TerminalViewProps> = ({ visible, directory }
} finally {
setIsRestarting(false);
}
}, [activeTabId, disconnectStream, terminalDirectory, enableTabs, isActionTab, isRestarting, recoverCurrentSessionDirectory, resetTerminalPreviewScan, setTabLifecycle, setTabSessionId, startStream, t, terminal, terminalLoginShell, terminalShell]);
}, [activeTabId, disconnectStream, terminalDirectory, enableTabs, isActionTab, isRestarting, resetTerminalPreviewScan, setTabLifecycle, setTabSessionId, startStream, t, terminal, terminalLoginShell, terminalShell]);
const handleHardRestart = React.useCallback(async () => {
// Keep semantics: “close tab -> new clean tab”.