diff --git a/apps/cli/src/commands/results/remote.ts b/apps/cli/src/commands/results/remote.ts index 94af40b1..70d0aba5 100644 --- a/apps/cli/src/commands/results/remote.ts +++ b/apps/cli/src/commands/results/remote.ts @@ -24,6 +24,31 @@ import { listResultFilesFromRunsDir, } from '../inspect/utils.js'; + +// ── In-memory TTL cache for listGitRuns ──────────────────────────── +// Avoids repeated expensive git ls-tree + git cat-file --batch operations +// on every API request. Cache key is repoDir, TTL is 60 seconds. +const gitRunsCache = new Map(); +const GIT_RUNS_CACHE_TTL_MS = 60_000; + +function cachedListGitRuns(repoDir: string) { + const now = Date.now(); + const cached = gitRunsCache.get(repoDir); + if (cached && cached.expiresAt > now) { + return cached.data; + } + const promise = listGitRuns(repoDir); + gitRunsCache.set(repoDir, { data: promise, expiresAt: now + GIT_RUNS_CACHE_TTL_MS }); + // Evict stale entry once the promise settles so a fresh fetch replaces it + promise.catch(() => {}).finally(() => { + const entry = gitRunsCache.get(repoDir); + if (entry && entry.expiresAt <= Date.now()) { + gitRunsCache.delete(repoDir); + } + }); + return promise; +} + export type RunSource = 'local' | 'remote'; export interface SourcedResultFileMeta extends ResultFileMeta { @@ -129,7 +154,7 @@ export async function getRemoteResultsStatus(cwd: string): Promise ({ filename: encodeRemoteRunId(r.run_id), raw_filename: r.run_id, diff --git a/apps/cli/src/commands/results/serve.ts b/apps/cli/src/commands/results/serve.ts index c7c47787..9eac714d 100644 --- a/apps/cli/src/commands/results/serve.ts +++ b/apps/cli/src/commands/results/serve.ts @@ -1607,7 +1607,9 @@ export const resultsServeCommand = command({ // ── Project sync preflight ─────────────────────────────────────── // Clone or pull any project entries that declare a source. - await syncProjects(registry.projects); + // Non-blocking: fire-and-forget so startup is instant even when some + // project paths are missing or slow (e.g. /tmp paths that timeout). + syncProjects(registry.projects).catch((err) => console.error("Background project sync failed:", err)); try { let results: EvaluationResult[] = []; diff --git a/apps/studio/src/components/EvalDetail.tsx b/apps/studio/src/components/EvalDetail.tsx index 283a665a..9b114d0c 100644 --- a/apps/studio/src/components/EvalDetail.tsx +++ b/apps/studio/src/components/EvalDetail.tsx @@ -264,6 +264,7 @@ function FilesTab({ const files = filesData?.files ?? []; const [selectedPath, setSelectedPath] = useState(null); + const [mobileShowTree, setMobileShowTree] = useState(false); const effectivePath = selectedPath ?? (files.length > 0 ? findFirstFile(files) : null); @@ -284,11 +285,52 @@ function FilesTab({ const displayLanguage = effectivePath ? (fileContentData?.language ?? 'plaintext') : 'plaintext'; return ( -
- -
+
+ {/* FileTree panel — desktop: side-by-side, mobile: full-width slide-over */} +
+ { + setSelectedPath(path); + // On mobile, auto-switch to content viewer after selecting a file + setMobileShowTree(false); + }} + /> +
+ + {/* MonacoViewer panel — desktop: side-by-side, mobile: full-width */} +
+ + {/* Mobile toggle button — floating bottom-right */} +
); } diff --git a/apps/studio/src/components/FileTree.tsx b/apps/studio/src/components/FileTree.tsx index c9c29953..917f466b 100644 --- a/apps/studio/src/components/FileTree.tsx +++ b/apps/studio/src/components/FileTree.tsx @@ -131,7 +131,7 @@ export function FileTree({ files, selectedPath, onSelect }: FileTreeProps) { }; return ( -
+
{files.length === 0 &&

No files.

} {files.map((node) => (