Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/execution-history-content-fit-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@executor-js/plugin-execution-history": patch
---

Rebuild the runs list as a real semantic `<table>` with content-fit columns.
Each column now sizes to the maximum width its rows need (browser
`table-layout: auto`), instead of fixed pixel widths — the sticky `<thead>` and
the rows share one column model so they stay aligned for free. Short / numeric /
badge columns (status, duration, tools, interaction, log) are centered; text
columns (started, trigger, actor, code) are left-aligned. The two columns that
can run long — `actor` and `code` — are width-capped and truncated (hover shows
the full value). Removes the fixed-width `COL_*` slot constants and the
`min-w` horizontal-scroll floor.
33 changes: 21 additions & 12 deletions packages/plugins/execution-history/src/react/RunsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LiveButton } from "./live-button";
import { LiveDivider } from "./live-row";
import { RefreshButton } from "./refresh-button";
import { RunListRow } from "./run-row";
import { RunsShell } from "./shell";
import { RunsShell, RUNS_TABLE_COLSPAN } from "./shell";
import { RunsTimelineChart } from "./timeline-chart";
import {
emptyRunsFilters,
Expand Down Expand Up @@ -165,15 +165,16 @@ export function RunsPage() {

if (view.isLoadMoreError) {
rowItems.push(
<div
key="load-more-error"
className="flex w-full flex-col items-center justify-center gap-2 border-t border-border/50 py-4 text-center"
>
<p className="font-mono text-xs text-destructive">Failed to load more</p>
<Button type="button" variant="outline" size="sm" onClick={view.retry}>
Retry
</Button>
</div>,
<tr key="load-more-error">
<td colSpan={RUNS_TABLE_COLSPAN} className="border-t border-border/50 py-4 text-center">
<span className="flex flex-col items-center justify-center gap-2">
<span className="font-mono text-xs text-destructive">Failed to load more</span>
<Button type="button" variant="outline" size="sm" onClick={view.retry}>
Retry
</Button>
</span>
</td>
</tr>,
);
}

Expand Down Expand Up @@ -226,9 +227,17 @@ export function RunsPage() {
) : undefined;

const body = view.isLoading ? (
<div className="p-6 text-sm text-muted-foreground">Loading runs…</div>
<tr>
<td colSpan={RUNS_TABLE_COLSPAN} className="p-6 text-center text-sm text-muted-foreground">
Loading runs…
</td>
</tr>
) : view.isError ? (
<div className="p-6 text-sm text-destructive">Unable to load runs.</div>
<tr>
<td colSpan={RUNS_TABLE_COLSPAN} className="p-6 text-center text-sm text-destructive">
Unable to load runs.
</td>
</tr>
) : (
rowItems
);
Expand Down
133 changes: 73 additions & 60 deletions packages/plugins/execution-history/src/react/column-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,26 @@ import { cn } from "@executor-js/react/lib/utils";

import type { RunsSortField } from "./use-runs-list";
import type { RunColumns } from "./view";
import {
COL_ACTOR,
COL_DURATION,
COL_INTERACTION,
COL_LOG,
COL_TOOLS,
COL_TRIGGER,
} from "./run-row";

// ---------------------------------------------------------------------------
// RunsColumnHeader — sticky header row aligned to RunListRow's layout.
// Imports the shared column slot classes from run-row so that header and rows
// use the same widths and breakpoints.
// RunsColumnHeader — the <thead> row. Cells are <th> with no fixed widths; the
// table's content-fit layout sizes each column to its widest cell across the
// header and all rows. Alignment matches RunListRow (short/numeric center, text
// left). `bg-background` keeps rows from bleeding through the sticky header.
// ---------------------------------------------------------------------------

const HEAD =
"border-b border-border/60 bg-background px-3 py-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60 whitespace-nowrap";

interface SortButtonProps {
readonly label: string;
readonly field: RunsSortField;
readonly sortField: RunsSortField;
readonly sortDirection: "asc" | "desc";
readonly onSort: (field: RunsSortField) => void;
readonly className?: string;
}

function SortButton({
label,
field,
sortField,
sortDirection,
onSort,
className,
}: SortButtonProps) {
function SortButton({ label, field, sortField, sortDirection, onSort }: SortButtonProps) {
const isActive = sortField === field;
const Icon = isActive ? (sortDirection === "desc" ? ArrowDown : ArrowUp) : ArrowUpDown;

Expand All @@ -46,11 +34,10 @@ function SortButton({
size="sm"
onClick={() => onSort(field)}
className={cn(
"h-auto p-0 inline-flex items-center gap-1 text-left",
"inline-flex h-auto items-center gap-1 p-0 text-left",
"text-[10px] font-medium uppercase tracking-wider",
"text-muted-foreground/60 hover:text-foreground hover:bg-transparent",
"text-muted-foreground/60 hover:bg-transparent hover:text-foreground",
isActive && "text-foreground",
className,
)}
>
<span>{label}</span>
Expand All @@ -73,52 +60,78 @@ export function RunsColumnHeader({
columns,
}: RunsColumnHeaderProps) {
return (
<div className="flex min-w-0 items-center gap-2 overflow-hidden px-4 py-1.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground/60">
{/* dot spacer */}
<span aria-hidden className="size-2 shrink-0" />

{/* Started (sortable) */}
<SortButton
label="started"
field="startedAt"
sortField={sortField}
sortDirection={sortDirection}
onSort={onSort}
className="w-[180px] shrink-0"
/>

{/* Status */}
<span className="w-[120px] shrink-0">status</span>

{/* Trigger */}
{columns.trigger ? <span className={COL_TRIGGER}>trigger</span> : null}

{/* Actor */}
{columns.actor ? <span className={COL_ACTOR}>actor</span> : null}

{/* Duration (sortable) */}
{columns.duration ? (
<tr>
{/* dot column (no label) */}
<th aria-hidden scope="col" className={HEAD} />

{/* Started (sortable, left) */}
<th scope="col" className={cn(HEAD, "text-left")}>
<SortButton
label="duration"
field="durationMs"
label="started"
field="startedAt"
sortField={sortField}
sortDirection={sortDirection}
onSort={onSort}
className={COL_DURATION}
/>
</th>

{/* Status (centered) */}
<th scope="col" className={cn(HEAD, "text-center")}>
status
</th>

{/* Trigger (left) */}
{columns.trigger ? (
<th scope="col" className={cn(HEAD, "text-left")}>
trigger
</th>
) : null}

{/* Actor (left) */}
{columns.actor ? (
<th scope="col" className={cn(HEAD, "text-left")}>
actor
</th>
) : null}

{/* Tools */}
{columns.tools ? <span className={COL_TOOLS}>tools</span> : null}
{/* Duration (sortable, centered) */}
{columns.duration ? (
<th scope="col" className={cn(HEAD, "text-center")}>
<SortButton
label="duration"
field="durationMs"
sortField={sortField}
sortDirection={sortDirection}
onSort={onSort}
/>
</th>
) : null}

{/* Interaction */}
{columns.interaction ? <span className={COL_INTERACTION}>interaction</span> : null}
{/* Tools (centered) */}
{columns.tools ? (
<th scope="col" className={cn(HEAD, "text-center")}>
tools
</th>
) : null}

{/* Log */}
{columns.log ? <span className={COL_LOG}>log</span> : null}
{/* Interaction (centered) */}
{columns.interaction ? (
<th scope="col" className={cn(HEAD, "text-center")}>
interaction
</th>
) : null}

{/* Log (centered) */}
{columns.log ? (
<th scope="col" className={cn(HEAD, "text-center")}>
log
</th>
) : null}

{/* Code */}
<span className="min-w-0 flex-1 truncate">code</span>
</div>
{/* Code (left) */}
<th scope="col" className={cn(HEAD, "text-left")}>
code
</th>
</tr>
);
}
30 changes: 19 additions & 11 deletions packages/plugins/execution-history/src/react/live-row.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { RUNS_TABLE_COLSPAN } from "./shell";

// A full-width "Live" divider row between past and newly-arrived runs. Rendered
// as a <tr><td colSpan> so it is valid inside the runs <tbody>.
export function LiveDivider() {
return (
<div className="relative flex items-center py-1">
<div className="flex-1 border-t border-border" />
<span className="mx-3 flex items-center gap-1.5 text-[11px] text-muted-foreground select-none">
<span className="relative flex size-1.5 shrink-0">
<span className="absolute inline-flex size-full animate-pulse rounded-full bg-emerald-500 opacity-75" />
<span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
</span>
Live
</span>
<div className="flex-1 border-t border-border" />
</div>
<tr>
<td colSpan={RUNS_TABLE_COLSPAN} className="p-0">
<div className="relative flex items-center py-1">
<div className="flex-1 border-t border-border" />
<span className="mx-3 flex items-center gap-1.5 text-[11px] text-muted-foreground select-none">
<span className="relative flex size-1.5 shrink-0">
<span className="absolute inline-flex size-full animate-pulse rounded-full bg-emerald-500 opacity-75" />
<span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
</span>
Live
</span>
<div className="flex-1 border-t border-border" />
</div>
</td>
</tr>
);
}
Loading
Loading