feat(workbench): extend TruncatedCell to trace edges and selection rails (#750)

Follow-up to #749, which deliberately scoped TruncatedCell to the
non-virtualized columnar tables and left trace edges + single-column
selection rails as-is. Wire it into those too so every truncated data
cell in the workbench has the same full-content reveal (popover +
copy + modal-for-long).

Component:
- Add `align` prop ("start" | "end"). `end` hugs the right edge and
  right-justifies the display, for right-aligned columns like the trace
  `to_stage` cell. Defaults to "start" (unchanged).

Wiring:
- Trace propagation edges: from_stage / to_stage (to_stage align=end).
- Trace pipeline stage rail: stage label + summary.
- Trace turns rail: prompt + surface excerpts.
- Runs rail: session_id. Replay rail: prompt excerpt. Packs rail:
  pack_id. CORE-Logos pack rail: pack_id. Logos alignment rail: edge
  (rich source→target display, plain-text value for copy/reveal).
- Contemplation run rail: run_id; stage-evidence ids.
- Demos rail: title; scenario trace hash.
- Proposal detail: replay-equivalence hash, evidence hashes;
  proposal chain provenance.

Two row elements that were real <button>s (trace stage rail,
contemplation run row) are converted to <div role="button"> with an
Enter/Space onKeyDown handler — the codebase's established selectable-row
pattern (ProposalTable, RowShell) — so nesting TruncatedCell's button
trigger is valid HTML rather than a button-in-button.

Intentionally left as-is (documented):
- RunsRoute TurnRefRow is an <a>/<Link> row; nesting an interactive
  trigger inside an anchor is invalid HTML. The surface excerpt is
  reachable by following the link.
- Digests everywhere keep DigestBadge (already copy + full-value title).
- Section headings (e.g. the trace stage-detail <h3>) are not data cells.

Evidence: workbench-ui `pnpm build` (tsc -b) green; full vitest suite
535 passed / 60 files, incl. a new align assertion on TruncatedCell.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Shay 2026-06-14 17:11:47 -07:00 committed by GitHub
parent 99a52746ae
commit de2d5da4f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 178 additions and 50 deletions

View file

@ -8,6 +8,7 @@ import { Panel } from "../../design/components/Panel/Panel";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
import { LoadingState } from "../../design/components/states/LoadingState";
@ -57,10 +58,17 @@ function RunRow({
onSelect: () => void;
}) {
return (
<button
type="button"
<div
role="button"
tabIndex={0}
aria-current={selected ? "true" : undefined}
onClick={onSelect}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onSelect();
}
}}
className={`grid w-full grid-cols-[minmax(0,1fr)_auto] items-start gap-3 border-b border-[var(--color-border-subtle)] px-3 py-2 text-left transition-colors hover:bg-[var(--color-surface-inset)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-inset focus-visible:outline-[var(--color-focus-ring)] ${
selected
? "border-l-2 border-l-[var(--color-selected-border)] bg-[var(--color-selected-bg)] pl-[10px]"
@ -68,8 +76,13 @@ function RunRow({
}`}
>
<span className="min-w-0">
<span className="block truncate font-mono text-xs text-[var(--color-text-muted)]">
{run.run_id}
<span className="block min-w-0">
<TruncatedCell
value={run.run_id}
label="run id"
mono
className="text-xs text-[var(--color-text-muted)]"
/>
</span>
<span className="mt-1 block text-sm text-[var(--color-text-primary)]">
{run.prompt ?? "Prompt not recorded"}
@ -82,7 +95,7 @@ function RunRow({
<span className="justify-self-end text-xs text-[var(--color-text-secondary)]">
{boolLabel(run.learning_arc_closed)}
</span>
</button>
</div>
);
}
@ -108,8 +121,13 @@ function StageEvidence({ scene }: { scene: ContemplationScene }) {
{rows.map((row) => (
<div key={row.label} className="contents">
<dt className="text-[var(--color-text-secondary)]">{row.label}</dt>
<dd className="m-0 truncate font-mono text-[var(--color-text-primary)]" title={row.value}>
{row.value}
<dd className="m-0 min-w-0">
<TruncatedCell
value={row.value}
label={row.label}
mono
className="text-[var(--color-text-primary)]"
/>
</dd>
</div>
))}

View file

@ -8,6 +8,7 @@ import { DigestBadge } from "../../design/components/DigestBadge/DigestBadge";
import { Button } from "../../design/components/primitives/Button";
import { Panel } from "../../design/components/Panel/Panel";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
@ -63,9 +64,11 @@ function DemoRow({
: "border-l-2 border-l-transparent pl-[10px]"
}`}
>
<span className="truncate text-sm font-semibold text-[var(--color-text-primary)]">
{demo.title}
</span>
<TruncatedCell
value={demo.title}
label="demo title"
className="text-sm font-semibold text-[var(--color-text-primary)]"
/>
<span className="font-mono text-xs text-[var(--color-text-muted)]">
{demo.scenario_count} scenarios
</span>
@ -225,9 +228,12 @@ function ResultRow({ scenario }: { scenario: DemoScenarioRunResult }) {
</p>
) : null}
{scenario.trace_hash ? (
<code className="truncate font-mono text-xs text-[var(--color-text-muted)]">
{scenario.trace_hash}
</code>
<TruncatedCell
value={scenario.trace_hash}
label="trace hash"
mono
className="text-xs text-[var(--color-text-muted)]"
/>
) : null}
{scenario.problems.length > 0 ? (
<ul className="m-0 grid list-none gap-1 p-0">

View file

@ -1,6 +1,7 @@
import { useMemo } from "react";
import { DagViewer } from "../../design/components/Dag/Dag";
import type { DagEdgeInput, DagNodeInput } from "../../design/components/Dag/layout";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import type { LogosAlignmentRow } from "../../types/api";
// Read-only CORE-Logos alignment tab (LG-4). The trilingual resonance graph
@ -70,9 +71,16 @@ function EdgeCard({
}`}
>
<div className="flex items-center justify-between gap-2 font-mono text-xs text-[var(--color-text-primary)]">
<span className="truncate">
{row.source_id} <span aria-hidden className="text-[var(--color-text-muted)]"></span> {row.target_id}
</span>
<TruncatedCell
value={`${row.source_id}${row.target_id}`}
display={
<>
{row.source_id}{" "}
<span aria-hidden className="text-[var(--color-text-muted)]"></span> {row.target_id}
</>
}
label="alignment edge"
/>
<span className="text-[var(--color-text-secondary)]">{row.weight.toFixed(2)}</span>
</div>
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-[var(--color-text-secondary)]">

View file

@ -14,6 +14,7 @@ import { Panel } from "../../design/components/Panel/Panel";
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
import { LoadingState } from "../../design/components/states/LoadingState";
@ -165,8 +166,13 @@ function PackRow({
}`}
>
<span className="min-w-0">
<span className="block truncate font-mono text-sm text-[var(--color-text-primary)]">
{pack.pack_id}
<span className="block min-w-0">
<TruncatedCell
value={pack.pack_id}
label="pack id"
mono
className="text-sm text-[var(--color-text-primary)]"
/>
</span>
<span className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-secondary)]">
<span>{roleLabel(pack.role)}</span>

View file

@ -9,6 +9,7 @@ import { Panel } from "../../design/components/Panel/Panel";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
import { TreeView } from "../../design/components/TreeView/TreeView";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
@ -81,8 +82,13 @@ function PackRow({
}`}
>
<span className="min-w-0">
<span className="block truncate text-sm text-[var(--color-text-primary)]">
{pack.pack_id}
<span className="block min-w-0">
<TruncatedCell
value={pack.pack_id}
label="pack id"
mono
className="text-sm text-[var(--color-text-primary)]"
/>
</span>
<span className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--color-text-secondary)]">
<span>{sourceLabel(pack.source)}</span>

