)[key];
+ }
+ return current;
+}
+
+/**
+ * Default column label from a path: its last named key, ignoring trailing array
+ * indices (so `$.tags[0]` labels as `tags`, not `0`). Falls back to the last
+ * segment, then the raw path.
+ */
+export function labelFromPath(path: string): string {
+ let normalized = path.trim();
+ if (normalized.startsWith("$")) normalized = normalized.slice(1);
+ if (normalized.length > 0 && !normalized.startsWith(".") && !normalized.startsWith("[")) {
+ normalized = `.${normalized}`;
+ }
+
+ const re = /\.([^.[\]]+)|\[(\d+)\]|\['((?:\\.|[^'\\])*)'\]|\["((?:\\.|[^"\\])*)"\]/g;
+ let lastKey: string | undefined;
+ let lastSegment: string | undefined;
+ let match: RegExpExecArray | null;
+ while ((match = re.exec(normalized)) !== null) {
+ const bracketKey = match[3] ?? match[4];
+ const key = match[1] ?? (bracketKey !== undefined ? unescapeBracketKey(bracketKey) : undefined);
+ if (key !== undefined) {
+ lastKey = key;
+ lastSegment = key;
+ } else if (match[2] !== undefined) {
+ lastSegment = match[2];
+ }
+ }
+ return lastKey ?? lastSegment ?? path;
+}
diff --git a/apps/webapp/app/components/schedules/ScheduleInspector.tsx b/apps/webapp/app/components/schedules/ScheduleInspector.tsx
index 5b65b73afb7..6f6874d2bb6 100644
--- a/apps/webapp/app/components/schedules/ScheduleInspector.tsx
+++ b/apps/webapp/app/components/schedules/ScheduleInspector.tsx
@@ -184,6 +184,7 @@ export function ScheduleInspector({
Last 5 runs
{
const options: RunListOptions = {
projectId: project.id,
+ columns: { visibleStandardIds: [], smartSources: ["metadata"] },
};
// pagination
@@ -310,7 +311,7 @@ export class ApiRunListPresenter extends BasePresenter {
const metadata = await parsePacket(
{
data: run.metadata ?? undefined,
- dataType: run.metadataType,
+ dataType: run.metadataType ?? "application/json",
},
{
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
diff --git a/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts b/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
index 6eb299a5e7b..38149abe5e7 100644
--- a/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/ErrorGroupPresenter.server.ts
@@ -12,6 +12,12 @@ import {
type NextRunList,
} from "~/presenters/v3/NextRunListPresenter.server";
import { sortVersionsDescending } from "~/utils/semver";
+import type { RunColumnId, SmartColumnSource } from "~/components/runs/v3/runColumns";
+
+type RunColumnsSelect = {
+ visibleStandardIds: RunColumnId[];
+ smartSources: SmartColumnSource[];
+};
const errorGroupGranularity = new TimeGranularity([
{ max: "1h", granularity: "1m" },
@@ -33,6 +39,7 @@ export type ErrorGroupOptions = {
to?: number;
cursor?: string;
direction?: Direction;
+ columns?: RunColumnsSelect;
};
const DEFAULT_RUNS_PAGE_SIZE = 25;
@@ -99,6 +106,7 @@ export class ErrorGroupPresenter extends BasePresenter {
to,
cursor,
direction,
+ columns,
}: ErrorGroupOptions
) {
const displayableEnvironment = await findDisplayableEnvironment(environmentId, userId);
@@ -128,6 +136,7 @@ export class ErrorGroupPresenter extends BasePresenter {
to: time.to.getTime(),
cursor,
direction,
+ columns,
}),
this.getState(environmentId, summary?.taskIdentifier, fingerprint),
]);
@@ -397,6 +406,7 @@ export class ErrorGroupPresenter extends BasePresenter {
to?: number;
cursor?: string;
direction?: Direction;
+ columns?: RunColumnsSelect;
}
): Promise {
const runListPresenter = new NextRunListPresenter(this.replica, this.clickhouse);
@@ -412,6 +422,7 @@ export class ErrorGroupPresenter extends BasePresenter {
to: options.to,
cursor: options.cursor,
direction: options.direction,
+ columns: options.columns,
});
if (result.runs.length === 0) {
diff --git a/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts b/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts
index c9a120334f3..52836fad293 100644
--- a/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts
+++ b/apps/webapp/app/presenters/v3/NextRunListPresenter.server.ts
@@ -25,6 +25,11 @@ import { machinePresetFromRun } from "~/v3/machinePresets.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { isCancellableRunStatus, isFinalRunStatus, isPendingRunStatus } from "~/v3/taskStatus";
import { runTriggeredAt } from "~/v3/runTimestamps";
+import {
+ deriveRunSelect,
+ type RunColumnId,
+ type SmartColumnSource,
+} from "~/components/runs/v3/runColumns";
// Positive-only cache: only envs known to have runs are stored (empty envs are re-checked),
// so "has runs" is monotonic and the TTL can be very long. Tiered memory + Redis.
@@ -81,6 +86,15 @@ export type RunListOptions = {
pageSize?: number;
// Run the empty-state "has any run ever" probe. Only the runs list consumes it.
includeHasAnyRuns?: boolean;
+ /**
+ * Visible-column set used to derive the Postgres select. Omitted => the
+ * default select (all fields, no payload/output). Provided by the list route
+ * so payload/output are only hydrated when a smart column references them.
+ */
+ columns?: {
+ visibleStandardIds: RunColumnId[];
+ smartSources: SmartColumnSource[];
+ };
};
const DEFAULT_PAGE_SIZE = 25;
@@ -159,6 +173,7 @@ export class NextRunListPresenter {
cursor,
pageSize = DEFAULT_PAGE_SIZE,
includeHasAnyRuns = false,
+ columns,
}: RunListOptions
) {
//get the time values from the raw values (including a default period)
@@ -255,7 +270,12 @@ export class NextRunListPresenter {
return date > now ? now : date;
}
+ const runSelect = columns
+ ? deriveRunSelect(columns.visibleStandardIds, columns.smartSources)
+ : undefined;
+
const { runs, pagination } = await runsRepository.listRuns({
+ runSelect,
organizationId,
environmentId,
projectId,
@@ -335,6 +355,10 @@ export class NextRunListPresenter {
rootTaskRunId: run.rootTaskRunId,
metadata: run.metadata,
metadataType: run.metadataType,
+ payload: run.payload,
+ payloadType: run.payloadType,
+ output: run.output,
+ outputType: run.outputType,
machinePreset: run.machinePreset ? machinePresetFromRun(run)?.name : undefined,
queue: {
name: run.queue.replace("task/", ""),
diff --git a/apps/webapp/app/presenters/v3/mapRunToLiveFields.server.ts b/apps/webapp/app/presenters/v3/mapRunToLiveFields.server.ts
index ce7654cc1bc..25b3e5ad2b9 100644
--- a/apps/webapp/app/presenters/v3/mapRunToLiveFields.server.ts
+++ b/apps/webapp/app/presenters/v3/mapRunToLiveFields.server.ts
@@ -19,5 +19,11 @@ export function mapRunToLiveFields(run: ListedRun) {
usageDurationMs: Number(run.usageDurationMs),
costInCents: run.costInCents,
baseCostInCents: run.baseCostInCents,
+ metadata: run.metadata,
+ metadataType: run.metadataType,
+ payload: run.payload,
+ payloadType: run.payloadType,
+ output: run.output,
+ outputType: run.outputType,
};
}
diff --git a/apps/webapp/app/presenters/v3/runColumnsFromRequest.server.ts b/apps/webapp/app/presenters/v3/runColumnsFromRequest.server.ts
new file mode 100644
index 00000000000..1a2c7c8d3c0
--- /dev/null
+++ b/apps/webapp/app/presenters/v3/runColumnsFromRequest.server.ts
@@ -0,0 +1,34 @@
+import {
+ parseColumnParams,
+ resolveColumnLayout,
+ visibleSmartSources,
+ visibleStandardIds,
+ type RunColumnId,
+ type SmartColumnSource,
+} from "~/components/runs/v3/runColumns";
+
+/**
+ * Read the runs-list column state (`cols`/`sc`) off the request and resolve the
+ * column set the presenter needs to derive its Postgres select. Gates are
+ * resolved permissively here because they do not affect the always-selected
+ * fields; only the referenced smart-column sources change what is hydrated.
+ */
+export function getRunColumnsForSelect(request: Request): {
+ visibleStandardIds: RunColumnId[];
+ smartSources: SmartColumnSource[];
+} {
+ const url = new URL(request.url);
+ const layout = resolveColumnLayout(
+ parseColumnParams(
+ url.searchParams.get("cols"),
+ url.searchParams.getAll("sc"),
+ url.searchParams.get("hide")
+ ),
+ { isManagedCloud: true, isDevelopment: false }
+ );
+
+ return {
+ visibleStandardIds: visibleStandardIds(layout.visible),
+ smartSources: visibleSmartSources(layout.visible),
+ };
+}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx
index d2197776f69..e0354185363 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx
@@ -38,6 +38,8 @@ import {
type AgentDetail,
} from "~/presenters/v3/AgentDetailPresenter.server";
import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server";
+import { getRunColumnsForSelect } from "~/presenters/v3/runColumnsFromRequest.server";
+import { RunsDisplayOptions } from "~/components/runs/v3/RunsDisplayOptions";
import { SessionListPresenter } from "~/presenters/v3/SessionListPresenter.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { getResizableSnapshot } from "~/services/resizablePanel.server";
@@ -162,6 +164,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
to,
cursor,
direction,
+ columns: getRunColumnsForSelect(request),
})
.catch(() => null);
@@ -335,11 +338,14 @@ export default function Page() {
) : (
-
-
- {(list) => (list ? : null)}
-
-
+ <>
+
+
+
+ {(list) => (list ? : null)}
+
+
+ >
)}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
index 85b88a5144b..cbca618883d 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
@@ -74,6 +74,8 @@ import {
type ErrorGroupSummary,
} from "~/presenters/v3/ErrorGroupPresenter.server";
import { type NextRunList } from "~/presenters/v3/NextRunListPresenter.server";
+import { getRunColumnsForSelect } from "~/presenters/v3/runColumnsFromRequest.server";
+import { RunsDisplayOptions } from "~/components/runs/v3/RunsDisplayOptions";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { requireUser, requireUserId } from "~/services/session.server";
import { rbac } from "~/services/rbac.server";
@@ -270,6 +272,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
to,
cursor,
direction,
+ columns: getRunColumnsForSelect(request),
})
.catch((error) => {
if (error instanceof ServiceValidationError) {
@@ -536,6 +539,12 @@ function ErrorGroupDetail({
>
Bulk replay…
+
)}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx
index cf3bef30c5c..921e429700c 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx
@@ -39,6 +39,7 @@ import { StepNumber } from "~/components/primitives/StepNumber";
import { TextLink } from "~/components/primitives/TextLink";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { RunsFilters, type TaskRunListSearchFilters } from "~/components/runs/v3/RunFilters";
+import { RunsDisplayOptions } from "~/components/runs/v3/RunsDisplayOptions";
import { TaskRunsTable } from "~/components/runs/v3/TaskRunsTable";
import { BULK_ACTION_RUN_LIMIT } from "~/consts";
import { $replica } from "~/db.server";
@@ -52,6 +53,7 @@ import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server";
import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server";
+import { getRunColumnsForSelect } from "~/presenters/v3/runColumnsFromRequest.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import {
setRootOnlyFilterPreference,
@@ -123,6 +125,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
projectId: project.id,
...filters,
includeHasAnyRuns: true,
+ columns: getRunColumnsForSelect(request),
});
// Only persist rootOnly when no tasks are filtered. While a task filter is active,
@@ -345,7 +348,7 @@ function RunsList({
hasFilters={list.hasFilters}
rootOnlyDefault={rootOnlyDefault}
/>
-
+
{showNewRunsBanner && (
+
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts
index 90088952f25..aa0d3993677 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/useRunsLiveReload.ts
@@ -87,6 +87,12 @@ function patchVisibleRunsWithLiveUpdates(currentRuns: ListedRun[], liveRuns: Liv
usageDurationMs: update.usageDurationMs,
costInCents: update.costInCents,
baseCostInCents: update.baseCostInCents,
+ metadata: update.metadata !== undefined ? update.metadata : run.metadata,
+ metadataType: update.metadataType !== undefined ? update.metadataType : run.metadataType,
+ payload: update.payload !== undefined ? update.payload : run.payload,
+ payloadType: update.payloadType !== undefined ? update.payloadType : run.payloadType,
+ output: update.output !== undefined ? update.output : run.output,
+ outputType: update.outputType !== undefined ? update.outputType : run.outputType,
};
});
}
@@ -245,6 +251,13 @@ export function useRunsLiveReload({
searchParams.set("runIds", activeRunIdsParam);
}
+ const locationParams = new URLSearchParams(location.search);
+ const colsValue = locationParams.get("cols");
+ if (colsValue) searchParams.set("cols", colsValue);
+ const hideValue = locationParams.get("hide");
+ if (hideValue) searchParams.set("hide", hideValue);
+ for (const smart of locationParams.getAll("sc")) searchParams.append("sc", smart);
+
if (checkForNewRuns) {
appendNewRunsSearchParams(searchParams, {
locationSearch: location.search,
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx
index ec488237293..b38eee1a82f 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.scheduled.$taskParam/route.tsx
@@ -75,6 +75,8 @@ import { useZoomToTimeFilter } from "~/hooks/useZoomToTimeFilter";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server";
+import { getRunColumnsForSelect } from "~/presenters/v3/runColumnsFromRequest.server";
+import { RunsDisplayOptions } from "~/components/runs/v3/RunsDisplayOptions";
import { ScheduleListPresenter } from "~/presenters/v3/ScheduleListPresenter.server";
import {
TaskDetailPresenter,
@@ -220,6 +222,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
cursor,
direction,
includeHasAnyRuns: true,
+ columns: getRunColumnsForSelect(request),
})
.catch(() => null);
@@ -370,6 +373,7 @@ export default function Page() {
onClick={() => showNewRunsRef.current()}
/>
) : null}
+
{(list) => (list ? : null)}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx
index 32a16c6896d..16f8e1fa16f 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.tasks.standard.$taskParam/route.tsx
@@ -46,6 +46,8 @@ import { useSearchParams } from "~/hooks/useSearchParam";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server";
+import { getRunColumnsForSelect } from "~/presenters/v3/runColumnsFromRequest.server";
+import { RunsDisplayOptions } from "~/components/runs/v3/RunsDisplayOptions";
import {
TaskDetailPresenter,
type TaskActivity,
@@ -163,6 +165,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
cursor,
direction,
includeHasAnyRuns: true,
+ columns: getRunColumnsForSelect(request),
})
.catch(() => null);
@@ -266,6 +269,7 @@ export default function Page() {
onClick={() => showNewRunsRef.current()}
/>
) : null}
+
{(list) => (list ? : null)}
diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx
index 232ad8d7cce..8903e53ede9 100644
--- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx
+++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx
@@ -136,6 +136,7 @@ export default function Page() {
0
@@ -45,6 +48,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
projectId: project.id,
environmentId: environment.id,
runId: runIds,
+ runSelect: deriveRunSelect(
+ columns.visibleStandardIds,
+ columns.smartSources.filter((source) => source !== "payload")
+ ),
page: { size: 100 },
})
.then(({ runs: listedRuns }) => listedRuns.map(mapRunToLiveFields))
diff --git a/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts
new file mode 100644
index 00000000000..b1edcd0e22b
--- /dev/null
+++ b/apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.smart-column-sample.ts
@@ -0,0 +1,78 @@
+import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
+import { deriveRunSelect, type SmartColumnSource } from "~/components/runs/v3/runColumns";
+import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server";
+import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
+import { loadProjectEnvironmentFromRequest } from "~/services/loadProjectEnvironmentFromRequest.server";
+import { RunsRepository } from "~/services/runsRepository/runsRepository.server";
+import { $replica } from "~/db.server";
+import { isFinalRunStatus } from "~/v3/taskStatus";
+
+/** How many recent runs the smart-column preview can page through. */
+const SAMPLE_RUN_COUNT = 10;
+
+const SAMPLE_SOURCES: SmartColumnSource[] = ["payload", "metadata", "output"];
+
+function parseSampleSource(value: string | null): SmartColumnSource {
+ return SAMPLE_SOURCES.includes(value as SmartColumnSource)
+ ? (value as SmartColumnSource)
+ : "payload";
+}
+
+/**
+ * The most recent runs for the current filters, with their raw
+ * payload/metadata/output packets, feeding the "Add smart column" preview. The
+ * client picks which run to sample, parses, and resolves the JSON path; the
+ * server never parses (same rule as the list).
+ */
+export async function loader({ request, params }: LoaderFunctionArgs) {
+ const { project, environment } = await loadProjectEnvironmentFromRequest(request, params);
+ const filters = await getRunFiltersFromRequest(request);
+ const source = parseSampleSource(new URL(request.url).searchParams.get("source"));
+
+ const clickhouse = await clickhouseFactory.getClickhouseForOrganization(
+ project.organizationId,
+ "runsList"
+ );
+ const runsRepository = new RunsRepository({ clickhouse, prisma: $replica });
+
+ const { runs } = await runsRepository.listRuns({
+ organizationId: project.organizationId,
+ projectId: project.id,
+ environmentId: environment.id,
+ tasks: filters.tasks,
+ versions: filters.versions,
+ statuses: filters.statuses,
+ tags: filters.tags,
+ scheduleId: filters.scheduleId,
+ period: filters.period,
+ from: filters.from,
+ to: filters.to,
+ rootOnly: filters.rootOnly,
+ batchId: filters.batchId,
+ runId: filters.runId,
+ bulkId: filters.bulkId,
+ queues: filters.queues,
+ regions: filters.regions,
+ machines: filters.machines,
+ errorId: filters.errorId,
+ taskKinds: filters.sources,
+ runSelect: deriveRunSelect([], [source]),
+ page: { size: SAMPLE_RUN_COUNT },
+ });
+
+ return {
+ runs: runs.map((run) => ({
+ friendlyId: run.friendlyId,
+ status: run.status,
+ hasFinished: isFinalRunStatus(run.status),
+ startedAt: (run.startedAt ?? run.lockedAt)?.toISOString(),
+ createdAt: run.createdAt.toISOString(),
+ payload: run.payload,
+ payloadType: run.payloadType,
+ metadata: run.metadata,
+ metadataType: run.metadataType,
+ output: run.output,
+ outputType: run.outputType,
+ })),
+ };
+}
diff --git a/apps/webapp/app/routes/storybook.icons/route.tsx b/apps/webapp/app/routes/storybook.icons/route.tsx
index 36acd29b234..0bf0d531b80 100644
--- a/apps/webapp/app/routes/storybook.icons/route.tsx
+++ b/apps/webapp/app/routes/storybook.icons/route.tsx
@@ -39,6 +39,7 @@ import { ClockIcon } from "~/assets/icons/ClockIcon";
import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon";
import { AWS, DigitalOcean } from "~/assets/icons/CloudProviderIcon";
import { CodeSquareIcon } from "~/assets/icons/CodeSquareIcon";
+import { ColumnsIcon } from "~/assets/icons/ColumnsIcon";
import { ConcurrencyIcon } from "~/assets/icons/ConcurrencyIcon";
import {
CheckingConnectionIcon,
@@ -104,6 +105,7 @@ import { QuestionMarkIcon } from "~/assets/icons/QuestionMarkIcon";
import { QueuesIcon } from "~/assets/icons/QueuesIcon";
import { RadarPulseIcon } from "~/assets/icons/RadarPulseIcon";
import { FlagEurope, FlagUSA } from "~/assets/icons/RegionIcons";
+import { ResetIcon } from "~/assets/icons/ResetIcon";
import { RightSideMenuIcon } from "~/assets/icons/RightSideMenuIcon";
import { RolesIcon } from "~/assets/icons/RolesIcon";
import { RunFunctionIcon } from "~/assets/icons/RunFunctionIcon";
@@ -117,6 +119,7 @@ import { SideMenuRightClosedIcon } from "~/assets/icons/SideMenuRightClosed";
import { SlackIcon } from "~/assets/icons/SlackIcon";
import { SlackMonoIcon } from "~/assets/icons/SlackMonoIcon";
import { SlidersIcon } from "~/assets/icons/SlidersIcon";
+import { SmartColumnIcon } from "~/assets/icons/SmartColumnIcon";
import { SnakedArrowIcon } from "~/assets/icons/SnakedArrowIcon";
import { SparkleListIcon } from "~/assets/icons/SparkleListIcon";
import { StarIcon } from "~/assets/icons/StarIcon";
@@ -181,6 +184,7 @@ const icons: IconEntry[] = [
{ name: "ClockIcon", render: simple(ClockIcon) },
{ name: "ClockRotateLeftIcon", render: simple(ClockRotateLeftIcon) },
{ name: "CodeSquareIcon", render: simple(CodeSquareIcon) },
+ { name: "ColumnsIcon", render: simple(ColumnsIcon) },
{ name: "ConcurrencyIcon", render: simple(ConcurrencyIcon) },
{ name: "ConnectedIcon", render: simple(ConnectedIcon) },
{ name: "CubeSparkleIcon", render: simple(CubeSparkleIcon) },
@@ -248,6 +252,7 @@ const icons: IconEntry[] = [
{ name: "QuestionMarkIcon", render: simple(QuestionMarkIcon) },
{ name: "QueuesIcon", render: simple(QueuesIcon) },
{ name: "RadarPulseIcon", render: simple(RadarPulseIcon) },
+ { name: "ResetIcon", render: simple(ResetIcon) },
{ name: "RightSideMenuIcon", render: simple(RightSideMenuIcon) },
{ name: "RolesIcon", render: simple(RolesIcon) },
{ name: "RunFunctionIcon", render: simple(RunFunctionIcon) },
@@ -264,6 +269,7 @@ const icons: IconEntry[] = [
{ name: "SlackIcon", render: simple(SlackIcon) },
{ name: "SlackMonoIcon", render: simple(SlackMonoIcon) },
{ name: "SlidersIcon", render: simple(SlidersIcon) },
+ { name: "SmartColumnIcon", render: simple(SmartColumnIcon) },
{ name: "SnakedArrowIcon", render: simple(SnakedArrowIcon) },
{ name: "SparkleListIcon", render: simple(SparkleListIcon) },
{ name: "StarIcon", render: simple(StarIcon) },
diff --git a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts
index 808e30431ea..2e911e5e958 100644
--- a/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts
+++ b/apps/webapp/app/services/runsRepository/clickhouseRunsRepository.server.ts
@@ -3,6 +3,7 @@ import { ErrorId, RunId } from "@trigger.dev/core/v3/isomorphic";
import {
type FilterRunsOptions,
type IRunsRepository,
+ type ListedRun,
type ListRunsOptions,
type RunIdsPage,
type RunListInputOptions,
@@ -15,9 +16,48 @@ import { decodeRunsCursor, encodeRunsCursor } from "./runsCursor.server";
import { runStore } from "~/v3/runStore.server";
import { type PrismaClientOrTransaction } from "~/db.server";
-import { boundedIn } from "@trigger.dev/database";
+import { boundedIn, type Prisma } from "@trigger.dev/database";
type RunCursorRow = { runId: string; createdAt: number };
+/**
+ * Default hydrate select for the runs list, used when a caller does not derive
+ * one from the visible columns (bulk actions, the live poll). Kept in sync with
+ * the `ListedRun` payload type.
+ */
+const LIST_RUN_DEFAULT_SELECT = {
+ id: true,
+ friendlyId: true,
+ taskIdentifier: true,
+ taskVersion: true,
+ runtimeEnvironmentId: true,
+ status: true,
+ createdAt: true,
+ queueTimestamp: true,
+ scheduleId: true,
+ startedAt: true,
+ lockedAt: true,
+ delayUntil: true,
+ updatedAt: true,
+ completedAt: true,
+ isTest: true,
+ spanId: true,
+ idempotencyKey: true,
+ ttl: true,
+ expiredAt: true,
+ costInCents: true,
+ baseCostInCents: true,
+ usageDurationMs: true,
+ runTags: true,
+ depth: true,
+ rootTaskRunId: true,
+ batchId: true,
+ machinePreset: true,
+ queue: true,
+ workerQueue: true,
+ region: true,
+ annotations: true,
+} satisfies Prisma.TaskRunSelect;
+
/**
* Hydrates a set of rows for a ClickHouse-derived run-id set against the given
* read client. The closure MUST select `id` so `#hydrateRunsByIds` can key
@@ -264,52 +304,24 @@ export class ClickHouseRunsRepository implements IRunsRepository {
const store = this.options.runStore ?? runStore;
- let runs = await this.#hydrateRunsByIds(runIds, (client, ids) =>
- store.findRuns(
- {
- where: {
- id: {
- in: boundedIn(ids),
+ const select: Prisma.TaskRunSelect = options.runSelect
+ ? { ...options.runSelect, id: true }
+ : LIST_RUN_DEFAULT_SELECT;
+
+ let runs = await this.#hydrateRunsByIds(
+ runIds,
+ (client, ids) =>
+ store.findRuns(
+ {
+ where: {
+ id: {
+ in: boundedIn(ids),
+ },
},
+ select,
},
- select: {
- id: true,
- friendlyId: true,
- taskIdentifier: true,
- taskVersion: true,
- runtimeEnvironmentId: true,
- status: true,
- createdAt: true,
- queueTimestamp: true,
- scheduleId: true,
- startedAt: true,
- lockedAt: true,
- delayUntil: true,
- updatedAt: true,
- completedAt: true,
- isTest: true,
- spanId: true,
- idempotencyKey: true,
- ttl: true,
- expiredAt: true,
- costInCents: true,
- baseCostInCents: true,
- usageDurationMs: true,
- runTags: true,
- depth: true,
- rootTaskRunId: true,
- batchId: true,
- metadata: true,
- metadataType: true,
- machinePreset: true,
- queue: true,
- workerQueue: true,
- region: true,
- annotations: true,
- },
- },
- client
- )
+ client
+ ) as Promise
);
// ClickHouse is slightly delayed, so we're going to do in-memory status filtering too
diff --git a/apps/webapp/app/services/runsRepository/runsRepository.server.ts b/apps/webapp/app/services/runsRepository/runsRepository.server.ts
index 431ed271883..0b1049125dd 100644
--- a/apps/webapp/app/services/runsRepository/runsRepository.server.ts
+++ b/apps/webapp/app/services/runsRepository/runsRepository.server.ts
@@ -119,17 +119,37 @@ export type ListedRun = Prisma.TaskRunGetPayload<{
depth: true;
rootTaskRunId: true;
batchId: true;
- metadata: true;
- metadataType: true;
machinePreset: true;
queue: true;
workerQueue: true;
region: true;
annotations: true;
};
-}>;
+}> & {
+ /**
+ * Source blobs hydrated only when a smart column references them (see
+ * `runSelect`). Absent from the default list select; metadata is display-only
+ * on the list, payload/output can be large.
+ */
+ payload?: string;
+ payloadType?: string;
+ output?: string | null;
+ outputType?: string;
+ metadata?: string | null;
+ metadataType?: string;
+};
-export type ListRunsOptions = RunListInputOptions & Pagination;
+export type ListRunsOptions = RunListInputOptions &
+ Pagination & {
+ /**
+ * Overrides the default list `select`. The runs list derives this from the
+ * visible columns so only the fields a shown column needs are hydrated (in
+ * particular payload/output are omitted unless a smart column asks). Must
+ * include `id` for hydration keying; behaviour-critical fields are enforced
+ * by the caller's `deriveRunSelect`.
+ */
+ runSelect?: Prisma.TaskRunSelect;
+ };
export type TagListOptions = {
organizationId: string;
diff --git a/apps/webapp/vitest.config.ts b/apps/webapp/vitest.config.ts
index 1981ce26a06..dabe517bf4f 100644
--- a/apps/webapp/vitest.config.ts
+++ b/apps/webapp/vitest.config.ts
@@ -19,6 +19,7 @@ export default defineConfig({
"app/runEngine/services/**/*.test.ts",
"app/utils/**/*.test.ts",
"app/components/code/**/*.test.ts",
+ "app/components/runs/**/*.test.ts",
"app/components/dashboard-agent/**/*.test.ts",
"app/components/queues/**/*.test.ts",
"app/routes/storybook.agent-ui/*.test.ts",