feat(web): add "Created by" avatar column to runs list view

Visible by default to the right of Status; toggleable via the column
picker. Extracts the principal avatar/label helper out of the run
summary panel so both surfaces share one renderer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-05-26 00:25:53 -04:00
parent 7c69807f1d
commit 71c06c1bc4
No known key found for this signature in database
6 changed files with 100 additions and 74 deletions

View file

@ -1,14 +1,5 @@
import type { ReactNode } from "react";
import {
BoltIcon,
ChatBubbleLeftEllipsisIcon,
Cog6ToothIcon,
CpuChipIcon,
QuestionMarkCircleIcon,
ServerIcon,
} from "@heroicons/react/20/solid";
import type {
Principal,
Run,
SandboxResources,
SandboxState,
@ -20,6 +11,7 @@ import {
formatCpuCores,
formatUsdMicros,
} from "../lib/format";
import { principalDisplay } from "../lib/principal-display";
import { useRun, useRunArtifacts, useRunSandboxDetails } from "../lib/queries";
import { SANDBOX_STATE_DISPLAY } from "../lib/sandbox-state";
import { Tooltip } from "./ui";
@ -53,62 +45,6 @@ function Cell({ label, children }: { label: string; children: ReactNode }) {
);
}
interface CreatedByDisplay {
glyph: ReactNode;
label: string;
}
function principalGlyph(icon: ReactNode) {
return (
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 text-teal-500">
{icon}
</span>
);
}
function createdByDisplay(actor: Principal): CreatedByDisplay {
switch (actor.kind) {
case "user": {
let glyph: ReactNode;
if (actor.avatar_url) {
glyph = (
<img
alt=""
src={actor.avatar_url}
className="size-5 rounded-full outline -outline-offset-1 outline-line-strong"
/>
);
} else {
const initial = actor.login.charAt(0).toUpperCase() || "?";
glyph = (
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 font-mono text-[10px] font-medium text-teal-500">
{initial}
</span>
);
}
return { glyph, label: actor.login };
}
case "agent":
return { glyph: principalGlyph(<CpuChipIcon className="size-3" />), label: "agent" };
case "system":
return { glyph: principalGlyph(<Cog6ToothIcon className="size-3" />), label: "system" };
case "slack":
return {
glyph: principalGlyph(<ChatBubbleLeftEllipsisIcon className="size-3" />),
label: "slack",
};
case "webhook":
return { glyph: principalGlyph(<BoltIcon className="size-3" />), label: "webhook" };
case "worker":
return { glyph: principalGlyph(<ServerIcon className="size-3" />), label: "worker" };
case "anonymous":
return {
glyph: principalGlyph(<QuestionMarkCircleIcon className="size-3" />),
label: "anonymous",
};
}
}
export interface RunSummaryPanelViewProps {
run: Run | null;
runLoading: boolean;
@ -156,7 +92,7 @@ export function RunSummaryPanelView({
artifactsCount,
artifactsLoading,
}: RunSummaryPanelViewProps) {
const created = run?.created_by ? createdByDisplay(run.created_by) : null;
const created = run?.created_by ? principalDisplay(run.created_by) : null;
const diff = run?.diff ?? null;
const cost = formatUsdMicros(run?.billing?.total_usd_micros);

View file

@ -3,9 +3,11 @@ import { Link } from "react-router";
import { ciConfig, columnStatusDisplay, deriveCiStatus } from "../../data/runs";
import type { RunWithStatus } from "../../data/runs";
import { formatRelativeTime } from "../../lib/format";
import { principalDisplay } from "../../lib/principal-display";
import { InlineMarkdown } from "../inline-markdown";
import { PullRequestChip } from "../pull-request-chip";
import { SizeChip } from "../size-chip";
import { Tooltip } from "../ui";
import { RowActionsMenu } from "./row-actions-menu";
import { SelectionCheckbox } from "./selection-checkbox";
import type { ToggleableColumn } from "./toggleable-column";
@ -50,6 +52,18 @@ export function RunTableRow({
<span className={`font-mono text-xs ${statusDisplay.text}`}>{run.statusLabel}</span>
</span>
</td>
{show("created_by") && (
<td className="relative z-10 w-8 whitespace-nowrap px-3 py-2.5">
{run.createdBy && (() => {
const display = principalDisplay(run.createdBy);
return (
<Tooltip label={display.label}>
<span aria-label={`Created by ${display.label}`}>{display.glyph}</span>
</Tooltip>
);
})()}
</td>
)}
{show("repo") && (
<td className="whitespace-nowrap px-3 py-2.5 font-mono text-xs font-medium text-teal-500">
{run.repo}

View file

@ -152,6 +152,11 @@ export function RunsListView({
/>
</th>
<SortHeader label="Status" sortKey="status" activeSort={sort} direction={direction} onClick={onSortClick} />
{show("created_by") && (
<th scope="col" className="w-8 whitespace-nowrap px-3 py-2.5 font-medium">
By
</th>
)}
{show("repo") && (
<SortHeader label="Repo" sortKey="repo" activeSort={sort} direction={direction} onClick={onSortClick} />
)}

View file

@ -1,4 +1,5 @@
export const TOGGLEABLE_COLUMNS = [
"created_by",
"repo",
"workflow",
"created",
@ -14,14 +15,15 @@ const TOGGLEABLE_COLUMN_SET = new Set<string>(TOGGLEABLE_COLUMNS);
export type ToggleableColumn = (typeof TOGGLEABLE_COLUMNS)[number];
export const toggleableColumnLabels: Record<ToggleableColumn, string> = {
repo: "Repo",
workflow: "Workflow",
created: "Created",
updated: "Updated",
elapsed: "Elapsed",
size: "Size",
changes: "Changes",
pr: "PR",
repo: "Repo",
workflow: "Workflow",
created_by: "Created by",
created: "Created",
updated: "Updated",
elapsed: "Elapsed",
size: "Size",
changes: "Changes",
pr: "PR",
};
export function parseHiddenColumns(raw: string | null): Set<ToggleableColumn> {

View file

@ -1,6 +1,7 @@
import { formatDurationMs } from "../lib/format";
import {
BoardColumn,
type Principal,
type Run,
type RunSize,
type RunStatus as ApiRunStatus,
@ -39,6 +40,7 @@ export interface RunItem {
sandboxWorkingDirectory?: string;
sourceDirectory?: string;
createdAt?: string;
createdBy?: Principal | null;
lastEventAt?: string;
size?: RunSize;
}
@ -108,6 +110,7 @@ export function mapRunListItem(item: Run): RunItem {
sandboxWorkingDirectory: runtime?.working_directory ?? undefined,
sourceDirectory: item.source_directory ?? undefined,
createdAt: item.timestamps.created_at,
createdBy: item.created_by,
lastEventAt: item.timestamps.last_event_at ?? undefined,
additions: item.diff?.additions,
deletions: item.diff?.deletions,

View file

@ -0,0 +1,66 @@
import type { ReactNode } from "react";
import {
BoltIcon,
ChatBubbleLeftEllipsisIcon,
Cog6ToothIcon,
CpuChipIcon,
QuestionMarkCircleIcon,
ServerIcon,
} from "@heroicons/react/20/solid";
import type { Principal } from "@qltysh/fabro-api-client";
export interface PrincipalDisplay {
glyph: ReactNode;
label: string;
}
function principalIconGlyph(icon: ReactNode) {
return (
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 text-teal-500">
{icon}
</span>
);
}
export function principalDisplay(actor: Principal): PrincipalDisplay {
switch (actor.kind) {
case "user": {
let glyph: ReactNode;
if (actor.avatar_url) {
glyph = (
<img
alt=""
src={actor.avatar_url}
className="size-5 rounded-full outline -outline-offset-1 outline-line-strong"
/>
);
} else {
const initial = actor.login.charAt(0).toUpperCase() || "?";
glyph = (
<span className="grid size-5 place-items-center rounded-full bg-teal-500/20 font-mono text-[10px] font-medium text-teal-500">
{initial}
</span>
);
}
return { glyph, label: actor.login };
}
case "agent":
return { glyph: principalIconGlyph(<CpuChipIcon className="size-3" />), label: "agent" };
case "system":
return { glyph: principalIconGlyph(<Cog6ToothIcon className="size-3" />), label: "system" };
case "slack":
return {
glyph: principalIconGlyph(<ChatBubbleLeftEllipsisIcon className="size-3" />),
label: "slack",
};
case "webhook":
return { glyph: principalIconGlyph(<BoltIcon className="size-3" />), label: "webhook" };
case "worker":
return { glyph: principalIconGlyph(<ServerIcon className="size-3" />), label: "worker" };
case "anonymous":
return {
glyph: principalIconGlyph(<QuestionMarkCircleIcon className="size-3" />),
label: "anonymous",
};
}
}