View file

@ -1,4 +1,5 @@
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { DagViewer, type DagEdgeInput, type DagNodeInput } from "../../design/components/Dag";
import type { ProposalDetail } from "../../types/api";
import { chainRecords, isRecord, jsonSource, stringField } from "./proposalView";
@ -68,9 +69,12 @@ export function ProposalChainViewer({ proposal }: { proposal: ProposalDetail })
{index + 1}. {nodeLabel(record, index)}
</span>
{provenance(record) ? (
<span className="truncate text-xs text-[var(--color-text-muted)]">
{provenance(record)}
</span>
<TruncatedCell
value={provenance(record) as string}
label="provenance"
align="end"
className="text-xs text-[var(--color-text-muted)]"
/>
) : null}
</div>
<StableJsonViewer source={jsonSource(record)} />

View file

@ -8,6 +8,7 @@ import type { ProposalSummary, MathProposalDetail, ProposalState, DownstreamEffe
import { Button } from "../../design/components/primitives/Button";
import { EmptyState } from "../../design/components/states/EmptyState";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { useListNavigation } from "../../design/hooks/useListNavigation";
import { ErrorState } from "../../design/components/states/ErrorState";
import { LoadingState } from "../../design/components/states/LoadingState";
@ -396,8 +397,13 @@ function MathProposalDetailView({
</div>
<div>
<dt className="text-[var(--color-text-muted)]">Replay Equivalence Hash</dt>
<dd className="m-0 text-[var(--color-text-primary)] font-mono truncate mt-0.5" title={proposal.replay_equivalence_hash}>
{proposal.replay_equivalence_hash}
<dd className="m-0 mt-0.5 min-w-0">
<TruncatedCell
value={proposal.replay_equivalence_hash}
label="replay equivalence hash"
mono
className="text-[var(--color-text-primary)]"
/>
</dd>
</div>
</dl>
@ -454,9 +460,14 @@ function MathProposalDetailView({
{hashes.map((hash: string, i: number) => (
<div key={i} className="flex items-center gap-2">
<span className="text-xs text-[var(--color-text-muted)] font-mono">#{i + 1}</span>
<code className="bg-[var(--color-surface-inset)] px-2 py-1 rounded font-mono text-xs text-[var(--color-text-primary)] truncate flex-1">
{hash}
</code>
<span className="min-w-0 flex-1 rounded bg-[var(--color-surface-inset)] px-2 py-1">
<TruncatedCell
value={hash}
label={`evidence hash #${i + 1}`}
mono
className="text-xs text-[var(--color-text-primary)]"
/>
</span>
</div>
))}
</div>

View file

@ -7,6 +7,7 @@ import { Panel } from "../../design/components/Panel/Panel";
import { SearchInput } from "../../design/components/SearchInput/SearchInput";
import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
import { EmptyState } from "../../design/components/states/EmptyState";
import { ErrorState } from "../../design/components/states/ErrorState";
@ -75,8 +76,13 @@ function TurnRow({
<span className="block text-xs text-[var(--color-text-secondary)]">
<Timestamp iso={turn.timestamp} format="relative" />
</span>
<span className="mt-1 block truncate text-sm text-[var(--color-text-primary)]">
{turn.prompt_excerpt || `Turn #${turn.turn_id}`}
<span className="mt-1 block min-w-0">
<TruncatedCell
value={turn.prompt_excerpt || `Turn #${turn.turn_id}`}
label="prompt"
wrap="pre-wrap"
className="text-sm text-[var(--color-text-primary)]"
/>
</span>
</span>
<span className="justify-self-end font-mono text-xs text-[var(--color-text-muted)]">

View file

@ -11,6 +11,7 @@ import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
import { Button } from "../../design/components/primitives/Button";
import { EmptyState } from "../../design/components/states/EmptyState";
@ -155,8 +156,13 @@ function RunRow({
<span className="block text-sm text-[var(--color-text-primary)]">
{sourceLabel(run.source)}
</span>
<span className="mt-1 block truncate font-mono text-xs text-[var(--color-text-muted)]">
{run.session_id}
<span className="mt-1 block min-w-0">
<TruncatedCell
value={run.session_id}
label="session id"
mono
className="text-xs text-[var(--color-text-muted)]"
/>
</span>
<span className="mt-1 flex items-center gap-2 text-xs text-[var(--color-text-secondary)]">
<span>{run.turn_count} turns</span>

View file

@ -19,6 +19,7 @@ import { SplitPane } from "../../design/components/SplitPane/SplitPane";
import { StableJsonViewer } from "../../design/components/StableJsonViewer";
import { TabBar, type Tab } from "../../design/components/TabBar/TabBar";
import { Timestamp } from "../../design/components/Timestamp/Timestamp";
import { TruncatedCell } from "../../design/components/TruncatedCell";
import { VirtualizedList } from "../../design/components/VirtualizedList/VirtualizedList";
import {
EpistemicStateBadge,
@ -212,7 +213,9 @@ function PipelineStageRail({
{stages.map((stage, index) => {
const selected = stage.stage_id === selectedStageId;
return (
<button
<div
role="button"
tabIndex={0}
aria-pressed={selected}
className={`grid min-h-16 grid-cols-[2rem_minmax(0,1fr)] gap-2 rounded border px-2 py-2 text-left focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--color-focus-ring)] ${
selected
@ -221,23 +224,36 @@ function PipelineStageRail({
}`}
key={stage.stage_id}
onClick={() => onSelect(stage.stage_id)}
type="button"
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
onSelect(stage.stage_id);
}
}}
>
<span className="font-mono text-xs text-[var(--color-text-muted)]">
{String(index + 1).padStart(2, "0")}
</span>
<span className="min-w-0">
<span className="flex items-center justify-between gap-2">
<span className="truncate font-mono text-xs font-semibold text-[var(--color-text-primary)]">
{stage.label}
</span>
<TruncatedCell
value={stage.label}
label="stage"
mono
className="text-xs font-semibold text-[var(--color-text-primary)]"
/>
<PipelineStageStatus status={stage.status} />
</span>
<span className="mt-1 block truncate text-xs text-[var(--color-text-secondary)]">
{stage.summary}
<span className="mt-1 block min-w-0">
<TruncatedCell
value={stage.summary}
label="summary"
wrap="pre-wrap"
className="text-xs text-[var(--color-text-secondary)]"
/>
</span>
</span>
</button>
</div>
);
})}
</section>
@ -259,11 +275,20 @@ function PipelineTransitionList({ record }: { record: CognitivePipelineRecord })
className="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-2 rounded border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] px-2 py-1 font-mono text-[11px]"
key={`${edge.from_stage}:${edge.to_stage}`}
>
<span className="truncate text-[var(--color-text-secondary)]">{edge.from_stage}</span>
<TruncatedCell
value={edge.from_stage}
label="from stage"
mono
className="text-[var(--color-text-secondary)]"
/>
<span className="text-[var(--color-text-muted)]">{edge.label ?? "propagate"}</span>
<span className="truncate text-right text-[var(--color-text-primary)]">
{edge.to_stage}
</span>
<TruncatedCell
value={edge.to_stage}
label="to stage"
mono
align="end"
className="text-[var(--color-text-primary)]"
/>
</li>
))}
</ol>
@ -563,11 +588,21 @@ function TraceRow({
<span className="block text-xs text-[var(--color-text-secondary)]">
<Timestamp iso={turn.timestamp} format="relative" />
</span>
<span className="mt-1 block truncate text-sm text-[var(--color-text-primary)]">
{firstLine(turn.prompt_excerpt) || `Turn #${turn.turn_id}`}
<span className="mt-1 block min-w-0">
<TruncatedCell
value={firstLine(turn.prompt_excerpt) || `Turn #${turn.turn_id}`}
label="prompt"
wrap="pre-wrap"
className="text-sm text-[var(--color-text-primary)]"
/>
</span>
<span className="mt-1 block truncate text-xs text-[var(--color-text-muted)]">
{turn.surface_excerpt}
<span className="mt-1 block min-w-0">
<TruncatedCell
value={turn.surface_excerpt}
label="surface"
wrap="pre-wrap"
className="text-xs text-[var(--color-text-muted)]"
/>
</span>
</span>
<span className="justify-self-end">

View file

@ -45,6 +45,12 @@ describe("TruncatedCell", () => {
expect(screen.getByRole("dialog", { name: "source" })).toBeInTheDocument();
});
it("right-justifies content when align is end", () => {
render(<TruncatedCell value="to_stage" label="to stage" align="end" />);
const display = screen.getByText("to_stage");
expect(display).toHaveClass("text-right");
});
it("does not bubble clicks to a surrounding row handler", async () => {
const onRowClick = vi.fn();
render(

View file

@ -44,6 +44,12 @@ export interface TruncatedCellProps {
wrap?: "break-all" | "pre-wrap";
/** Class applied to the inline display span. */
className?: string;
/**
* Horizontal alignment of the cell content. `end` hugs the right edge (for
* right-aligned table columns) and right-justifies the display text.
* Defaults to `start`.
*/
align?: "start" | "end";
}
function shouldOfferModal(value: string) {
@ -94,6 +100,7 @@ export function TruncatedCell({
mono,
wrap = "break-all",
className,
align = "start",
}: TruncatedCellProps) {
const [modalOpen, setModalOpen] = useState(false);
const offerModal = shouldOfferModal(value);
@ -101,8 +108,17 @@ export function TruncatedCell({
const stop = (event: { stopPropagation: () => void }) => event.stopPropagation();
return (
<span className={cn("group/cell flex min-w-0 items-center gap-1", mono && "font-mono")}>
<span className={cn("truncate", className)} title={value}>
<span
className={cn(
"group/cell flex min-w-0 items-center gap-1",
align === "end" && "justify-end",
mono && "font-mono",
)}
>
<span
className={cn("truncate", align === "end" && "text-right", className)}
title={value}
>
{display ?? value}
</span>
<Popover.Root>