Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions apps/cli/src/commands/results/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { data: any; expiresAt: number }>();
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 {
Expand Down Expand Up @@ -129,7 +154,7 @@ export async function getRemoteResultsStatus(cwd: string): Promise<RemoteResults
let runCount = 0;
if (config && status.available) {
try {
runCount = (await listGitRuns(config.path)).length;
runCount = (await cachedListGitRuns(config.path)).length;
} catch {
runCount = listResultFilesFromRunsDir(resolveResultsRepoRunsDir(config)).length;
}
Expand Down Expand Up @@ -187,7 +212,7 @@ export async function listMergedResultFiles(
let remoteRuns: SourcedResultFileMeta[] = [];
if (config.mode === 'github') {
try {
const gitRuns = await listGitRuns(config.path);
const gitRuns = await cachedListGitRuns(config.path);
remoteRuns = gitRuns.map((r) => ({
filename: encodeRemoteRunId(r.run_id),
raw_filename: r.run_id,
Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/commands/results/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
48 changes: 45 additions & 3 deletions apps/studio/src/components/EvalDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ function FilesTab({
const files = filesData?.files ?? [];

const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [mobileShowTree, setMobileShowTree] = useState(false);

const effectivePath = selectedPath ?? (files.length > 0 ? findFirstFile(files) : null);

Expand All @@ -284,11 +285,52 @@ function FilesTab({
const displayLanguage = effectivePath ? (fileContentData?.language ?? 'plaintext') : 'plaintext';

return (
<div className="flex h-full min-h-[400px] gap-4">
<FileTree files={files} selectedPath={effectivePath} onSelect={setSelectedPath} />
<div className="flex-1">
<div className="relative flex h-full min-h-[400px] gap-4">
{/* FileTree panel — desktop: side-by-side, mobile: full-width slide-over */}
<div
className={`${
mobileShowTree ? 'block' : 'hidden'
} md:block w-full md:w-auto`}
>
<FileTree
files={files}
selectedPath={effectivePath}
onSelect={(path) => {
setSelectedPath(path);
// On mobile, auto-switch to content viewer after selecting a file
setMobileShowTree(false);
}}
/>
</div>

{/* MonacoViewer panel — desktop: side-by-side, mobile: full-width */}
<div
className={`${
!mobileShowTree ? 'block' : 'hidden'
} md:block flex-1 h-full`}
>
<MonacoViewer value={displayValue} language={displayLanguage} height="100%" />
</div>

{/* Mobile toggle button — floating bottom-right */}
<button
type="button"
onClick={() => setMobileShowTree(!mobileShowTree)}
className="md:hidden fixed bottom-4 right-4 z-50 flex items-center gap-2 rounded-full bg-gray-800 px-4 py-2.5 text-sm font-medium text-gray-200 shadow-lg border border-gray-700 hover:bg-gray-700 active:bg-gray-600 transition-colors"
aria-label={mobileShowTree ? 'Switch to file content viewer' : 'Switch to file tree'}
>
{mobileShowTree ? (
<>
<span>📄</span>
<span>Content</span>
</>
) : (
<>
<span>📁</span>
<span>Files</span>
</>
)}
</button>
</div>
);
}
2 changes: 1 addition & 1 deletion apps/studio/src/components/FileTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export function FileTree({ files, selectedPath, onSelect }: FileTreeProps) {
};

return (
<div className="w-64 overflow-y-auto rounded-lg border border-gray-800 bg-gray-900 py-2">
<div className="w-full md:w-64 overflow-y-auto rounded-lg border border-gray-800 bg-gray-900 py-2">
{files.length === 0 && <p className="px-4 py-2 text-sm text-gray-500">No files.</p>}
{files.map((node) => (
<TreeNode
Expand Down
Loading