Skip to content
Merged
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
17 changes: 11 additions & 6 deletions apps/cli/src/commands/results/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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],
Expand All @@ -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),
});
Expand Down Expand Up @@ -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) {
Expand Down
79 changes: 79 additions & 0 deletions apps/cli/test/commands/results/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
21 changes: 20 additions & 1 deletion apps/dashboard/src/components/TranscriptTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number> {
const counts = new Map<string, number>();
for (const message of messages) {
Expand Down Expand Up @@ -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 (
<div className="space-y-4">
{hasCanonicalAnswer && (
Expand Down Expand Up @@ -964,7 +983,7 @@ export function TranscriptTimeline({
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => setExpandedToolIds(new Set(allToolIds))}
onClick={expandAllToolCalls}
className="rounded-md border border-gray-700 px-3 py-1.5 text-sm text-gray-300 transition-colors hover:border-amber-800 hover:text-amber-200"
>
Expand all tool calls
Expand Down
7 changes: 7 additions & 0 deletions apps/dashboard/src/components/transcript-timeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TranscriptTimeline,
findAnswerPath,
findTranscriptPath,
messageIdsWithToolCalls,
parseTranscriptJsonl,
} from './TranscriptTimeline';
import {
Expand Down Expand Up @@ -121,6 +122,12 @@ describe('TranscriptTimeline', () => {
expect(html).toMatch(/data-testid="tool-call-call-read-1" data-expanded="false"/);
});

it('identifies parent messages that must open when expanding all tool calls', () => {
const parsed = parseTranscriptJsonl(structuredTranscriptJsonl);

expect(messageIdsWithToolCalls(parsed.entries)).toEqual(['1-assistant-1']);
});

it('renders expand and collapse controls for tool calls', () => {
const html = renderStructuredTranscript();

Expand Down
Loading