Rename /runs/{id}/compare to /runs/{id}/files with pagination

Replace the RunCompare envelope (checkpoints + files + stats) with a
standard PaginatedRunFileList response containing FileDiff items,
matching the existing pagination pattern used by other endpoints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-08 14:37:30 -04:00
parent e73f2ab00e
commit 5a720ddb26
8 changed files with 55 additions and 106 deletions

View file

@ -27,7 +27,7 @@ export default [
route("stages/:stageId", "routes/run-stages.tsx"),
route("configuration", "routes/run-configuration.tsx"),
route("graph", "routes/run-graph.tsx"),
route("compare", "routes/run-compare.tsx"),
route("files", "routes/run-files.tsx"),
route("verification", "routes/run-verification.tsx"),
route("usage", "routes/run-usage.tsx"),
route("retro", "routes/run-retro.tsx"),

View file

@ -11,7 +11,7 @@ import type { Route } from "./+types/run-detail";
const tabs = [
{ name: "Overview", path: "", count: null },
{ name: "Stages", path: "/stages/detect-drift", count: null },
{ name: "Files Changed", path: "/compare", count: null },
{ name: "Files Changed", path: "/files", count: null },
{ name: "Verification", path: "/verification", count: null },
{ name: "Retro", path: "/retro", count: null },
{ name: "Usage", path: "/usage", count: null },

View file

@ -1,6 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams } from "react-router";
import { ChevronDownIcon, Cog6ToothIcon } from "@heroicons/react/24/outline";
import {
MultiFileDiff,
type AnnotationSide,
@ -8,13 +6,13 @@ import {
} from "@pierre/diffs/react";
import { useTheme } from "../lib/theme";
import { apiJson } from "../api-client";
import type { RunCompare } from "@qltysh/arc-api-client";
import type { Route } from "./+types/run-compare";
import type { PaginatedRunFileList } from "@qltysh/arc-api-client";
import type { Route } from "./+types/run-files";
export const handle = { wide: true };
export async function loader({ request, params }: Route.LoaderArgs) {
const data = await apiJson<RunCompare>(`/runs/${params.id}/compare`, { request });
const data = await apiJson<PaginatedRunFileList>(`/runs/${params.id}/files`, { request });
return data;
}
@ -217,29 +215,6 @@ export async function execute(
},
];
const BLOCK_COUNT = 5;
function DiffStat({ additions, deletions }: { additions: number; deletions: number }) {
const total = additions + deletions;
const addBlocks = total === 0 ? 0 : Math.round((additions / total) * BLOCK_COUNT);
const delBlocks = BLOCK_COUNT - addBlocks;
return (
<div className="flex items-center gap-2 font-mono text-xs">
<span className="font-semibold text-mint">+{additions.toLocaleString()}</span>
<span className="font-semibold text-coral">-{deletions.toLocaleString()}</span>
<div className="flex gap-0.5">
{Array.from({ length: BLOCK_COUNT }, (_, i) => (
<span
key={`block-${i}`}
className={`inline-block size-2.5 rounded-sm ${i < addBlocks ? "bg-mint" : "bg-coral"}`}
/>
))}
</div>
</div>
);
}
interface SteerAnnotation {
fileName: string;
lineNumber: number;
@ -508,21 +483,14 @@ function buildAnnotationsForFile(
return annotations;
}
export default function RunCompare({ loaderData }: Route.ComponentProps) {
export default function RunFiles({ loaderData }: Route.ComponentProps) {
const runFiles = loaderData;
const checkpoints = [
{ id: "all", label: "All changes" },
...runFiles.checkpoints.map((cp) => ({ id: cp.id, label: cp.label })),
];
const files = runFiles.files.length > 0
? runFiles.files.map((f) => ({
const files = runFiles.data.length > 0
? runFiles.data.map((f) => ({
oldFile: { name: f.old_file.name, contents: f.old_file.contents },
newFile: { name: f.new_file.name, contents: f.new_file.contents },
}))
: fallbackFiles;
const diffStats = runFiles.stats;
const [checkpoint, setCheckpoint] = useState(checkpoints[0].id);
const [openSteers, setOpenSteers] = useState(
() => new Map<string, SteerAnnotation>(),
);
@ -560,31 +528,6 @@ export default function RunCompare({ loaderData }: Route.ComponentProps) {
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="relative">
<select
value={checkpoint}
onChange={(e) => setCheckpoint(e.target.value)}
className="appearance-none rounded-md border border-line bg-panel/80 py-2 pl-3 pr-8 text-sm text-fg-2 outline-none transition-colors focus:border-focus focus:ring-0"
>
{checkpoints.map((cp) => (
<option key={cp.id} value={cp.id}>{cp.label}</option>
))}
</select>
<ChevronDownIcon className="pointer-events-none absolute right-2 top-1/2 size-4 -translate-y-1/2 text-fg-muted" />
</div>
<div className="ml-auto flex items-center gap-3">
<DiffStat additions={diffStats.additions} deletions={diffStats.deletions} />
<button
type="button"
title="Settings"
className="flex size-8 items-center justify-center rounded-md border border-line bg-panel/80 text-fg-3 transition-colors hover:bg-overlay hover:text-fg"
>
<Cog6ToothIcon className="size-4" />
</button>
</div>
</div>
{files.map(({ oldFile, newFile }) => (
<DiffWithSteer
key={newFile.name}

View file

@ -74,7 +74,7 @@ To migrate: Update any DOT workflow prompts referencing `{{script.output}}` or `
</Warning>
<Warning>
**`GET /runs/{id}/files` renamed to `GET /runs/{id}/compare`.** The endpoint returns the same data but the path now reflects its purpose of comparing file changes between checkpoints.
**`GET /runs/{id}/compare` renamed to `GET /runs/{id}/files`.** The endpoint now returns a standard paginated list of `FileDiff` items instead of the `RunCompare` envelope.
To migrate: Update API integrations using `/files` to use `/compare`.
To migrate: Update API integrations using `/compare` to use `/files`, and adjust response parsing from `{ checkpoints, files, stats }` to `{ data, meta }`.
</Warning>

View file

@ -74,6 +74,7 @@ models/paginated-api-question-list.ts
models/paginated-history-entry-list.ts
models/paginated-model-list.ts
models/paginated-retro-list.ts
models/paginated-run-file-list.ts
models/paginated-run-list.ts
models/paginated-run-stage-list.ts
models/paginated-run-verification-list.ts
@ -95,7 +96,6 @@ models/retro-stats.ts
models/root-response-urls.ts
models/root-response.ts
models/run-checkpoint.ts
models/run-compare.ts
models/run-configuration.ts
models/run-error.ts
models/run-list-item.ts

View file

@ -24,9 +24,9 @@ import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError
// @ts-ignore
import type { ErrorResponse } from '../models';
// @ts-ignore
import type { PaginatedRunVerificationList } from '../models';
import type { PaginatedRunFileList } from '../models';
// @ts-ignore
import type { RunCompare } from '../models';
import type { PaginatedRunVerificationList } from '../models';
// @ts-ignore
import type { RunUsage } from '../models';
/**
@ -35,17 +35,19 @@ import type { RunUsage } from '../models';
export const RunOutputsApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
* Returns file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Compare
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Files
* @param {string} id Unique run identifier (ULID).
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveRunCompare: async (id: string, checkpoint?: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
retrieveRunFiles: async (id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('retrieveRunCompare', 'id', id)
const localVarPath = `/runs/{id}/compare`
assertParamExists('retrieveRunFiles', 'id', id)
const localVarPath = `/runs/{id}/files`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -69,6 +71,14 @@ export const RunOutputsApiAxiosParamCreator = function (configuration?: Configur
localVarQueryParameter['checkpoint'] = checkpoint;
}
if (pageLimit !== undefined) {
localVarQueryParameter['page[limit]'] = pageLimit;
}
if (pageOffset !== undefined) {
localVarQueryParameter['page[offset]'] = pageOffset;
}
localVarHeaderParameter['Accept'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -182,17 +192,19 @@ export const RunOutputsApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = RunOutputsApiAxiosParamCreator(configuration)
return {
/**
* Returns file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Compare
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Files
* @param {string} id Unique run identifier (ULID).
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async retrieveRunCompare(id: string, checkpoint?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<RunCompare>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveRunCompare(id, checkpoint, options);
async retrieveRunFiles(id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PaginatedRunFileList>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveRunFiles(id, checkpoint, pageLimit, pageOffset, options);
const localVarOperationServerIndex = configuration?.serverIndex ?? 0;
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.retrieveRunCompare']?.[localVarOperationServerIndex]?.url;
const localVarOperationServerBasePath = operationServerMap['RunOutputsApi.retrieveRunFiles']?.[localVarOperationServerIndex]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
@ -233,15 +245,17 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas
const localVarFp = RunOutputsApiFp(configuration)
return {
/**
* Returns file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Compare
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Files
* @param {string} id Unique run identifier (ULID).
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
retrieveRunCompare(id: string, checkpoint?: string, options?: RawAxiosRequestConfig): AxiosPromise<RunCompare> {
return localVarFp.retrieveRunCompare(id, checkpoint, options).then((request) => request(axios, basePath));
retrieveRunFiles(id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig): AxiosPromise<PaginatedRunFileList> {
return localVarFp.retrieveRunFiles(id, checkpoint, pageLimit, pageOffset, options).then((request) => request(axios, basePath));
},
/**
* Returns token and cost usage broken down by stage and model for a specific run.
@ -273,15 +287,17 @@ export const RunOutputsApiFactory = function (configuration?: Configuration, bas
*/
export class RunOutputsApi extends BaseAPI {
/**
* Returns file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Compare
* Returns a paginated list of file-level diffs produced by the run, optionally filtered to a specific checkpoint.
* @summary Retrieve Run Files
* @param {string} id Unique run identifier (ULID).
* @param {string} [checkpoint] Filter to a specific checkpoint ID. Omit to include all changes.
* @param {number} [pageLimit] Maximum number of items to return per page.
* @param {number} [pageOffset] Number of items to skip before returning results.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
public retrieveRunCompare(id: string, checkpoint?: string, options?: RawAxiosRequestConfig) {
return RunOutputsApiFp(this.configuration).retrieveRunCompare(id, checkpoint, options).then((request) => request(this.axios, this.basePath));
public retrieveRunFiles(id: string, checkpoint?: string, pageLimit?: number, pageOffset?: number, options?: RawAxiosRequestConfig) {
return RunOutputsApiFp(this.configuration).retrieveRunFiles(id, checkpoint, pageLimit, pageOffset, options).then((request) => request(this.axios, this.basePath));
}
/**

View file

@ -54,6 +54,7 @@ export * from './paginated-api-question-list';
export * from './paginated-history-entry-list';
export * from './paginated-model-list';
export * from './paginated-retro-list';
export * from './paginated-run-file-list';
export * from './paginated-run-list';
export * from './paginated-run-stage-list';
export * from './paginated-run-verification-list';
@ -75,7 +76,6 @@ export * from './retro-stats';
export * from './root-response';
export * from './root-response-urls';
export * from './run-checkpoint';
export * from './run-compare';
export * from './run-configuration';
export * from './run-error';
export * from './run-list-item';

View file

@ -13,28 +13,18 @@
*/
// May contain unused imports in some cases
// @ts-ignore
import type { DiffStats } from './diff-stats';
// May contain unused imports in some cases
// @ts-ignore
import type { FileCheckpoint } from './file-checkpoint';
// May contain unused imports in some cases
// @ts-ignore
import type { FileDiff } from './file-diff';
// May contain unused imports in some cases
// @ts-ignore
import type { PaginationMeta } from './pagination-meta';
/**
* File-level diff output for a run, with checkpoint filtering support.
* Paginated list of file diffs produced by a run.
*/
export interface RunCompare {
/**
* Available checkpoints for filtering.
*/
'checkpoints': Array<FileCheckpoint>;
/**
* File diffs, optionally filtered by checkpoint.
*/
'files': Array<FileDiff>;
'stats': DiffStats;
export interface PaginatedRunFileList {
'data': Array<FileDiff>;
'meta': PaginationMeta;
}