diff --git a/apps/cli/src/commands/results/serve.ts b/apps/cli/src/commands/results/serve.ts index cea680041..b2d16a335 100644 --- a/apps/cli/src/commands/results/serve.ts +++ b/apps/cli/src/commands/results/serve.ts @@ -554,6 +554,11 @@ function resolveRunArtifactPath( return { absolutePath }; } +function runWorkspaceDirFromManifestPath(manifestPath: string): string { + const manifestDir = path.dirname(manifestPath); + return path.basename(manifestDir) === '.internal' ? path.dirname(manifestDir) : manifestDir; +} + function isPathInsideDirectory(baseDir: string, candidatePath: string): boolean { const relative = path.relative(baseDir, candidatePath); return ( @@ -943,7 +948,7 @@ async function readArtifactCatalogEntryText( meta: SourcedResultFileMeta, entry: ArtifactCatalogEntry, ): Promise<{ content?: string; error?: string }> { - const baseDir = path.dirname(meta.path); + const baseDir = runWorkspaceDirFromManifestPath(meta.path); if (entry.storage === 'local') { const resolved = resolveReadableRunArtifactFile(baseDir, entry.displayPath); if (resolved.error) return { error: resolved.error }; @@ -1496,7 +1501,7 @@ async function handleRunLog(c: C, { searchDir, projectId }: DataContext) { if (meta.source === 'remote') { return c.json({ error: 'Run log is not available for remote runs' }, 404); } - const logPath = path.join(path.dirname(meta.path), 'console.log'); + const logPath = path.join(runWorkspaceDirFromManifestPath(meta.path), 'console.log'); if (!existsSync(logPath)) { return c.json({ error: 'Run log not found for this run' }, 404); } @@ -1527,7 +1532,7 @@ async function handleRunDetail(c: C, { searchDir, projectId }: DataContext) { const resumeMeta = meta.source === 'local' ? deriveResumeMeta(searchDir, meta.path, summaryMetadata) : {}; const liveStatus = meta.source === 'local' ? getActiveRunStatus(meta.path) : undefined; - const baseDir = path.dirname(meta.path); + const baseDir = runWorkspaceDirFromManifestPath(meta.path); return c.json({ results: attachExternalTraceFields( attachRunDetailReadModelFields(stripHeavyFields(loaded), records, baseDir), @@ -1906,7 +1911,7 @@ async function handleEvalDetail(c: C, { searchDir, projectId }: DataContext) { const selection = manifestRecordSelection(records, evalId, resultDir.value); const result = selection ? loaded[selection.index] : undefined; if (!selection || !result) return c.json({ error: 'Eval not found' }, 404); - const baseDir = path.dirname(meta.path); + const baseDir = runWorkspaceDirFromManifestPath(meta.path); const [stripped] = attachRunDetailReadModelFields( stripHeavyFields([result]), [selection.record], @@ -1932,7 +1937,7 @@ async function handleEvalFiles(c: C, { searchDir, projectId }: DataContext) { if (!selection) return c.json({ error: 'Eval not found' }, 404); const { record } = selection; - const baseDir = path.dirname(meta.path); + const baseDir = runWorkspaceDirFromManifestPath(meta.path); const catalog = buildResultArtifactCatalog(record, { runPath: relativeRunPathFromManifestPath(meta.path), }); @@ -1978,7 +1983,7 @@ async function handleEvalFileContent(c: C, { searchDir, projectId }: DataContext const entry = findArtifactCatalogEntry(catalog, filePath) ?? catalogEntryForDiscoveredLocalFile( - buildLocalResultArtifactTree(path.dirname(meta.path), record, catalog), + buildLocalResultArtifactTree(runWorkspaceDirFromManifestPath(meta.path), record, catalog), filePath, ); if (!entry) { diff --git a/apps/cli/test/commands/results/serve.test.ts b/apps/cli/test/commands/results/serve.test.ts index 5c1066e1d..2c46a1d46 100644 --- a/apps/cli/test/commands/results/serve.test.ts +++ b/apps/cli/test/commands/results/serve.test.ts @@ -3113,6 +3113,85 @@ describe('serve app', () => { expect(serializedFiles).toContain('"storage":"local"'); }); + it('loads canonical .internal index transcript artifacts from the run workspace root', async () => { + const runsDir = localResultsExperimentDir(tempDir, 'canonical-transcript'); + const runId = '2026-03-25T10-30-00-000Z'; + const timestampDir = path.join(runsDir, runId); + const resultDir = 'demo-test-greeting--111111111111'; + const transcriptArtifactPath = `${resultDir}/sample-1/transcript.json`; + const answerArtifactPath = `${resultDir}/sample-1/outputs/answer.md`; + const transcriptPath = path.join(timestampDir, transcriptArtifactPath); + const answerPath = path.join(timestampDir, answerArtifactPath); + const transcriptJson = `${JSON.stringify( + { + schema_version: 'agentv.normalized_transcript.v1', + target: 'codex', + turns: [ + { + v: 1, + agent: 'codex', + type: 'assistant', + content: [ + { + type: 'tool_use', + id: 'call-read-package', + tool_name: 'file_read', + input: { path: 'package.json' }, + result: { status: 'success', output: { name: 'demo' } }, + }, + ], + }, + ], + }, + null, + 2, + )}\n`; + + mkdirSync(path.dirname(transcriptPath), { recursive: true }); + writeFileSync(transcriptPath, transcriptJson); + mkdirSync(path.dirname(answerPath), { recursive: true }); + writeFileSync(answerPath, 'done'); + mkdirSync(path.join(timestampDir, '.internal'), { recursive: true }); + writeFileSync( + path.join(timestampDir, '.internal', 'index.jsonl'), + toJsonl({ + ...RESULT_A, + experiment: 'canonical-transcript', + result_dir: resultDir, + transcript_path: transcriptArtifactPath, + answer_path: answerArtifactPath, + }), + ); + + const app = createApp([], tempDir, tempDir, undefined, { studioDir }); + const transcriptRes = await app.request( + `/api/runs/${encodeURIComponent(runId)}/evals/test-greeting/transcript?result_dir=${encodeURIComponent(resultDir)}`, + ); + + expect(transcriptRes.status).toBe(200); + const transcriptData = (await transcriptRes.json()) as { + status: string; + transcript_path: string; + content: string; + answer_path: string; + answer_content: string; + }; + expect(transcriptData).toMatchObject({ + status: 'ok', + transcript_path: transcriptArtifactPath, + content: transcriptJson, + answer_path: answerArtifactPath, + answer_content: 'done', + }); + + const rawRes = await app.request( + `/api/runs/${encodeURIComponent(runId)}/evals/test-greeting/files/${transcriptArtifactPath}?result_dir=${encodeURIComponent(resultDir)}&raw=1`, + ); + + expect(rawRes.status).toBe(200); + expect(await rawRes.text()).toBe(transcriptJson); + }); + it('loads pointer-shaped transcript metadata when it resolves to a local artifact path', async () => { const runsDir = localResultsExperimentDir(tempDir, 'pointer-transcript'); const runId = '2026-03-25T11-00-00-000Z'; diff --git a/apps/dashboard/src/components/TranscriptTimeline.tsx b/apps/dashboard/src/components/TranscriptTimeline.tsx index 4621d04e6..022c67699 100644 --- a/apps/dashboard/src/components/TranscriptTimeline.tsx +++ b/apps/dashboard/src/components/TranscriptTimeline.tsx @@ -468,6 +468,12 @@ function defaultExpandedMessageIds(messages: readonly TranscriptMessageViewModel return ids; } +export function messageIdsWithToolCalls(entries: readonly TranscriptJsonLine[]): string[] { + return buildTranscriptViewModel(entries) + .filter((message) => message.toolCalls.length > 0) + .map((message) => message.id); +} + function summarizeRoleCounts(messages: readonly TranscriptMessageViewModel[]): Map { const counts = new Map(); for (const message of messages) { @@ -885,6 +891,19 @@ export function TranscriptTimeline({ }); } + function expandAllToolCalls() { + setExpandedToolIds(new Set(allToolIds)); + setExpandedMessageIds((current) => { + const next = new Set(current); + for (const message of messages) { + if (message.toolCalls.length > 0) { + next.add(message.id); + } + } + return next; + }); + } + return (
{hasCanonicalAnswer && ( @@ -964,7 +983,7 @@ export function TranscriptTimeline({