import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { opencodeClient } from '@/lib/opencode/client'; import type { DirectorySwitchResult } from '@/lib/opencode/client'; import { getDesktopHomeDirectory } from '@/lib/desktop'; import { updateDesktopSettings } from '@/lib/persistence'; import { startConfigUpdate, finishConfigUpdate, updateConfigUpdateMessage } from '@/lib/configUpdate'; import { useSessionStore } from '@/stores/useSessionStore'; import { refreshAfterOpenCodeRestart } from '@/stores/useAgentsStore'; import { useCommandsStore } from '@/stores/useCommandsStore'; import { useFileSearchStore } from '@/stores/useFileSearchStore'; import { emitConfigChange } from '@/lib/configSync'; import { streamDebugEnabled } from '@/stores/utils/streamDebug'; import { getSafeStorage } from './utils/safeStorage'; interface DirectoryStore { currentDirectory: string; directoryHistory: string[]; historyIndex: number; homeDirectory: string; hasPersistedDirectory: boolean; isHomeReady: boolean; isSwitchingDirectory: boolean; setDirectory: (path: string, options?: { showOverlay?: boolean }) => void; goBack: () => void; goForward: () => void; goToParent: () => void; goHome: () => Promise; synchronizeHomeDirectory: (path: string) => void; } let cachedHomeDirectory: string | null = null; const safeStorage = getSafeStorage(); const persistedLastDirectory = safeStorage.getItem('lastDirectory'); const initialHasPersistedDirectory = typeof persistedLastDirectory === 'string' && persistedLastDirectory.length > 0; const notifyOpenCodeWorkingDirectory = (path: string, options?: { showOverlay?: boolean }) => { const showOverlay = options?.showOverlay ?? true; if (showOverlay) { startConfigUpdate('Switching project directory…'); } return opencodeClient.setOpenCodeWorkingDirectory(path).catch((error) => { console.warn('Failed to synchronize OpenCode working directory:', error); throw error; }); }; const scheduleDirectoryFollowUp = ( restartPromise: Promise, options: { showOverlay: boolean }, onComplete?: (result: DirectorySwitchResult | null) => void ) => { const { showOverlay } = options; const reloadSessions = () => { try { useSessionStore.getState().loadSessions(); } catch (err) { console.error('Failed to reload sessions after directory change:', err); } }; void (async () => { let result: DirectorySwitchResult | null = null; try { result = await restartPromise; } catch (error) { console.error('Failed to update OpenCode working directory:', error); if (showOverlay) { updateConfigUpdateMessage('Failed to switch directory. Please try again.'); await new Promise((resolve) => setTimeout(resolve, 1500)); finishConfigUpdate(); } onComplete?.(result); reloadSessions(); return; } try { if (result && result.restarted) { try { if (typeof window !== 'undefined' && window.localStorage) { window.localStorage.removeItem('commands-store'); } } catch (storageError) { console.warn('Failed to reset commands-store cache:', storageError); } await refreshAfterOpenCodeRestart({ message: 'Refreshing OpenCode configuration…' }); try { await useCommandsStore.getState().loadCommands(); try { emitConfigChange('commands', { source: 'useCommandsStore' }); } catch (syncError) { console.warn('Failed to emit command configuration change:', syncError); } } catch (commandError) { console.warn('Failed to reload commands after directory change:', commandError); } } else if (showOverlay) { finishConfigUpdate(); } } catch (error) { console.error('Failed to refresh configuration after directory change:', error); if (showOverlay) { updateConfigUpdateMessage('Failed to refresh configuration. Please reload manually.'); await new Promise((resolve) => setTimeout(resolve, 1500)); finishConfigUpdate(); } } finally { onComplete?.(result); reloadSessions(); } })(); }; const invalidateFileSearchCache = (scope?: string | null) => { try { useFileSearchStore.getState().invalidateDirectory(scope); } catch (error) { console.warn('Failed to invalidate file search cache:', error); } }; const normalizeDirectoryPath = (value: string): string => { const trimmed = value.trim(); if (!trimmed) { return trimmed; } const normalized = trimmed.replace(/\\/g, '/'); if (normalized.length > 1) { return normalized.replace(/\/+$/, ''); } return normalized; }; const resolveTildePath = (path: string, homeDir?: string | null): string => { const trimmed = path.trim(); if (!trimmed.startsWith('~')) { return trimmed; } if (trimmed === '~') { return homeDir || trimmed; } if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) { return homeDir ? `${homeDir}${trimmed.slice(1)}` : trimmed; } return trimmed; }; const resolveDirectoryPath = (path: string, homeDir?: string | null): string => { const expanded = resolveTildePath(path, homeDir); return normalizeDirectoryPath(expanded); }; const getHomeDirectory = () => { if (typeof window !== 'undefined') { const storedHome = safeStorage.getItem('homeDirectory') || cachedHomeDirectory || null; const saved = safeStorage.getItem('lastDirectory'); if (saved) { return resolveDirectoryPath(saved, storedHome); } if (cachedHomeDirectory) return cachedHomeDirectory; const desktopHome = (typeof window.__OPENCHAMBER_HOME__ === 'string' && window.__OPENCHAMBER_HOME__.length > 0 ? window.__OPENCHAMBER_HOME__ : window.opencodeDesktop && typeof window.opencodeDesktop.homeDirectory === 'string' ? window.opencodeDesktop.homeDirectory : null); if (desktopHome && desktopHome.length > 0) { cachedHomeDirectory = desktopHome; safeStorage.setItem('homeDirectory', desktopHome); return desktopHome; } if (storedHome) { cachedHomeDirectory = storedHome; return storedHome; } } const nodeHome = typeof process !== 'undefined' && process?.env?.HOME; if (nodeHome) { return nodeHome; } return process?.cwd?.() || '/'; }; const normalizeHomeCandidate = (value?: string | null) => { if (typeof value !== 'string') { return null; } const trimmed = value.trim(); if (!trimmed) { return null; } const normalized = trimmed.replace(/\\/g, '/'); if (normalized.length > 1) { const withoutTrailingSlash = normalized.replace(/\/+$/, ''); if (withoutTrailingSlash && withoutTrailingSlash.length > 0) { if (withoutTrailingSlash === '/') { return null; } return withoutTrailingSlash; } } if (normalized === '/' || normalized.length === 0) { return null; } return normalized; }; const persistResolvedHome = (resolved: string) => { cachedHomeDirectory = resolved; if (typeof window !== 'undefined') { safeStorage.setItem('homeDirectory', resolved); } void updateDesktopSettings({ homeDirectory: resolved }); return resolved; }; const initializeHomeDirectory = async () => { const acceptCandidate = (candidate?: string | null) => { const normalized = normalizeHomeCandidate(candidate); return normalized ? persistResolvedHome(normalized) : null; }; try { const fsHome = await opencodeClient.getFilesystemHome(); const resolved = acceptCandidate(fsHome); if (resolved) { return resolved; } } catch (filesystemError) { console.warn('Failed to obtain filesystem home directory:', filesystemError); } try { const info = await opencodeClient.getSystemInfo(); const resolved = acceptCandidate(info?.homeDirectory); if (resolved) { return resolved; } } catch (error) { console.warn('Failed to get home directory from system info:', error); } try { const desktopHome = await getDesktopHomeDirectory(); const resolved = acceptCandidate(desktopHome); if (resolved) { return resolved; } } catch (desktopError) { console.warn('Failed to obtain desktop-integrated home directory:', desktopError); } const fallback = getHomeDirectory(); const resolvedFallback = acceptCandidate(fallback); if (resolvedFallback) { return resolvedFallback; } return fallback; }; const initialHomeDirectory = getHomeDirectory(); if (initialHomeDirectory) { opencodeClient.setDirectory(initialHomeDirectory); } const initialIsHomeReady = Boolean(initialHomeDirectory && initialHomeDirectory !== '/'); export const useDirectoryStore = create()( devtools( (set, get) => ({ currentDirectory: initialHomeDirectory, directoryHistory: [initialHomeDirectory], historyIndex: 0, homeDirectory: initialHomeDirectory, hasPersistedDirectory: initialHasPersistedDirectory, isHomeReady: initialIsHomeReady, isSwitchingDirectory: false, setDirectory: (path: string, options?: { showOverlay?: boolean }) => { const homeDir = cachedHomeDirectory || get().homeDirectory || safeStorage.getItem('homeDirectory'); const resolvedPath = resolveDirectoryPath(path, homeDir); if (streamDebugEnabled()) { console.log('[DirectoryStore] setDirectory called with path:', resolvedPath); } const showOverlay = options?.showOverlay ?? true; opencodeClient.setDirectory(resolvedPath); invalidateFileSearchCache(); const restartPromise = notifyOpenCodeWorkingDirectory(resolvedPath, { showOverlay }); if (streamDebugEnabled()) { console.log('[DirectoryStore] notifyOpenCodeWorkingDirectory initiated'); } set((state) => { const newHistory = [...state.directoryHistory.slice(0, state.historyIndex + 1), resolvedPath]; safeStorage.setItem('lastDirectory', resolvedPath); void updateDesktopSettings({ lastDirectory: resolvedPath }); return { currentDirectory: resolvedPath, directoryHistory: newHistory, historyIndex: newHistory.length - 1, hasPersistedDirectory: true, isHomeReady: true, isSwitchingDirectory: true, }; }); scheduleDirectoryFollowUp(restartPromise, { showOverlay }, () => { set((state) => { if (state.currentDirectory !== resolvedPath) { return {}; } if (!state.isSwitchingDirectory) { return {}; } return { isSwitchingDirectory: false }; }); }); }, goBack: () => { const state = get(); if (state.historyIndex > 0) { const newIndex = state.historyIndex - 1; const newDirectory = state.directoryHistory[newIndex]; opencodeClient.setDirectory(newDirectory); invalidateFileSearchCache(); const restartPromise = notifyOpenCodeWorkingDirectory(newDirectory); safeStorage.setItem('lastDirectory', newDirectory); void updateDesktopSettings({ lastDirectory: newDirectory }); set({ currentDirectory: newDirectory, historyIndex: newIndex, hasPersistedDirectory: true, isHomeReady: true, isSwitchingDirectory: true, }); scheduleDirectoryFollowUp(restartPromise, { showOverlay: true }, () => { set((state) => { if (state.currentDirectory !== newDirectory) { return {}; } if (!state.isSwitchingDirectory) { return {}; } return { isSwitchingDirectory: false }; }); }); } }, goForward: () => { const state = get(); if (state.historyIndex < state.directoryHistory.length - 1) { const newIndex = state.historyIndex + 1; const newDirectory = state.directoryHistory[newIndex]; opencodeClient.setDirectory(newDirectory); invalidateFileSearchCache(); const restartPromise = notifyOpenCodeWorkingDirectory(newDirectory); safeStorage.setItem('lastDirectory', newDirectory); void updateDesktopSettings({ lastDirectory: newDirectory }); set({ currentDirectory: newDirectory, historyIndex: newIndex, hasPersistedDirectory: true, isHomeReady: true, isSwitchingDirectory: true, }); scheduleDirectoryFollowUp(restartPromise, { showOverlay: true }, () => { set((state) => { if (state.currentDirectory !== newDirectory) { return {}; } if (!state.isSwitchingDirectory) { return {}; } return { isSwitchingDirectory: false }; }); }); } }, goToParent: () => { const { currentDirectory, setDirectory } = get(); const homeDir = cachedHomeDirectory || get().homeDirectory || getHomeDirectory(); if (currentDirectory === homeDir || currentDirectory === '/') { return; } const cleanPath = currentDirectory.endsWith('/') ? currentDirectory.slice(0, -1) : currentDirectory; const lastSlash = cleanPath.lastIndexOf('/'); if (lastSlash === -1) { const home = cachedHomeDirectory || getHomeDirectory(); setDirectory(home); } else if (lastSlash === 0) { setDirectory('/'); } else { setDirectory(cleanPath.substring(0, lastSlash)); } }, goHome: async () => { const homeDir = cachedHomeDirectory || get().homeDirectory || (await initializeHomeDirectory()); get().setDirectory(homeDir); }, synchronizeHomeDirectory: (homePath: string) => { const state = get(); const resolvedHome = homePath; cachedHomeDirectory = resolvedHome; const needsUpdate = state.homeDirectory !== resolvedHome; const savedLastDirectory = safeStorage.getItem('lastDirectory'); const hasSavedLastDirectory = typeof savedLastDirectory === 'string' && savedLastDirectory.length > 0; const shouldReplaceCurrent = !hasSavedLastDirectory && ( state.currentDirectory === '/' || state.currentDirectory === state.homeDirectory || !state.currentDirectory ); if (!needsUpdate && !shouldReplaceCurrent) { if (!state.isHomeReady) { set({ isHomeReady: true }); } return; } const resolvedReady = typeof resolvedHome === 'string' && resolvedHome !== '' && resolvedHome !== '/'; const resolvedCurrent = state.currentDirectory ? resolveDirectoryPath(state.currentDirectory, resolvedHome) : state.currentDirectory; const resolvedHistory = state.directoryHistory.map((entry) => resolveDirectoryPath(entry, resolvedHome)); const historyChanged = resolvedHistory.some((entry, index) => entry !== state.directoryHistory[index]); const currentChanged = Boolean(resolvedCurrent && resolvedCurrent !== state.currentDirectory); const updates: Partial = { homeDirectory: resolvedHome, hasPersistedDirectory: hasSavedLastDirectory, isHomeReady: resolvedReady }; if (shouldReplaceCurrent) { updates.currentDirectory = resolvedHome; updates.directoryHistory = [resolvedHome]; updates.historyIndex = 0; updates.isSwitchingDirectory = true; } else if (currentChanged || historyChanged) { updates.currentDirectory = resolvedCurrent as string; updates.directoryHistory = resolvedHistory; updates.historyIndex = Math.min(state.historyIndex, resolvedHistory.length - 1); updates.isSwitchingDirectory = true; } set(() => updates as Partial); if ((shouldReplaceCurrent || currentChanged) && resolvedReady) { const nextDirectory = shouldReplaceCurrent ? resolvedHome : (resolvedCurrent as string); opencodeClient.setDirectory(nextDirectory); invalidateFileSearchCache(); safeStorage.setItem('lastDirectory', nextDirectory); void updateDesktopSettings({ lastDirectory: nextDirectory }); const restartPromise = notifyOpenCodeWorkingDirectory(nextDirectory, { showOverlay: false }); scheduleDirectoryFollowUp(restartPromise, { showOverlay: false }, () => { set((state) => { if (state.currentDirectory !== nextDirectory) { return {}; } if (!state.isSwitchingDirectory) { return {}; } return { isSwitchingDirectory: false }; }); }); } void updateDesktopSettings({ homeDirectory: resolvedHome }); } }), { name: 'directory-store' } ) ); if (typeof window !== 'undefined') { initializeHomeDirectory().then((home) => { useDirectoryStore.getState().synchronizeHomeDirectory(home); }); }