refactor(sidebar): remove dead directoryStatus probe artifact (#1660)
Commit 7205d7a3 replaced runtime directory probing with buildKnownSessionDirectories
filtering, but left the hook, prop, state, and UI checks as a transitional artifact.
- Delete useDirectoryStatusProbe.ts
- Remove directoryStatus state/prop from SessionSidebar, SessionGroupSection, SessionNodeItem
- Remove isMissingDirectory checks, opacity-75 styling, and disabled states
- Simplify handleSessionSelect signature
- Update sidebar DOCUMENTATION.md
Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
This commit is contained in:
committed by
GitHub
co-authored by
Leonid Skorobogatyy
parent
a73090f396
commit
d909c43979
@@ -1,187 +0,0 @@
|
||||
import React from 'react';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { mapWithConcurrency } from '@/lib/concurrency';
|
||||
import { normalizePath } from '../utils';
|
||||
|
||||
type ProjectLike = { path: string };
|
||||
|
||||
type DirectoryStatusValue = 'unknown' | 'exists' | 'missing';
|
||||
|
||||
type Args = {
|
||||
sortedSessions: Session[];
|
||||
projects: ProjectLike[];
|
||||
directoryStatus: Map<string, DirectoryStatusValue>;
|
||||
setDirectoryStatus: React.Dispatch<React.SetStateAction<Map<string, DirectoryStatusValue>>>;
|
||||
};
|
||||
|
||||
const PROBE_CONCURRENCY = 3;
|
||||
const MISSING_CACHE_KEY = 'oc.directoryProbe.missing';
|
||||
// Re-probe missing directories periodically in case they're recreated
|
||||
const MISSING_REPROBE_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
type MissingCache = Record<string, number>; // directory -> timestamp
|
||||
|
||||
function loadMissingCache(): MissingCache {
|
||||
try {
|
||||
const raw = localStorage.getItem(MISSING_CACHE_KEY);
|
||||
if (!raw) return {};
|
||||
return JSON.parse(raw) as MissingCache;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveMissingCache(cache: MissingCache): void {
|
||||
try {
|
||||
localStorage.setItem(MISSING_CACHE_KEY, JSON.stringify(cache));
|
||||
} catch {
|
||||
// ignore quota errors
|
||||
}
|
||||
}
|
||||
|
||||
async function probeDirectory(directory: string): Promise<DirectoryStatusValue> {
|
||||
try {
|
||||
await opencodeClient.listLocalDirectory(directory);
|
||||
return 'exists';
|
||||
} catch (error) {
|
||||
const looksLikeSdkWorktree =
|
||||
directory.includes('/opencode/worktree/') ||
|
||||
directory.includes('/.opencode/data/worktree/') ||
|
||||
directory.includes('/.local/share/opencode/worktree/');
|
||||
|
||||
const reachable = await opencodeClient.probeDirectory(directory).catch(() => false);
|
||||
if (reachable) {
|
||||
return 'exists';
|
||||
}
|
||||
|
||||
const message = error instanceof Error ? error.message.toLowerCase() : '';
|
||||
const definitelyMissing =
|
||||
message.includes('enoent') ||
|
||||
message.includes('not found') ||
|
||||
message.includes('does not exist') ||
|
||||
message.includes('no such file');
|
||||
|
||||
if (definitelyMissing || looksLikeSdkWorktree) {
|
||||
return 'missing';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
export const useDirectoryStatusProbe = ({
|
||||
sortedSessions,
|
||||
projects,
|
||||
directoryStatus,
|
||||
setDirectoryStatus,
|
||||
}: Args): void => {
|
||||
const directoryStatusRef = React.useRef<Map<string, DirectoryStatusValue>>(new Map());
|
||||
const probeInFlightRef = React.useRef(false);
|
||||
const missingCacheRef = React.useRef<MissingCache>(loadMissingCache());
|
||||
|
||||
React.useEffect(() => {
|
||||
directoryStatusRef.current = directoryStatus;
|
||||
}, [directoryStatus]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const directories = new Set<string>();
|
||||
const normalizedProjectRoots = new Set<string>();
|
||||
sortedSessions.forEach((session) => {
|
||||
const dir = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
|
||||
if (dir) directories.add(dir);
|
||||
});
|
||||
projects.forEach((project) => {
|
||||
const normalized = normalizePath(project.path);
|
||||
if (normalized) {
|
||||
directories.add(normalized);
|
||||
normalizedProjectRoots.add(normalized);
|
||||
}
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const missingCache = missingCacheRef.current;
|
||||
const toProbe: string[] = [];
|
||||
const preseeded = new Map<string, DirectoryStatusValue>();
|
||||
|
||||
for (const directory of directories) {
|
||||
const isProjectRoot = normalizedProjectRoots.has(directory);
|
||||
const known = directoryStatusRef.current.get(directory);
|
||||
if (known === 'exists') continue;
|
||||
|
||||
const cachedAt = missingCache[directory];
|
||||
if (known === 'missing') {
|
||||
if (isProjectRoot) {
|
||||
toProbe.push(directory);
|
||||
continue;
|
||||
}
|
||||
if (cachedAt && now - cachedAt < MISSING_REPROBE_MS) {
|
||||
continue;
|
||||
}
|
||||
toProbe.push(directory);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use cached "missing" status if fresh enough — skip the HTTP probe
|
||||
if (!isProjectRoot && cachedAt && now - cachedAt < MISSING_REPROBE_MS) {
|
||||
preseeded.set(directory, 'missing');
|
||||
continue;
|
||||
}
|
||||
|
||||
toProbe.push(directory);
|
||||
}
|
||||
|
||||
// Apply preseeded missing statuses immediately (no HTTP call)
|
||||
if (preseeded.size > 0) {
|
||||
setDirectoryStatus((prev) => {
|
||||
let changed = false;
|
||||
const next = new Map(prev);
|
||||
for (const [dir, status] of preseeded) {
|
||||
if (next.get(dir) !== status) {
|
||||
next.set(dir, status);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}
|
||||
|
||||
if (toProbe.length === 0 || probeInFlightRef.current) return;
|
||||
probeInFlightRef.current = true;
|
||||
|
||||
let cancelled = false;
|
||||
let cacheChanged = false;
|
||||
|
||||
void mapWithConcurrency(toProbe, PROBE_CONCURRENCY, async (directory) => {
|
||||
const status = await probeDirectory(directory);
|
||||
|
||||
// Update missing cache
|
||||
if (status === 'missing') {
|
||||
missingCache[directory] = Date.now();
|
||||
cacheChanged = true;
|
||||
} else if (missingCache[directory]) {
|
||||
delete missingCache[directory];
|
||||
cacheChanged = true;
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
setDirectoryStatus((prev) => {
|
||||
if (prev.get(directory) === status) return prev;
|
||||
const next = new Map(prev);
|
||||
next.set(directory, status);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
return { directory, status };
|
||||
}).finally(() => {
|
||||
probeInFlightRef.current = false;
|
||||
if (cacheChanged) {
|
||||
saveMissingCache(missingCache);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [sortedSessions, projects, setDirectoryStatus]);
|
||||
};
|
||||
@@ -16,7 +16,7 @@ type Args = {
|
||||
activeSessionByProject: Map<string, string>;
|
||||
setActiveSessionByProject: React.Dispatch<React.SetStateAction<Map<string, string>>>;
|
||||
currentSessionId: string | null;
|
||||
handleSessionSelect: (sessionId: string, sessionDirectory: string | null, isMissingDirectory: boolean, projectId?: string | null) => void;
|
||||
handleSessionSelect: (sessionId: string, sessionDirectory: string | null, projectId?: string | null) => void;
|
||||
newSessionDraftOpen: boolean;
|
||||
mobileVariant: boolean;
|
||||
openNewSessionDraft: (options?: { directoryOverride?: string | null }) => void;
|
||||
@@ -139,7 +139,7 @@ export const useProjectSessionSelection = (args: Args): { currentSessionDirector
|
||||
return;
|
||||
}
|
||||
const targetDirectory = projectMap.get(targetSessionId)?.directory ?? null;
|
||||
handleSessionSelect(targetSessionId, targetDirectory, false, activeProjectId);
|
||||
handleSessionSelect(targetSessionId, targetDirectory, activeProjectId);
|
||||
}, [
|
||||
activeProjectId,
|
||||
activeSessionByProject,
|
||||
|
||||
@@ -64,11 +64,7 @@ export const useSessionActions = (args: Args) => {
|
||||
}, []);
|
||||
|
||||
const handleSessionSelect = React.useCallback(
|
||||
(sessionId: string, sessionDirectory?: string | null, disabled?: boolean, projectId?: string | null) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
(sessionId: string, sessionDirectory?: string | null, projectId?: string | null) => {
|
||||
const resetSessionSearch = () => {
|
||||
if (!args.isSessionSearchOpen && args.sessionSearchQuery.length === 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user