fix: stabilize codex recent project selection
This commit is contained in:
parent
c8f9d9bbdd
commit
f0f43be064
4 changed files with 98 additions and 13 deletions
|
|
@ -29,6 +29,7 @@ export interface ListDashboardRecentProjectsDeps<TViewModel> {
|
|||
export class ListDashboardRecentProjectsUseCase<TViewModel> {
|
||||
readonly #cacheTtlMs: number;
|
||||
readonly #degradedCacheTtlMs: number;
|
||||
readonly #inFlightByCacheKey = new Map<string, Promise<TViewModel>>();
|
||||
|
||||
constructor(private readonly deps: ListDashboardRecentProjectsDeps<TViewModel>) {
|
||||
this.#cacheTtlMs = deps.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
||||
|
|
@ -41,6 +42,20 @@ export class ListDashboardRecentProjectsUseCase<TViewModel> {
|
|||
return cached;
|
||||
}
|
||||
|
||||
const inFlight = this.#inFlightByCacheKey.get(cacheKey);
|
||||
if (inFlight) {
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
const execution = this.#executeUncached(cacheKey).finally(() => {
|
||||
this.#inFlightByCacheKey.delete(cacheKey);
|
||||
});
|
||||
|
||||
this.#inFlightByCacheKey.set(cacheKey, execution);
|
||||
return execution;
|
||||
}
|
||||
|
||||
async #executeUncached(cacheKey: string): Promise<TViewModel> {
|
||||
const startedAt = this.deps.clock.now();
|
||||
const results = await Promise.all(
|
||||
this.deps.sources.map((source, index) => this.#loadSource(source, index))
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import type { RecentProjectIdentityResolver } from '@features/recent-projects/ma
|
|||
import type { ServiceContext } from '@main/services';
|
||||
|
||||
const CODEX_THREAD_LIMIT = 40;
|
||||
const CODEX_LIVE_FETCH_TIMEOUT_MS = 1_200;
|
||||
const CODEX_ARCHIVED_FETCH_TIMEOUT_MS = 1_800;
|
||||
const CODEX_REQUEST_TIMEOUT_MS = 1_800;
|
||||
const CODEX_SOURCE_TIMEOUT_MS = 1_500;
|
||||
const CODEX_LIVE_FETCH_TIMEOUT_MS = 4_500;
|
||||
const CODEX_ARCHIVED_FETCH_TIMEOUT_MS = 2_500;
|
||||
const CODEX_REQUEST_TIMEOUT_MS = 4_500;
|
||||
const CODEX_SOURCE_TIMEOUT_MS = 5_200;
|
||||
const FAST_ARCHIVED_MERGE_TIMEOUT_MS = 150;
|
||||
|
||||
function isInteractiveSource(source: unknown): boolean {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { execCli } from '@main/utils/childProcess';
|
||||
import { constants as fsConstants } from 'node:fs';
|
||||
import * as fsp from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const CACHE_VERIFY_TTL_MS = 30_000;
|
||||
|
||||
|
|
@ -6,13 +8,60 @@ let cachedBinaryPath: string | null | undefined;
|
|||
let cacheVerifiedAt = 0;
|
||||
let resolveInFlight: Promise<string | null> | null = null;
|
||||
|
||||
async function verifyBinary(candidate: string): Promise<string | null> {
|
||||
async function fileExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await execCli(candidate, ['--version'], { timeout: 2_000, windowsHide: true });
|
||||
return candidate;
|
||||
await fsp.access(filePath, fsConstants.X_OK);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function expandWindowsExtensions(candidate: string): string[] {
|
||||
if (process.platform !== 'win32') {
|
||||
return [candidate];
|
||||
}
|
||||
|
||||
const pathext = process.env.PATHEXT?.split(';').filter(Boolean) ?? [
|
||||
'.EXE',
|
||||
'.CMD',
|
||||
'.BAT',
|
||||
'.COM',
|
||||
];
|
||||
const hasKnownExtension = pathext.some((ext) =>
|
||||
candidate.toLowerCase().endsWith(ext.toLowerCase())
|
||||
);
|
||||
|
||||
if (hasKnownExtension) {
|
||||
return [candidate];
|
||||
}
|
||||
|
||||
return [candidate, ...pathext.map((ext) => `${candidate}${ext.toLowerCase()}`)];
|
||||
}
|
||||
|
||||
async function verifyBinary(candidate: string): Promise<string | null> {
|
||||
const expandedCandidates = expandWindowsExtensions(candidate);
|
||||
|
||||
if (path.isAbsolute(candidate) || candidate.includes(path.sep)) {
|
||||
for (const expandedCandidate of expandedCandidates) {
|
||||
if (await fileExists(expandedCandidate)) {
|
||||
return expandedCandidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathEntries = (process.env.PATH ?? '').split(path.delimiter).filter(Boolean);
|
||||
for (const pathEntry of pathEntries) {
|
||||
for (const expandedCandidate of expandedCandidates) {
|
||||
const resolvedCandidate = path.join(pathEntry, expandedCandidate);
|
||||
if (await fileExists(resolvedCandidate)) {
|
||||
return resolvedCandidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export class CodexBinaryResolver {
|
||||
|
|
|
|||
|
|
@ -325,7 +325,28 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
|||
});
|
||||
}, [viewMode, repositoryGroups, projects]);
|
||||
|
||||
const activeProjectValue = viewMode === 'grouped' ? selectedRepositoryId : activeProjectId;
|
||||
const effectiveSelectedWorktreeId =
|
||||
selectedWorktreeId ?? activeProjectId ?? selectedProjectId ?? null;
|
||||
const effectiveSelectedRepositoryId = useMemo(() => {
|
||||
if (selectedRepositoryId) {
|
||||
return selectedRepositoryId;
|
||||
}
|
||||
|
||||
if (!effectiveSelectedWorktreeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
repositoryGroups.find((repo) =>
|
||||
repo.worktrees.some((worktree) => worktree.id === effectiveSelectedWorktreeId)
|
||||
)?.id ?? null
|
||||
);
|
||||
}, [effectiveSelectedWorktreeId, repositoryGroups, selectedRepositoryId]);
|
||||
|
||||
const activeProjectValue =
|
||||
viewMode === 'grouped'
|
||||
? effectiveSelectedRepositoryId
|
||||
: (activeProjectId ?? selectedProjectId ?? null);
|
||||
|
||||
const handleProjectValueChange = (id: string): void => {
|
||||
if (viewMode === 'grouped') selectRepository(id);
|
||||
|
|
@ -333,8 +354,8 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
|||
};
|
||||
|
||||
// Worktree state
|
||||
const activeRepo = repositoryGroups.find((r) => r.id === selectedRepositoryId);
|
||||
const activeWorktree = activeRepo?.worktrees.find((w) => w.id === selectedWorktreeId);
|
||||
const activeRepo = repositoryGroups.find((r) => r.id === effectiveSelectedRepositoryId);
|
||||
const activeWorktree = activeRepo?.worktrees.find((w) => w.id === effectiveSelectedWorktreeId);
|
||||
const worktrees = (activeRepo?.worktrees ?? []).filter(
|
||||
(w) => (w.totalSessions ?? w.sessions.length) > 0
|
||||
);
|
||||
|
|
@ -684,7 +705,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
|||
{mainWorktree && (
|
||||
<WorktreeItem
|
||||
worktree={mainWorktree}
|
||||
isSelected={mainWorktree.id === selectedWorktreeId}
|
||||
isSelected={mainWorktree.id === effectiveSelectedWorktreeId}
|
||||
onSelect={() => handleSelectWorktree(mainWorktree)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -705,7 +726,7 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
|||
<WorktreeItem
|
||||
key={worktree.id}
|
||||
worktree={worktree}
|
||||
isSelected={worktree.id === selectedWorktreeId}
|
||||
isSelected={worktree.id === effectiveSelectedWorktreeId}
|
||||
onSelect={() => handleSelectWorktree(worktree)}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
Loading…
Reference in a new issue