diff --git a/apps/studio/src-tauri/src/lib.rs b/apps/studio/src-tauri/src/lib.rs index a40d002..ea9739f 100644 --- a/apps/studio/src-tauri/src/lib.rs +++ b/apps/studio/src-tauri/src/lib.rs @@ -63,6 +63,7 @@ struct IgnitionProjectTraceFile { struct IgnitionProjectCheckpointFile { path: String, entry: Value, + payload: Option, } #[derive(Debug, Serialize)] @@ -410,7 +411,7 @@ fn run_checkpoint_inference_path( let checkpoint_index_path = source_run_dir .join("checkpoints") .join(CHECKPOINT_INDEX_FILE); - let (checkpoints, _) = read_checkpoint_index(&checkpoint_index_path)?; + let (checkpoints, _) = read_checkpoint_index(&root, &checkpoint_index_path)?; let checkpoint = checkpoints .into_iter() .find(|entry| entry.entry.get("id").and_then(Value::as_str) == Some(checkpoint_id.as_str())) @@ -612,8 +613,10 @@ fn read_project_runs( let (metrics, metric_bytes) = read_metrics(&run_dir.join(METRICS_FILE))?; let (traces, trace_bytes) = read_trace_files(root, &run_dir.join("traces"))?; - let (checkpoints, checkpoint_bytes) = - read_checkpoint_index(&run_dir.join("checkpoints").join(CHECKPOINT_INDEX_FILE))?; + let (checkpoints, checkpoint_bytes) = read_checkpoint_index( + root, + &run_dir.join("checkpoints").join(CHECKPOINT_INDEX_FILE), + )?; byte_length += metric_bytes + trace_bytes + checkpoint_bytes; @@ -683,7 +686,10 @@ fn read_trace_files( Ok((traces, byte_length)) } -fn read_checkpoint_index(path: &Path) -> Result<(Vec, u64), String> { +fn read_checkpoint_index( + root: &Path, + path: &Path, +) -> Result<(Vec, u64), String> { if !path.exists() { return Ok((Vec::new(), 0)); } @@ -696,9 +702,15 @@ fn read_checkpoint_index(path: &Path) -> Result<(Vec { expect(inferenceCalls).toBe(1); expect(text).toContain("studio-checkpoint-inference-final"); expect(text).toContain("checkpoint_inference"); + expect(text).toContain("Payload Preview"); + expect(text).toContain("Linked Inference Runs"); + expect(text).toContain("inference replay"); expect(text).toContain("Frame Detail"); expect(text).toContain("Reward Debugger"); expect(text).not.toContain("No replay loaded."); + const baselineSelect = renderer?.root.findByProps({ + "aria-label": "Compare against run", + }); + + act(() => { + baselineSelect?.props.onChange({ target: { value: "native-run" } }); + }); + + const comparisonText = JSON.stringify(renderer?.toJSON()); + + expect(comparisonText).toContain("Compare against"); + expect(comparisonText).toContain("inference replay"); + expect(comparisonText).toContain("training run"); + act(() => renderer?.unmount()); }); @@ -1036,6 +1053,34 @@ function chartPointLabel(renderer: ReactTestRenderer | undefined): string { .join("") ?? ""; } +function nativeCheckpointPayload() { + return { + version: 1, + algorithm: "tabular-q-learning", + envId: "Target2D-v0", + observationShape: [4], + actions: ["right"], + config: { + learningRate: 0.1, + discount: 0.95, + epsilon: 0, + initialQ: 0, + observationPrecision: 2, + seed: 7, + }, + metrics: { + states: 1, + transitions: 1, + episodes: 1, + lastTdError: 0, + }, + qTable: { + "0,0,1,0": [1], + }, + createdAt: "2026-05-28T00:01:00.000Z", + }; +} + function nativeProjectDirectory(): NativeIgnitionProjectDirectory { const createdAt = "2026-05-28T00:00:00.000Z"; const runUpdatedAt = "2026-05-28T00:01:00.000Z"; @@ -1132,6 +1177,7 @@ function nativeProjectDirectory(): NativeIgnitionProjectDirectory { path: "runs/native-run/checkpoints/final.json", createdAt: runUpdatedAt, }, + payload: nativeCheckpointPayload(), }], }], artifacts: [{ diff --git a/apps/studio/src/App.tsx b/apps/studio/src/App.tsx index 2cf649a..c9b0387 100644 --- a/apps/studio/src/App.tsx +++ b/apps/studio/src/App.tsx @@ -28,7 +28,10 @@ import { type NativeStudioApi, type NativeTrainingMetricEvent, } from "./native"; -import { studioWorkspaceFromNativeProjectDirectory } from "./native-project"; +import { + studioCheckpointViewsFromNativeProjectDirectory, + studioWorkspaceFromNativeProjectDirectory, +} from "./native-project"; import { sampleWorkspace } from "./sample-workspace"; import { StudioViewportPanel } from "./StudioViewportPanel"; import "./styles.css"; @@ -301,6 +304,20 @@ export function App({ setLoadError(undefined); } + function applyNativeProjectDirectory( + directory: NativeCheckpointInferenceRunResult["project"], + options: { readonly runId?: string; readonly episodeId?: string } = {}, + ): StudioWorkspaceView { + const nextWorkspace = studioWorkspaceFromNativeProjectDirectory(directory, options); + + applyWorkspaceView(nextWorkspace); + setCheckpointViews(studioCheckpointViewsFromNativeProjectDirectory(directory, nextWorkspace)); + setLoadedProjectPath(directory.path); + setLoadedSource(directory.directoryName); + + return nextWorkspace; + } + function applyNativeTrainingResult(result: NativeLocalTrainingRunResult) { const trainingSession = result.trainingSession; @@ -309,12 +326,8 @@ export function App({ } const runId = trainingSession.run?.id; - const nextWorkspace = studioWorkspaceFromNativeProjectDirectory( - result.project, - runId === undefined ? {} : { runId }, - ); - applyWorkspaceView(nextWorkspace); + applyNativeProjectDirectory(result.project, runId === undefined ? {} : { runId }); setTrainingSessionViews((views) => upsertTrainingSessionView(views, trainingSession)); setSelectedTrainingSessionId(trainingSession.id); @@ -325,26 +338,20 @@ export function App({ if (runId !== undefined) { setSelectedRunId(runId); } - - setLoadedProjectPath(result.project.path); - setLoadedSource(result.project.directoryName); } function applyNativeCheckpointInferenceResult( result: NativeCheckpointInferenceRunResult, checkpoint: StoredCheckpoint, ) { - const nextWorkspace = studioWorkspaceFromNativeProjectDirectory(result.project, { + applyNativeProjectDirectory(result.project, { runId: result.runId, episodeId: result.episodeId, }); - applyWorkspaceView(nextWorkspace); setSelectedRunId(result.runId); setSelectedEpisodeId(result.episodeId); setSelectedCheckpointKey(checkpointKey(checkpoint)); - setLoadedProjectPath(result.project.path); - setLoadedSource(result.project.directoryName); } function ingestTrainingMetricEvent(event: NativeTrainingMetricEvent) { @@ -395,9 +402,7 @@ export function App({ return; } - applyStudioArtifact(studioWorkspaceFromNativeProjectDirectory(directory)); - setLoadedSource(directory.directoryName); - setLoadedProjectPath(directory.path); + applyNativeProjectDirectory(directory); } catch (error) { setLoadError(error instanceof Error ? error.message : String(error)); } @@ -613,7 +618,10 @@ export function App({

{selectedHistoryRow?.runId ?? "No run selected"}

-

{selectedHistoryRow?.algorithm ?? "No algorithm"}

+

+ {selectedHistoryRow?.algorithm ?? "No algorithm"} + {selectedHistoryRow === undefined ? "" : ` · ${runModeLabel(selectedHistoryRow)}`} +

{selectedHistoryRow?.status ?? "empty"} @@ -740,7 +748,7 @@ function RunComparisonPanel(props: {
Focus run {selectedRun.runId} - {selectedRun.algorithm} + {selectedRun.algorithm} · {runModeLabel(selectedRun)}