diff --git a/apps/fabro-web/app/components/runs-list/run-table-row.tsx b/apps/fabro-web/app/components/runs-list/run-table-row.tsx
index 52efd1152..d1b328178 100644
--- a/apps/fabro-web/app/components/runs-list/run-table-row.tsx
+++ b/apps/fabro-web/app/components/runs-list/run-table-row.tsx
@@ -5,6 +5,7 @@ import type { RunWithStatus } from "../../data/runs";
import { formatRelativeTime } from "../../lib/format";
import { InlineMarkdown } from "../inline-markdown";
import { PullRequestChip } from "../pull-request-chip";
+import { SizeChip } from "../size-chip";
import { RowActionsMenu } from "./row-actions-menu";
import { SelectionCheckbox } from "./selection-checkbox";
import type { ToggleableColumn } from "./toggleable-column";
@@ -101,6 +102,11 @@ export function RunTableRow({
{run.elapsed}
)}
+ {show("size") && (
+
+ {run.size != null && }
+ |
+ )}
{show("changes") && (
{run.additions != null && +{run.additions.toLocaleString()}}
diff --git a/apps/fabro-web/app/components/runs-list/runs-list-view.tsx b/apps/fabro-web/app/components/runs-list/runs-list-view.tsx
index 84780c578..6437c2cfb 100644
--- a/apps/fabro-web/app/components/runs-list/runs-list-view.tsx
+++ b/apps/fabro-web/app/components/runs-list/runs-list-view.tsx
@@ -150,6 +150,9 @@ export function RunsListView({
{show("elapsed") && (
)}
+ {show("size") && (
+
+ )}
{show("changes") && (
)}
diff --git a/apps/fabro-web/app/components/runs-list/toggleable-column.ts b/apps/fabro-web/app/components/runs-list/toggleable-column.ts
index 8907f306e..3fcc3eb65 100644
--- a/apps/fabro-web/app/components/runs-list/toggleable-column.ts
+++ b/apps/fabro-web/app/components/runs-list/toggleable-column.ts
@@ -1,9 +1,10 @@
export const TOGGLEABLE_COLUMNS = [
- "elapsed",
"repo",
"workflow",
"created",
"updated",
+ "elapsed",
+ "size",
"changes",
"pr",
] as const;
@@ -11,11 +12,12 @@ export const TOGGLEABLE_COLUMNS = [
export type ToggleableColumn = (typeof TOGGLEABLE_COLUMNS)[number];
export const toggleableColumnLabels: Record = {
- elapsed: "Elapsed",
repo: "Repo",
workflow: "Workflow",
created: "Created",
updated: "Updated",
+ elapsed: "Elapsed",
+ size: "Size",
changes: "Changes",
pr: "PR",
};
diff --git a/apps/fabro-web/app/components/size-chip.tsx b/apps/fabro-web/app/components/size-chip.tsx
new file mode 100644
index 000000000..e3d07ed40
--- /dev/null
+++ b/apps/fabro-web/app/components/size-chip.tsx
@@ -0,0 +1,33 @@
+import type { RunSize } from "@qltysh/fabro-api-client";
+
+import { formatUsdMicros } from "../lib/format";
+import { Tooltip } from "./ui";
+
+const SIZE_TONE: Record = {
+ XS: { className: "bg-overlay text-fg-muted", note: null },
+ S: { className: "bg-overlay text-fg-muted", note: null },
+ M: { className: "bg-overlay text-fg-muted", note: null },
+ L: { className: "bg-amber/15 text-amber", note: "risky" },
+ XL: { className: "bg-coral/15 text-coral", note: "unhealthy" },
+};
+
+export function SizeChip({
+ size,
+ totalUsdMicros,
+}: {
+ size: RunSize;
+ totalUsdMicros?: number | null;
+}) {
+ const tone = SIZE_TONE[size];
+ const billed = totalUsdMicros != null ? ` · ${formatUsdMicros(totalUsdMicros)} billed` : "";
+ const tooltip = tone.note != null
+ ? `Size ${size} (${tone.note})${billed}`
+ : `Size ${size}${billed}`;
+ return (
+
+
+ {size}
+
+
+ );
+}
diff --git a/apps/fabro-web/app/data/runs.ts b/apps/fabro-web/app/data/runs.ts
index 6f587846c..1ddcb14a1 100644
--- a/apps/fabro-web/app/data/runs.ts
+++ b/apps/fabro-web/app/data/runs.ts
@@ -2,6 +2,7 @@ import { formatDurationMs } from "../lib/format";
import {
BoardColumn,
type Run,
+ type RunSize,
type RunStatus as ApiRunStatus,
} from "@qltysh/fabro-api-client";
@@ -39,6 +40,7 @@ export interface RunItem {
sourceDirectory?: string;
createdAt?: string;
lastEventAt?: string;
+ size?: RunSize;
}
export const columnStatuses = [
@@ -109,6 +111,7 @@ export function mapRunListItem(item: Run): RunItem {
lastEventAt: item.timestamps.last_event_at ?? undefined,
additions: item.diff?.additions,
deletions: item.diff?.deletions,
+ size: item.size,
};
}
diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx
index 06ebbab7c..6deedbe6e 100644
--- a/apps/fabro-web/app/routes/run-detail.tsx
+++ b/apps/fabro-web/app/routes/run-detail.tsx
@@ -36,6 +36,7 @@ import {
import { EditableRunTitle } from "../components/editable-run-title";
import { GitPullRequestIcon } from "../components/icons";
import { InterviewDock } from "../components/interview-dock";
+import { SizeChip } from "../components/size-chip";
import { SteerBar, type SteerBarHandle } from "../components/steer-bar";
import { ErrorState } from "../components/state";
import { useToast } from "../components/toast";
@@ -82,7 +83,6 @@ import {
formatAbsoluteTs,
formatDurationMs,
formatRelativeTime,
- formatUsdMicros,
} from "../lib/format";
import { queryKeys } from "../lib/query-keys";
import { useRunEvents } from "../lib/run-events";
@@ -561,16 +561,8 @@ export default function RunDetail({ params }: { params: { id: string } }) {
{run.workflow}
);
- const totalUsdMicros = summary.billing?.total_usd_micros;
- const sizeTooltip = totalUsdMicros != null
- ? `Size ${summary.size} · ${formatUsdMicros(totalUsdMicros)} billed`
- : `Size ${summary.size}`;
const sizeChip = (
-
-
- {summary.size}
-
-
+
);
const visibility = lifecycleActionVisibility(run.lifecycleStatus);
diff --git a/apps/fabro-web/app/routes/runs.test.tsx b/apps/fabro-web/app/routes/runs.test.tsx
index 0cea3dca4..a77f36379 100644
--- a/apps/fabro-web/app/routes/runs.test.tsx
+++ b/apps/fabro-web/app/routes/runs.test.tsx
@@ -249,7 +249,7 @@ describe("runs route workspace preferences", () => {
}),
);
- expect(loadStoredRunsWorkspaceSearchParams(storage).toString()).toBe("hide=elapsed%2Crepo");
+ expect(loadStoredRunsWorkspaceSearchParams(storage).toString()).toBe("hide=repo%2Celapsed");
});
test("valid stored preferences produce canonical URL params", () => {
diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml
index 63ac4fc8c..0e6075427 100644
--- a/docs/public/api-reference/fabro-api.yaml
+++ b/docs/public/api-reference/fabro-api.yaml
@@ -4763,7 +4763,7 @@ components:
description: Field to sort by. Defaults to `created_at`.
schema:
type: string
- enum: [created_at, updated_at, status, elapsed, repo, title, workflow, changes]
+ enum: [created_at, updated_at, status, elapsed, repo, title, workflow, changes, size]
default: created_at
example: created_at
diff --git a/lib/crates/fabro-server/src/server/handler/runs.rs b/lib/crates/fabro-server/src/server/handler/runs.rs
index c5e892379..090d1ba5d 100644
--- a/lib/crates/fabro-server/src/server/handler/runs.rs
+++ b/lib/crates/fabro-server/src/server/handler/runs.rs
@@ -97,6 +97,7 @@ enum RunsSortKey {
Title,
Workflow,
Changes,
+ Size,
}
#[derive(Debug, Clone, Copy, Default, serde::Deserialize)]
@@ -194,6 +195,7 @@ fn sort_runs(runs: &mut [fabro_types::Run], key: RunsSortKey, direction: RunsSor
RunsSortKey::Title => run_title_key(a).cmp(&run_title_key(b)),
RunsSortKey::Workflow => run_workflow_key(a).cmp(&run_workflow_key(b)),
RunsSortKey::Changes => run_changes_total(a).cmp(&run_changes_total(b)),
+ RunsSortKey::Size => a.size.cmp(&b.size),
};
let primary = if asc { primary } else { primary.reverse() };
// Stable tiebreak: newer ULIDs (and thus newer runs) first.
diff --git a/lib/crates/fabro-types/src/run_summary.rs b/lib/crates/fabro-types/src/run_summary.rs
index fb5e7f576..1cde31614 100644
--- a/lib/crates/fabro-types/src/run_summary.rs
+++ b/lib/crates/fabro-types/src/run_summary.rs
@@ -211,6 +211,8 @@ pub struct RunBillingSummary {
Default,
PartialEq,
Eq,
+ PartialOrd,
+ Ord,
Hash,
Serialize,
Deserialize,
diff --git a/lib/packages/fabro-api-client/src/api/runs-api.ts b/lib/packages/fabro-api-client/src/api/runs-api.ts
index 9a074ce61..0977f14f1 100644
--- a/lib/packages/fabro-api-client/src/api/runs-api.ts
+++ b/lib/packages/fabro-api-client/src/api/runs-api.ts
@@ -1120,7 +1120,7 @@ export const RunsApiAxiosParamCreator = function (configuration?: Configuration)
};
},
/**
- * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
+ * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
* @summary Retry Run
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@@ -1868,7 +1868,7 @@ export const RunsApiFp = function(configuration?: Configuration) {
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath);
},
/**
- * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
+ * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
* @summary Retry Run
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@@ -2264,7 +2264,7 @@ export const RunsApiFactory = function (configuration?: Configuration, basePath?
return localVarFp.retrieveRunGraphSource(id, options).then((request) => request(axios, basePath));
},
/**
- * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
+ * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
* @summary Retry Run
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@@ -2652,7 +2652,7 @@ export class RunsApi extends BaseAPI {
}
/**
- * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Cancelled, active, succeeded, and archived runs are not retryable.
+ * Creates a fresh run from the failed or dead source run\'s captured durable definition, records `retried_from` on the new run, and schedules it for execution. The source run is left unchanged. Active, succeeded, and archived runs are not retryable.
* @summary Retry Run
* @param {string} id Unique run identifier (ULID).
* @param {*} [options] Override http request option.
@@ -2773,7 +2773,8 @@ export const ListRunsSortEnum = {
REPO: 'repo',
TITLE: 'title',
WORKFLOW: 'workflow',
- CHANGES: 'changes'
+ CHANGES: 'changes',
+ SIZE: 'size'
} as const;
export type ListRunsSortEnum = typeof ListRunsSortEnum[keyof typeof ListRunsSortEnum];
export const ListRunsDirectionEnum = {
|