refactor(ui): theme-scale type and shape in trace views, aligned pane bars

Add local/no-arbitrary-design-value, scoped to TraceView and Lens, banning
arbitrary font size, tracking, leading, radius, border and CSS property
values. Map the Figma-export values onto the theme scale and replace hex
colors with info/destructive tokens.

Add PaneBar, a fixed-height bordered row, and build the step tree and span
detail headers from it so their borders line up across the split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yujong Lee 2026-10-03 18:22:14 -07:00
parent 07eb0dd10f
commit e7c3092571
26 changed files with 270 additions and 498 deletions

View file

@ -99,6 +99,11 @@ const eslintConfig = [
],
rules: { "local/no-ad-hoc-z-index": ["error", { allowPopupLayer: true }] },
},
{
files: ["src/components/view_logs/TraceView/**/*.tsx", "src/components/lens/**/*.tsx"],
ignores: ["src/**/*.test.tsx"],
rules: { "local/no-arbitrary-design-value": "error" },
},
{
files: ["tests/eslint-rules/**/*.{ts,tsx}"],
rules: { "local/no-noop-hover-variant": "off", "local/no-ad-hoc-z-index": "off" },

View file

@ -4,6 +4,7 @@ import noComplexJsxArrow from "./no-complex-jsx-arrow.mjs";
import filenamePascalCase from "./filename-pascal-case.mjs";
import noNoopHoverVariant from "./no-noop-hover-variant.mjs";
import noAdHocZIndex from "./no-ad-hoc-z-index.mjs";
import noArbitraryDesignValue from "./no-arbitrary-design-value.mjs";
const plugin = {
rules: {
@ -13,6 +14,7 @@ const plugin = {
"filename-pascal-case": filenamePascalCase,
"no-noop-hover-variant": noNoopHoverVariant,
"no-ad-hoc-z-index": noAdHocZIndex,
"no-arbitrary-design-value": noArbitraryDesignValue,
},
};

View file

@ -1,26 +1,7 @@
import { utilityOf } from "./tailwind-utility.mjs";
const AD_HOC_Z = /^-?z-(?:\d+|\[[^\]]*\]|\([^)]*\))$/;
const OPENERS = { "[": "]", "(": ")" };
const utilityOf = (token) => {
const closers = [];
const lastTopLevelColon = [...token].reduce((found, ch, i) => {
if (closers.length > 0 && ch === closers[closers.length - 1]) {
closers.pop();
return found;
}
if (ch in OPENERS) {
closers.push(OPENERS[ch]);
return found;
}
return ch === ":" && closers.length === 0 ? i : found;
}, -1);
return token
.slice(lastTopLevelColon + 1)
.replace(/^!/, "")
.replace(/!$/, "");
};
const classify = (token, allowPopupLayer) => {
const utility = utilityOf(token);
if (AD_HOC_Z.test(utility)) return "adHoc";

View file

@ -0,0 +1,42 @@
import { utilityOf } from "./tailwind-utility.mjs";
const ARBITRARY_SCALE = /^(?:text|tracking|leading|rounded(?:-[a-z]+)?|border(?:-[a-z]+)?)-\[/;
const ARBITRARY_PROPERTY = /^\[[a-z-]+:/;
const isOffending = (token) => {
const utility = utilityOf(token);
return ARBITRARY_SCALE.test(utility) || ARBITRARY_PROPERTY.test(utility);
};
const rule = {
meta: {
type: "problem",
docs: {
description:
"Disallow arbitrary font size, tracking, leading, radius and border values, and arbitrary CSS properties. Use the theme scale so surfaces share one type and shape system.",
},
schema: [],
messages: {
arbitrary:
"`{{token}}` bypasses the theme scale. Use a scale utility (text-xs/sm, leading-*, tracking-*, rounded-sm/md/lg, border/border-2) instead.",
},
},
create(context) {
const check = (node, value) => {
if (typeof value !== "string" || !value.includes("[")) return;
for (const token of value.split(/\s+/).filter(isOffending)) {
context.report({ node, messageId: "arbitrary", data: { token } });
}
};
return {
Literal(node) {
check(node, node.value);
},
TemplateElement(node) {
check(node, node.value.cooked);
},
};
},
};
export default rule;

View file

@ -0,0 +1,20 @@
const OPENERS = { "[": "]", "(": ")" };
export const utilityOf = (token) => {
const closers = [];
const lastTopLevelColon = [...token].reduce((found, ch, i) => {
if (closers.length > 0 && ch === closers[closers.length - 1]) {
closers.pop();
return found;
}
if (ch in OPENERS) {
closers.push(OPENERS[ch]);
return found;
}
return ch === ":" && closers.length === 0 ? i : found;
}, -1);
return token
.slice(lastTopLevelColon + 1)
.replace(/^!/, "")
.replace(/!$/, "");
};

View file

@ -26,16 +26,16 @@ import { scopeLabel } from "../model/format";
import { type Finding, type Lens } from "../model/types";
import { useListSearchRoute } from "../route";
const PRIORITY_COLOR = { high: "text-[#e5484d]", medium: "text-amber-500", low: "text-muted-foreground" } as const;
const PRIORITY_COLOR = { high: "text-destructive", medium: "text-amber-500", low: "text-muted-foreground" } as const;
const ROW =
"border-b border-border/60 transition-colors duration-150 hover:bg-trace-row-hover motion-reduce:transition-none";
const META = "truncate text-[11px] text-muted-foreground";
const META = "truncate text-xs text-muted-foreground";
function JobIcon({ lens }: { lens: Lens }) {
const status = lens.jobs[0]?.status;
const className = "size-4 shrink-0";
if (status === "queued" || status === "running")
return <CircleDashed aria-hidden="true" className={cn(className, "text-[#3b5bfd]")} />;
return <CircleDashed aria-hidden="true" className={cn(className, "text-info")} />;
if (status === "failed") return <CircleX aria-hidden="true" className={cn(className, "text-destructive")} />;
if (status === "cancelled")
return <CircleSlash aria-hidden="true" className={cn(className, "text-muted-foreground")} />;
@ -47,7 +47,7 @@ function TwoLine({ meta, title, className }: { meta: ReactNode; title: ReactNode
return (
<span className="flex min-w-0 flex-col gap-0.5">
<span className={META}>{meta}</span>
<span className={cn("truncate text-[13px] text-foreground", className)}>{title}</span>
<span className={cn("truncate text-sm text-foreground", className)}>{title}</span>
</span>
);
}
@ -87,7 +87,7 @@ export function InvestigationList({
<Input
aria-label="Search investigations"
placeholder="Search investigations"
className="h-7 bg-background pl-8 text-[12px]"
className="h-7 bg-background pl-8 text-xs"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
@ -149,7 +149,7 @@ export function InvestigationList({
</td>
<td
className={cn(
"w-[180px] truncate px-3 text-right text-[12px]",
"w-[180px] truncate px-3 text-right text-xs",
latest?.status === "failed" ? "text-destructive" : "text-muted-foreground",
)}
>
@ -159,14 +159,14 @@ export function InvestigationList({
{findings.length > 0 && (
<span
title={`${findings.length} open ${findings.length === 1 ? "finding" : "findings"}`}
className="inline-flex min-w-5 justify-center rounded-full bg-muted px-1.5 font-mono text-[11px] tabular-nums text-foreground"
className="inline-flex min-w-5 justify-center rounded-full bg-muted px-1.5 font-mono text-xs tabular-nums text-foreground"
>
{findings.length}
</span>
)}
</td>
<td
className="w-[120px] px-3 text-right text-[12px] text-muted-foreground"
className="w-[120px] px-3 text-right text-xs text-muted-foreground"
title={latest ? formatActivityTimestamp(latest.created_at) : undefined}
>
{latest ? agoLabel(Date.parse(latest.created_at), now) : "never run"}
@ -227,12 +227,12 @@ export function InvestigationList({
/>
</span>
</td>
<td className="px-3 text-right text-[12px] text-muted-foreground">
<td className="px-3 text-right text-xs text-muted-foreground">
{runs} {runs === 1 ? "run" : "runs"}
</td>
<td />
<td
className="px-3 text-right text-[12px] text-muted-foreground"
className="px-3 text-right text-xs text-muted-foreground"
title={formatActivityTimestamp(finding.last_seen)}
>
{agoLabel(Date.parse(finding.last_seen), now)}
@ -249,12 +249,10 @@ export function InvestigationList({
</tbody>
</table>
{!shown.length && (
<div className="py-16 text-center text-[12px] text-muted-foreground">
No investigations match your search.
</div>
<div className="py-16 text-center text-xs text-muted-foreground">No investigations match your search.</div>
)}
</div>
<footer className="flex h-8 shrink-0 items-center border-t border-border bg-muted/40 px-3 font-mono text-[11px] text-muted-foreground">
<footer className="flex h-8 shrink-0 items-center border-t border-border bg-muted/40 px-3 font-mono text-xs text-muted-foreground">
{shown.length} {shown.length === 1 ? "investigation" : "investigations"} ·{" "}
{lenses.filter((l) => l.settings.enabled).length} watching
</footer>

View file

@ -11,8 +11,8 @@ import type { Job } from "../model/types";
const STEP_MARK = { model: "✓", stage: "▸", error: "!" } as const;
const STEP_TONE = {
model: "text-foreground",
stage: "text-[#0011b3] dark:text-[#8b9bff]",
error: "text-[#e5484d]",
stage: "text-info",
error: "text-destructive",
} as const;
export function StepFeed({ job }: { job: Job }) {
@ -21,7 +21,7 @@ export function StepFeed({ job }: { job: Job }) {
const models = modelsUsed(steps);
const calls = steps.filter((s) => s.kind === "model").length;
return (
<section aria-label="Live investigation steps" className="rounded-md border bg-background font-mono text-[11px]">
<section aria-label="Live investigation steps" className="rounded-md border bg-background font-mono text-xs">
<button
type="button"
onClick={() => setOpen(!open)}

View file

@ -1,303 +0,0 @@
"use client";
import moment from "moment";
import { useMemo, useState } from "react";
import { HeaderActions } from "@/components/lens/HeaderActions";
import { Button } from "@/components/ui/button";
import { AgentTracesTable } from "./AgentTracesTable";
import { RunDrawer } from "./RunDrawer";
import { ALL_AGENTS, RunsToolbar, type RunStatusFilter } from "./RunsToolbar";
import {
type TraceRef,
traceKey,
traceRefOf,
useOpenTraceRouting,
useRunFilterRouting,
useZoomRouting,
} from "./traceRouting";
import type { TraceSummary } from "./traceTypes";
import { previewText, traceAgentNames } from "./traceUtils";
import { TimeRangeControls } from "./TimeRangeControls";
import { TracesTimeline, type TimeWindow } from "./TracesTimeline";
import { ActiveDot } from "./ActiveDot";
import { TracingSetupCard } from "./TracingSetupCard";
import { useTracesLive } from "./tracesApi";
import { type AgentTracesResult, traceWindowStartMs, useAgentTraces, useTraceAvailability } from "./useAgentTraces";
/** Client-side search (input text or trace id) plus agent / status filters over the loaded runs. */
export function filterRuns(
runs: TraceSummary[],
query: string,
agent: string,
status: RunStatusFilter,
): TraceSummary[] {
const q = query.trim().toLowerCase();
return runs.filter((run) => {
const haystack = [run.trace_id, previewText(run.input_preview), run.name].map((s) => s.toLowerCase());
const matchesQuery = !q || haystack.some((text) => text.includes(q));
const matchesAgent = agent === ALL_AGENTS || traceAgentNames(run).includes(agent);
const failed = run.error_count > 0;
const matchesStatus = status === "all" || (status === "error" ? failed : !failed);
return matchesQuery && matchesAgent && matchesStatus;
});
}
const filterByWindow = (runs: TraceSummary[], range: TimeWindow): TraceSummary[] =>
runs.filter((run) => {
const t = moment(run.start_time).valueOf();
return t >= range.startMs && t < range.endMs;
});
export interface TimeControls {
rangeHours: number;
onRangeHoursChange: (hours: number) => void;
onLiveChange: (live: boolean) => void;
}
interface AgentTracesSectionProps {
accessToken: string;
isActive: boolean;
startTime: string;
endTime: string;
isCustomDate: boolean;
isLiveTail: boolean;
/** Page-owned time range + live state; when given, the toolbar shows the range / Live control group. */
timeControls?: TimeControls;
readOnly?: boolean;
canMintTracingKey?: boolean;
}
function useTracingSetup(traces: AgentTracesResult, isActive: boolean, rangeChanged: boolean) {
const [setupResult, setSetupResult] = useState<{ detail: string | null } | null>(null);
const waitingForFirstTrace = traces.traces.length === 0 && !rangeChanged;
const settledResponse = isActive && !traces.isFetching && !traces.error;
const rememberSetup = waitingForFirstTrace || setupResult !== null;
if (settledResponse && rememberSetup && setupResult?.detail !== traces.notEnabledDetail) {
setSetupResult({ detail: traces.notEnabledDetail });
}
const disabledDetail = traces.notEnabledDetail ?? (traces.isFetching ? setupResult?.detail : null);
const loadingFirstPage = traces.isLoading && setupResult === null;
const isEmpty = !loadingFirstPage && !traces.error && traces.traces.length === 0;
return { disabledDetail, isEmpty, received: setupResult !== null && traces.traces.length > 0 };
}
function TraceHistoryError({ history }: { history: ReturnType<typeof useTraceAvailability> }) {
if (!history.error) return null;
return (
<div role="alert" className="flex items-center justify-between gap-4 border-b px-3 py-3 text-sm">
<p>Could not check earlier traces. {history.error.message}</p>
<Button variant="outline" size="sm" disabled={history.isFetching} onClick={() => void history.refetch()}>
Retry trace check
</Button>
</div>
);
}
/** The Runs view: filters, the runs table and footer — or one run, in place, once a row is clicked. */
export function AgentTracesSection({
accessToken,
isActive,
startTime,
endTime,
isCustomDate,
isLiveTail,
timeControls,
readOnly = false,
canMintTracingKey = false,
}: AgentTracesSectionProps) {
const live = useTracesLive();
const { trace: openTrace, openTrace: openRun, selection, fullScreen, setFullScreen } = useOpenTraceRouting();
const { query, agent, status, setQuery, setAgent, setStatus } = useRunFilterRouting();
const [showSetup, setShowSetup] = useState(false);
const [zoom, setZoom] = useZoomRouting();
const [rangeChanged, setRangeChanged] = useState(false);
const traceQuery = { accessToken, startTime, endTime, isCustomDate, isLiveTail, enabled: isActive };
const traces = useAgentTraces(traceQuery);
const setup = useTracingSetup(traces, isActive, rangeChanged);
const checkHistory = setup.isEmpty && !rangeChanged;
const history = useTraceAvailability(accessToken, isActive && checkHistory && setup.disabledDetail == null);
const checkTraces = () => {
traces.refetch();
if (setup.disabledDetail == null) void history.refetch();
};
const agents = useMemo(() => Array.from(new Set(traces.traces.flatMap(traceAgentNames))).sort(), [traces.traces]);
// Relative ranges end "now" (the list query uses Date.now() too); round to the minute so the histogram is stable.
const endMs = isCustomDate ? moment(endTime).valueOf() : moment().endOf("minute").valueOf();
const range = useMemo(
() => ({ startMs: traceWindowStartMs(startTime, endTime, isCustomDate, endMs), endMs }),
[startTime, endTime, isCustomDate, endMs],
);
const filtered = useMemo(
() => filterRuns(traces.traces, query, agent, status),
[traces.traces, query, agent, status],
);
const runs = useMemo(() => (zoom ? filterByWindow(filtered, zoom) : filtered), [filtered, zoom]);
const changeRange = (hours: number, apply: (hours: number) => void) => {
setZoom(null);
setRangeChanged(true);
apply(hours);
};
const openSentTrace = (trace: TraceSummary) => {
setShowSetup(false);
setRangeChanged(true);
checkTraces();
openRun(traceRefOf(trace));
};
const setupProps = {
accessToken,
readOnly,
canMintTracingKey,
onOpenTrace: openSentTrace,
onCheck: checkTraces,
checking: traces.isFetching,
};
if (setup.disabledDetail != null) return <TracingSetupCard detail={setup.disabledDetail} {...setupProps} />;
// Onboarding only on the first, default view; an empty range the user picked keeps its controls.
if (checkHistory && !history.error && history.data === false)
return <TracingSetupCard detail={null} {...setupProps} />;
if (showSetup) {
return (
<div>
<button
type="button"
onClick={() => setShowSetup(false)}
className="mb-3 text-[13px] text-muted-foreground hover:text-foreground"
>
← Back to traces
</button>
<TracingSetupCard detail={null} connected {...setupProps} />
</div>
);
}
const toggleRun = (trace: TraceSummary | null) => {
const ref: TraceRef | null = trace && traceRefOf(trace);
openRun(ref !== null && openTrace !== null && traceKey(ref) === traceKey(openTrace) ? null : ref);
};
return (
<div className="flex min-h-[560px] flex-1 flex-col overflow-hidden bg-card">
{checkHistory && <TraceHistoryError history={history} />}
<TracesReceived received={setup.received} />
<TracingSetupAction live={live} isActive={isActive} onClick={() => setShowSetup(true)} />
<RunDrawer
trace={openTrace}
runs={runs}
accessToken={accessToken}
selection={selection}
onSelect={openRun}
fullScreen={fullScreen}
onFullScreenChange={setFullScreen}
/>
<RunsToolbar
query={query}
agent={agent}
status={status}
agents={agents}
onQueryChange={setQuery}
onAgentChange={setAgent}
onStatusChange={setStatus}
>
{timeControls && (
<TimeRangeControls
range={zoom ?? range}
rangeHours={timeControls.rangeHours}
onRangeHoursChange={(hours) => changeRange(hours, timeControls.onRangeHoursChange)}
live={isLiveTail}
showLive={live}
onLiveChange={timeControls.onLiveChange}
onRefresh={() => {
setZoom(null);
checkTraces();
}}
refreshing={traces.isFetching}
/>
)}
</RunsToolbar>
<TracesTimeline runs={filtered} range={range} selection={zoom} onSelect={setZoom} />
<AgentTracesTable
traces={runs}
isLoading={traces.isLoading || (checkHistory && history.isLoading)}
error={traces.error}
hasMore={traces.hasMore}
isFetching={traces.isFetching}
onRetry={traces.hasMore ? traces.loadMore : traces.refetch}
onLoadMore={traces.loadMore}
onOpenTrace={toggleRun}
selectedKey={openTrace === null ? null : traceKey(openTrace)}
/>
<RunsFooter
count={runs.length}
zoom={zoom}
isFetching={traces.isFetching}
failed={!!traces.error}
onResetZoom={() => setZoom(null)}
/>
</div>
);
}
function TracingSetupAction({ live, isActive, onClick }: { live: boolean; isActive: boolean; onClick: () => void }) {
if (!live || !isActive) return null;
return (
<HeaderActions>
<Button variant="outline" size="sm" onClick={onClick} className="shrink-0 gap-1.5">
<ActiveDot />
Set up tracing
</Button>
</HeaderActions>
);
}
function TracesReceived({ received }: { received: boolean }) {
if (!received) return null;
return (
<p role="status" className="border-b px-3 py-3 text-sm text-emerald-700 dark:text-emerald-400">
Traces received. Select a run to inspect it.
</p>
);
}
function RunsFooter({
count,
zoom,
isFetching,
failed,
onResetZoom,
}: {
count: number;
zoom: TimeWindow | null;
isFetching: boolean;
failed: boolean;
onResetZoom: () => void;
}) {
const settled = failed ? "Update failed" : "Updated just now";
const status = isFetching ? "Updating…" : settled;
return (
<footer
data-testid="runs-footer"
className="flex h-8 shrink-0 items-center border-t border-border bg-muted/40 px-3 font-mono text-[11px] text-muted-foreground"
>
{count} {count === 1 ? "run" : "runs"}
{zoom && (
<button
type="button"
onClick={() => onResetZoom()}
aria-label="Clear time zoom"
className="ml-3 rounded border border-info/40 bg-info/10 px-1.5 text-info hover:bg-info/20"
>
{moment(zoom.startMs).format("MMM DD, HH:mm")} to {moment(zoom.endMs).format("MMM DD, HH:mm")} ×
</button>
)}
<span className="ml-auto">{status}</span>
</footer>
);
}

View file

@ -100,7 +100,7 @@ export function AgentTracesTable({
className="w-full min-w-[900px] table-fixed border-collapse text-left"
>
<thead className="sticky top-0 z-sticky bg-[color-mix(in_oklab,var(--muted)_40%,var(--card))]">
<tr className="h-8 border-b border-border text-[10px] tracking-[0.08em] text-muted-foreground uppercase">
<tr className="h-8 border-b border-border text-xs tracking-wider text-muted-foreground uppercase">
<th className={`w-[170px] ${TH}`}>
<span className="inline-flex items-center gap-1 whitespace-nowrap">
Time <ArrowDown className="size-2.5" />
@ -128,14 +128,14 @@ export function AgentTracesTable({
onClick={() => onOpenTrace(run)}
aria-selected={selectedKey === (run.trace_ref || run.trace_id)}
className={cn(
"h-9 cursor-pointer border-b border-border/60 text-[12px] transition-colors duration-150 motion-reduce:transition-none",
"h-9 cursor-pointer border-b border-border/60 text-xs transition-colors duration-150 motion-reduce:transition-none",
selectedKey === (run.trace_ref || run.trace_id)
? "bg-trace-row-selected shadow-[inset_2px_0_0_var(--trace-brand)]"
: "hover:bg-trace-row-hover",
)}
>
<td
className="px-3 font-mono text-[11px] whitespace-nowrap tabular-nums text-muted-foreground"
className="px-3 font-mono text-xs whitespace-nowrap tabular-nums text-muted-foreground"
title={formatActivityTimestamp(run.start_time)}
>
{formatRunTimestamp(run.start_time)}
@ -149,13 +149,13 @@ export function AgentTracesTable({
</span>
{run.resolution_limited && (
<span
className="shrink-0 text-[10px] text-muted-foreground"
className="shrink-0 text-xs text-muted-foreground"
title="This run is too large to calculate all totals in this view"
>
Partial totals
</span>
)}
<span className="hidden shrink-0 font-mono text-[10px] text-muted-foreground 2xl:inline">
<span className="hidden shrink-0 font-mono text-xs text-muted-foreground 2xl:inline">
{run.trace_id}
</span>
</div>
@ -170,7 +170,7 @@ export function AgentTracesTable({
{run.error_count > 0 ? (
<StatusMark status="error" count={run.error_count} />
) : (
<span className="font-mono text-[11px] text-muted-foreground/60">0</span>
<span className="font-mono text-xs text-muted-foreground/60">0</span>
)}
</td>
<td>
@ -182,9 +182,9 @@ export function AgentTracesTable({
PLACEHOLDER_ROWS.map((row) => <PlaceholderRow key={row} rowRef={row === 0 ? tailRef : undefined} />)}
</tbody>
</table>
{isLoading && <div className="py-16 text-center text-[12px] text-muted-foreground">Loading runs…</div>}
{isLoading && <div className="py-16 text-center text-xs text-muted-foreground">Loading runs…</div>}
{error && (
<div role="alert" className="flex items-center justify-center gap-3 py-6 text-[12px] text-muted-foreground">
<div role="alert" className="flex items-center justify-center gap-3 py-6 text-xs text-muted-foreground">
<span>
{traces.length ? "Could not load more runs" : "Could not load runs"}: {error.message}
</span>
@ -196,16 +196,14 @@ export function AgentTracesTable({
</div>
)}
{canContinue && traces.length === 0 && (
<div className="flex items-center justify-center gap-3 py-16 text-[12px] text-muted-foreground">
<div className="flex items-center justify-center gap-3 py-16 text-xs text-muted-foreground">
<span>No loaded runs match these filters.</span>
<Button size="xs" variant="outline" disabled={isFetching} onClick={onLoadMore}>
Load older runs
</Button>
</div>
)}
{isEmpty && (
<div className="py-16 text-center text-[12px] text-muted-foreground">No runs match these filters.</div>
)}
{isEmpty && <div className="py-16 text-center text-xs text-muted-foreground">No runs match these filters.</div>}
</div>
);
}

View file

@ -14,7 +14,7 @@ interface AttributesDetailProps {
export function DetailGroup({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section aria-label={title} className="flex flex-col gap-2">
<h3 className="py-1 text-[13px] font-medium tracking-[-0.26px] text-trace-text">{title}</h3>
<h3 className="py-1 text-sm font-medium text-trace-text">{title}</h3>
<Card className="px-3 py-2.5">{children}</Card>
</section>
);
@ -38,7 +38,7 @@ export function AttributesDetail({ traceId, span, attributes, isLoading }: Attri
<KeyValueRows entries={attributeEntries} />
</DetailGroup>
)}
{isLoading && <div className="text-[13px] text-trace-duration">Loading attributes…</div>}
{isLoading && <div className="text-sm text-trace-duration">Loading attributes…</div>}
</div>
);
}

View file

@ -16,9 +16,8 @@ import { errorSource, parseJson, parseMessages, prettyPayload } from "./traceUti
const ERROR_SOURCE_LABEL: Record<ErrorSource, string> = { tool: "Tool", model: "Model", litellm: "LiteLLM" };
const TRACEBACK_MARKER = "Traceback (most recent call last):";
const STATUS_TEXT = "px-5 py-2 text-[13px] tracking-[-0.26px] text-muted-foreground";
const PAYLOAD_PRE =
"font-mono text-[13px] leading-[1.5] tracking-[-0.26px] break-words whitespace-pre-wrap text-foreground";
const STATUS_TEXT = "px-3 py-2 text-sm text-muted-foreground";
const PAYLOAD_PRE = "font-mono text-sm leading-normal break-words whitespace-pre-wrap text-foreground";
/** Exporters record `repr(exc)` + traceback with no separator; keep the exception line. */
export const errorHeadline = (error: string): string =>
@ -46,9 +45,9 @@ export function ErrorBlock({ span }: { span: Span }) {
return (
<section
aria-label="Error"
className="mx-3 mb-3 rounded-[4px] border-[0.67px] border-destructive/40 bg-destructive/5 px-3 py-2.5"
className="mx-3 mb-3 rounded-sm border border-destructive/40 bg-destructive/5 px-3 py-2.5"
>
<div className="flex items-center gap-2 text-[13px] leading-[1.2] font-medium tracking-[-0.26px] text-destructive">
<div className="flex items-center gap-2 text-sm leading-tight font-medium text-destructive">
<AlertTriangle className="size-3.5" />
{ERROR_SOURCE_LABEL[source]} · {errorReason(headline)}
</div>
@ -210,7 +209,7 @@ export function DetailContent({ accessToken, traceId, traceRef, span }: DetailCo
: parseMessages(detail?.input ?? "")?.length;
return (
<div className="flex flex-col px-2 pt-1 pb-4">
<div className="flex flex-col pt-1 pb-4">
<ErrorBlock span={span} />
{span.error && (
<DiagnosticContent
@ -239,9 +238,7 @@ export function DetailContent({ accessToken, traceId, traceRef, span }: DetailCo
</Section>
) : null}
{empty && span.status !== "error" && (
<div className="py-12 text-center text-[13px] tracking-[-0.26px] text-muted-foreground">
No content recorded for this span.
</div>
<div className="py-12 text-center text-sm text-muted-foreground">No content recorded for this span.</div>
)}
</div>
);

View file

@ -9,6 +9,7 @@ import { AttributesDetail } from "./AttributesDetail";
import { CopyButton } from "./CopyButton";
import { DetailContent, errorHeadline, useSpanDetail } from "./DetailContent";
import { IdChip } from "./IdChip";
import { PaneBar } from "./PaneBar";
import { RequestDetail } from "./RequestDetail";
import { SpanIcon } from "./SpanIcon";
import { useTracesApi } from "./tracesApi";
@ -47,7 +48,7 @@ function PaneHeader({
onClose: () => void;
}) {
return (
<div className="flex h-7 shrink-0 items-center justify-between gap-3 px-5">
<PaneBar className="justify-between gap-3">
<div className="flex min-w-0 items-center">
<span className="mr-2 shrink-0">
<SpanIcon type={type} model={model} error={failed} size="md" />
@ -64,20 +65,16 @@ function PaneHeader({
size="icon-xs"
onClick={onClose}
aria-label="Close details"
className="size-6 shrink-0 rounded-[4px] text-muted-foreground"
className="size-6 shrink-0 rounded-sm text-muted-foreground"
>
<PanelRightClose className="size-4" />
</Button>
</div>
</PaneBar>
);
}
function PaneFooter({ children }: { children: React.ReactNode }) {
return (
<div className="flex min-h-10 shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-t bg-background px-5 py-2">
{children}
</div>
);
return <div className="flex h-10 shrink-0 items-center gap-3 border-t bg-background px-3">{children}</div>;
}
function Meta({ label, value }: { label: string; value: string }) {
@ -112,10 +109,7 @@ function SpanPane({
);
const tokens = span.input_tokens + span.output_tokens;
return (
<aside
className="flex h-full min-w-0 flex-col bg-background pt-4 text-[13px] text-foreground"
aria-label="Span details"
>
<aside className="flex h-full min-w-0 flex-col bg-background text-sm text-foreground" aria-label="Span details">
<PaneHeader
type={span.type}
model={span.model}
@ -125,15 +119,15 @@ function SpanPane({
onClose={onClose}
/>
<Tabs value={tab} onValueChange={(value) => onSpanTabChange(value as SpanTab)} className="min-h-0 flex-1 gap-0">
<div className="shrink-0 border-b px-5">
<TabsList variant="line" aria-label="Span detail sections" className="h-10 gap-4 px-0">
<PaneBar>
<TabsList variant="line" aria-label="Span detail sections" className="h-full gap-4 px-0">
{SPAN_TABS.map((id) => (
<TabsTrigger key={id} value={id} className="flex-none px-0 text-xs">
{TAB_LABELS[id]}
</TabsTrigger>
))}
</TabsList>
</div>
</PaneBar>
<TabsContent value="content" className="min-h-0 overflow-auto pt-3">
<DetailContent accessToken={accessToken} traceId={traceId} traceRef={trace.summary.trace_ref} span={span} />
</TabsContent>
@ -163,8 +157,8 @@ function SpanPane({
function GroupMetric({ label, value }: { label: string; value: string }) {
return (
<div className="border-b border-border/60 py-3">
<div className="text-[13px] font-medium text-muted-foreground">{label}</div>
<div className="mt-1 text-[13px] text-foreground tabular-nums">{value}</div>
<div className="text-sm font-medium text-muted-foreground">{label}</div>
<div className="mt-1 text-sm text-foreground tabular-nums">{value}</div>
</div>
);
}
@ -186,10 +180,7 @@ function GroupPane({
const sample = (firstFailure ?? row.members[0]).span_id;
const handoff = useTracesApi(accessToken).handoff(trace.summary.trace_id, sample, trace.summary.trace_ref);
return (
<aside
className="flex h-full min-w-0 flex-col bg-background pt-4 text-[13px] text-foreground"
aria-label="Group details"
>
<aside className="flex h-full min-w-0 flex-col bg-background text-sm text-foreground" aria-label="Group details">
<PaneHeader
type={row.type}
model={row.members[0]?.model ?? null}
@ -201,7 +192,7 @@ function GroupPane({
}
onClose={onClose}
/>
<div className="min-h-0 flex-1 overflow-auto px-5 py-3">
<div className="min-h-0 flex-1 overflow-auto px-3 py-3">
<div className="grid grid-cols-2 gap-x-5">
<GroupMetric label="Invocations" value={row.members.length.toLocaleString()} />
<GroupMetric label="Failed" value={row.failedCount.toLocaleString()} />
@ -211,9 +202,9 @@ function GroupPane({
<GroupMetric label="Type" value={row.type} />
</div>
{firstFailure?.error && (
<section className="mt-3 rounded-[4px] border border-destructive/30 px-3.5 py-3">
<div className="text-[12px] font-medium text-destructive">Failure pattern</div>
<p className="mt-2 font-mono text-[12px] leading-5 text-foreground">{errorHeadline(firstFailure.error)}</p>
<section className="mt-3 rounded-sm border border-destructive/30 px-3.5 py-3">
<div className="text-xs font-medium text-destructive">Failure pattern</div>
<p className="mt-2 font-mono text-xs leading-5 text-foreground">{errorHeadline(firstFailure.error)}</p>
</section>
)}
</div>
@ -228,7 +219,7 @@ function GroupPane({
export function DetailPane({ trace, row, accessToken, spanTab, onSpanTabChange, onClose }: DetailPaneProps) {
if (!row || row.kind === "load-more") {
return (
<div className="grid h-full place-items-center bg-background text-[13px] text-muted-foreground">
<div className="grid h-full place-items-center bg-background text-sm text-muted-foreground">
Select a span to inspect it.
</div>
);

View file

@ -35,7 +35,7 @@ function KeyValueRow({ entry, mono }: { entry: KeyValue; mono: boolean }) {
const [key, value] = entry;
const [open, setOpen] = useState(false);
const valueClass = cn(
"min-w-0 flex-1 break-words text-left text-[13px] leading-5 text-foreground",
"min-w-0 flex-1 break-words text-left text-sm leading-5 text-foreground",
mono || ID_KEY.test(key) ? "font-mono" : "font-sans",
);
@ -72,7 +72,7 @@ function KeyValueRow({ entry, mono }: { entry: KeyValue; mono: boolean }) {
<button {...toggle} className="flex shrink-0 cursor-pointer items-baseline gap-2.5 text-left">
{head}
</button>
<pre className={cn(valueClass, "leading-[1.5] break-words whitespace-pre-wrap")}>{value}</pre>
<pre className={cn(valueClass, "leading-normal break-words whitespace-pre-wrap")}>{value}</pre>
</>
) : (
<button {...toggle} className="flex min-w-0 flex-1 cursor-pointer items-baseline gap-2.5 text-left">

View file

@ -31,7 +31,7 @@ const markdownComponents: Components = {
<pre
className={cn(
BLOCK_GAP,
"max-w-full overflow-x-auto rounded-[4px] bg-muted/40 p-2.5 font-mono text-[13px] leading-[1.5] tracking-normal",
"max-w-full overflow-x-auto rounded-sm bg-muted/40 p-2.5 font-mono text-sm leading-normal tracking-normal",
)}
{...props}
/>
@ -55,7 +55,7 @@ export function Card({ children, className }: { children: React.ReactNode; class
return <section className={cn("rounded-md border border-border bg-background", className)}>{children}</section>;
}
const TILE = "grid size-4 shrink-0 place-items-center rounded-[2px] p-0.5";
const TILE = "grid size-4 shrink-0 place-items-center rounded-sm p-0.5";
function RoleTile({ failed = false }: { role: string; failed?: boolean }) {
return <Wrench className={cn("size-3.5 shrink-0", failed ? "text-destructive" : "text-muted-foreground")} />;
@ -83,8 +83,8 @@ const HEADER =
"group/header flex min-w-0 items-center gap-2 border border-border bg-background py-2 pr-2 pl-3 transition-colors hover:bg-muted/30";
const LABEL = "min-w-0 truncate text-xs leading-5 font-medium text-foreground";
const CARD_COPY =
"ml-auto size-5 shrink-0 rounded-[3px] text-muted-foreground opacity-60 transition-opacity duration-150 group-hover/header:opacity-100 focus-visible:opacity-100 motion-reduce:transition-none [&_svg]:size-4";
const INLINE_COPY = "size-4 shrink-0 rounded-[3px] text-muted-foreground";
"ml-auto size-5 shrink-0 rounded-sm text-muted-foreground opacity-60 transition-opacity duration-150 group-hover/header:opacity-100 focus-visible:opacity-100 motion-reduce:transition-none [&_svg]:size-4";
const INLINE_COPY = "size-4 shrink-0 rounded-sm text-muted-foreground";
export function ToolCallBlock({ call }: { call: TraceToolCall }) {
const argEntries = objectEntries(call.args);
@ -111,7 +111,7 @@ export function MessageCard({ message }: { message: TraceMessage; model: string
const expanded = open && hasBody;
return (
<article className="rounded-md">
<div className={cn(HEADER, "sticky top-10 z-sticky", expanded ? "rounded-t-[4px] border-b-0" : "rounded-[4px]")}>
<div className={cn(HEADER, "sticky top-10 z-sticky", expanded ? "rounded-t-sm border-b-0" : "rounded-sm")}>
<FoldTile label={label} open={open} onToggle={() => setOpen((v) => !v)} />
<span className={LABEL}>{label}</span>
<CopyButton value={copyValue} label={`Copy ${label}`} iconOnly className={CARD_COPY} />
@ -142,7 +142,7 @@ export function ToolResultCard({ name, result, failed = false }: { name: string;
const tone = failed ? "text-destructive" : "text-muted-foreground";
return (
<article className="flex flex-col">
<div className={cn(HEADER, open ? "rounded-t-[4px]" : "rounded-[4px]")}>
<div className={cn(HEADER, open ? "rounded-t-sm" : "rounded-sm")}>
<RoleTile role="tool" failed={failed} />
<span className="flex min-w-0 max-w-[50%] shrink-0 items-center gap-1.5">
<span className={cn(LABEL, failed && "text-destructive")}>{name}</span>
@ -157,17 +157,17 @@ export function ToolResultCard({ name, result, failed = false }: { name: string;
className={cn("flex min-w-0 cursor-pointer items-center gap-1 text-left", tone)}
>
<FoldChevron open={open} className="size-3 shrink-0" />
{!open && <span className="min-w-0 truncate text-[13px] leading-[1.2]">{result}</span>}
{!open && <span className="min-w-0 truncate text-sm leading-tight">{result}</span>}
</button>
) : (
<span className={cn("min-w-0 break-words text-[13px] leading-5", tone)}>{result || "No output"}</span>
<span className={cn("min-w-0 break-words text-sm leading-5", tone)}>{result || "No output"}</span>
)}
<CopyButton value={result} label={`Copy ${name} result`} iconOnly className={CARD_COPY} />
</div>
{open && (
<pre
className={cn(
"max-h-80 overflow-auto rounded-b-[4px] border-[0.67px] border-t-0 border-border bg-muted/40 px-3 py-2 font-mono text-[12px] leading-[1.5] break-words whitespace-pre-wrap",
"max-h-80 overflow-auto rounded-b-sm border border-t-0 border-border bg-muted/40 px-3 py-2 font-mono text-xs leading-normal break-words whitespace-pre-wrap",
tone,
)}
>
@ -197,10 +197,10 @@ export function Section({
type="button"
aria-expanded={open}
onClick={() => setOpen((prev) => !prev)}
className="sticky top-0 z-sticky-pinned mb-2 flex h-10 w-full cursor-pointer items-center gap-2.5 bg-background py-1 pr-3 pl-2 text-left transition-colors duration-150 hover:bg-muted/40 motion-reduce:transition-none"
className="sticky top-0 z-sticky-pinned mb-2 flex h-10 w-full cursor-pointer items-center gap-2.5 bg-background px-3 text-left transition-colors duration-150 hover:bg-muted/40 motion-reduce:transition-none"
>
<FoldChevron open={open} className="size-3 shrink-0 text-foreground" />
<span className="text-[13px] leading-[1.2] font-medium tracking-[-0.26px] text-foreground">{title}</span>{" "}
<span className="text-sm leading-tight font-medium text-foreground">{title}</span>{" "}
{count !== undefined && (
<span className="text-xs font-normal text-muted-foreground">
{count} {count === 1 ? "message" : "messages"}

View file

@ -0,0 +1,6 @@
import { cn } from "@/lib/cva.config";
/** Fixed-height pane row; every pane in the run view stacks these so borders line up across the split. */
export function PaneBar({ children, className }: { children: React.ReactNode; className?: string }) {
return <div className={cn("flex h-10 shrink-0 items-center gap-2 border-b px-3", className)}>{children}</div>;
}

View file

@ -30,7 +30,7 @@ export function RequestDetail({ span, accessToken, traceStartMs }: RequestDetail
const logNotFound = lookupDone && logQuery.data === null;
if (span.type !== "llm") {
return <div className="py-16 text-center text-[13px] text-trace-duration">This span is not a model request.</div>;
return <div className="py-16 text-center text-sm text-trace-duration">This span is not a model request.</div>;
}
const usage: KeyValue[] = [
@ -57,15 +57,15 @@ export function RequestDetail({ span, accessToken, traceStartMs }: RequestDetail
<Button
variant="outline"
size="xs"
className="self-start border-trace-border text-[13px] text-trace-text-2 shadow-trace-xs"
className="self-start border-trace-border text-sm text-trace-text-2 shadow-trace-xs"
onClick={() => setDrawerOpen(true)}
>
Open request log <ArrowUpRight className="size-3.5" />
</Button>
{logNotFound && <p className="text-[13px] text-trace-duration">No request log found for this call.</p>}
{logNotFound && <p className="text-sm text-trace-duration">No request log found for this call.</p>}
</div>
) : (
<p className="text-[13px] text-trace-duration">Not linked to a LiteLLM request</p>
<p className="text-sm text-trace-duration">Not linked to a LiteLLM request</p>
)}
</DetailGroup>
<LogDetailsDrawer

View file

@ -48,7 +48,7 @@ export function RunsToolbar({
onChange={(e) => onQueryChange(e.target.value)}
placeholder="Search input or trace ID"
aria-label="Search runs"
className="h-7 pl-8 text-[12px]"
className="h-7 pl-8 text-xs"
/>
</div>
<Select
@ -56,7 +56,7 @@ export function RunsToolbar({
value={agent}
onValueChange={(value: string | null) => value !== null && onAgentChange(value)}
>
<SelectTrigger size="sm" className="h-7 min-w-[130px] text-[12px]" aria-label="Filter by agent">
<SelectTrigger size="sm" className="h-7 min-w-[130px] text-xs" aria-label="Filter by agent">
<SelectValue />
</SelectTrigger>
<SelectContent>
@ -72,7 +72,7 @@ export function RunsToolbar({
value={status}
onValueChange={(value: RunStatusFilter | null) => value !== null && onStatusChange(value)}
>
<SelectTrigger size="sm" className="h-7 min-w-[110px] text-[12px]" aria-label="Filter by status">
<SelectTrigger size="sm" className="h-7 min-w-[110px] text-xs" aria-label="Filter by status">
<SelectValue />
</SelectTrigger>
<SelectContent>

View file

@ -125,7 +125,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
</span>
)}
</div>
<span className="text-[12px] text-trace-duration">
<span className="text-xs text-trace-duration">
{TYPE_LABEL[facts.type]}
{facts.model ? ` · ${facts.model}` : ""}
</span>
@ -150,7 +150,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
<FactSection title="Tags">
<ul className="flex flex-wrap gap-1">
{facts.tags.map((tag) => (
<li key={tag} className="rounded-[3px] bg-trace-tag px-1 py-0.5 text-trace-duration">
<li key={tag} className="rounded-sm bg-trace-tag px-1 py-0.5 text-trace-duration">
{tag}
</li>
))}
@ -158,7 +158,7 @@ function HoverCardBody({ facts, traceStartMs }: { facts: HoverFacts; traceStartM
</FactSection>
)}
{facts.error && (
<div className="rounded-[4px] bg-destructive/10 px-2 py-1.5 font-mono text-[12px] break-words text-destructive">
<div className="rounded-sm bg-destructive/10 px-2 py-1.5 font-mono text-xs break-words text-destructive">
{errorHeadline(facts.error)}
</div>
)}
@ -183,7 +183,7 @@ export function SpanHoverCard({ facts, traceStartMs, children }: SpanHoverCardPr
sideOffset={0}
alignOffset={0}
data-testid="span-hover-card"
className="max-h-[min(520px,calc(100vh-16px))] w-auto max-w-[420px] overflow-y-auto rounded-[6px] border border-trace-border bg-trace-surface p-3 text-[13px] leading-[1.2] tracking-[-0.26px] text-trace-text shadow-trace-md ring-0 transition-opacity duration-150 ease-[cubic-bezier(0,0,0.2,1)] data-closed:animate-none data-ending-style:opacity-0 data-open:animate-none motion-reduce:transition-none"
className="max-h-[min(520px,calc(100vh-16px))] w-auto max-w-[420px] overflow-y-auto rounded-md border border-trace-border bg-trace-surface p-3 text-sm leading-tight text-trace-text shadow-trace-md ring-0 transition-opacity duration-150 ease-[cubic-bezier(0,0,0.2,1)] data-closed:animate-none data-ending-style:opacity-0 data-open:animate-none motion-reduce:transition-none"
>
<HoverCardBody facts={facts} traceStartMs={traceStartMs} />
</HoverCardContent>

View file

@ -21,8 +21,8 @@ import { useSpanProvider } from "./spanProvider";
import type { SpanType } from "./traceTypes";
const SIZE = {
sm: "size-4 rounded-[2px]",
card: "size-[18px] rounded-[3px]",
sm: "size-4 rounded-sm",
card: "size-[18px] rounded-sm",
md: "size-5 rounded-full",
lg: "size-5 rounded-full",
} as const;

View file

@ -16,6 +16,7 @@ import {
import { cn } from "@/lib/cva.config";
import { FoldChevron } from "./Collapse";
import { PaneBar } from "./PaneBar";
import { groupFacts, SpanHoverCard, spanFacts } from "./SpanHoverCard";
import { SpanIcon } from "./SpanIcon";
import type { GroupRowData, SpanRowData, TreeRow } from "./traceTree";
@ -50,9 +51,9 @@ const rowClass = (selected: boolean): string =>
selected ? "border-l-primary bg-accent" : "border-l-transparent hover:bg-muted/60",
);
const LINE = "border-dashed border-border [border-width:0] [border-left-width:1px]";
const ELBOW = "border-dashed border-border [border-width:0] [border-left-width:1px] [border-bottom-width:1px]";
const NAME = "line-clamp-2 break-words text-[13px] leading-[1.2] font-medium";
const LINE = "border-l border-dashed border-border";
const ELBOW = "border-b border-l border-dashed border-border";
const NAME = "line-clamp-2 break-words text-sm leading-tight font-medium";
const META = "text-xs leading-4 text-muted-foreground tabular-nums";
/** One 20px column per ancestor: pass-through rails for open branches, an elbow into this row's tile. */
@ -99,7 +100,7 @@ function Caret({ open, onToggle, label }: { open: boolean; onToggle: () => void;
role="button"
tabIndex={-1}
aria-label={label}
className="grid size-6 shrink-0 place-items-center self-start rounded-[4px] p-0.5 text-muted-foreground transition-colors duration-150 hover:bg-muted motion-reduce:transition-none"
className="grid size-6 shrink-0 place-items-center self-start rounded-sm p-0.5 text-muted-foreground transition-colors duration-150 hover:bg-muted motion-reduce:transition-none"
onClick={(event) => {
event.stopPropagation();
onToggle();
@ -295,79 +296,78 @@ export function SpanTree({
return (
<section className="flex h-full min-h-0 min-w-0 flex-col border-r border-border bg-card" aria-label="Run spans">
<div className="shrink-0 space-y-3 border-b px-3 py-3">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium">
Steps{" "}
<span className="ml-1 text-xs font-normal text-muted-foreground">
{summary.span_count.toLocaleString()}
</span>
<PaneBar className="justify-between">
<span className="text-sm font-medium">
Steps{" "}
<span className="ml-1 text-xs font-normal text-muted-foreground">
{filtering && `${rows.length.toLocaleString()} of `}
{summary.span_count.toLocaleString()}
</span>
<div className="flex items-center gap-1">
{onOpenDetails && (
<Button variant="ghost" size="icon-xs" onClick={onOpenDetails} aria-label="Show details">
<PanelRightOpen className="size-4" />
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" aria-label="Step display options" />}>
<MoreHorizontal className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuCheckboxItem checked={hideFramework} onCheckedChange={onToggleHideFramework}>
Hide framework spans
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onCollapseAll}>Collapse branches</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</span>
<div className="flex items-center gap-1">
{onOpenDetails && (
<Button variant="ghost" size="icon-xs" onClick={onOpenDetails} aria-label="Show details">
<PanelRightOpen className="size-4" />
</Button>
)}
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" size="icon-xs" aria-label="Step display options" />}>
<MoreHorizontal className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuCheckboxItem checked={hideFramework} onCheckedChange={onToggleHideFramework}>
Hide framework spans
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onCollapseAll}>Collapse branches</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="relative">
<Search className="pointer-events-none absolute top-2.5 left-2.5 size-3.5 text-muted-foreground" />
</PaneBar>
<PaneBar>
<div className="relative min-w-0 flex-1">
<Search className="pointer-events-none absolute top-1/2 left-2 size-3.5 -translate-y-1/2 text-muted-foreground" />
<Input
aria-label="Search steps"
placeholder="Search steps"
value={query}
onChange={(event) => onQueryChange(event.target.value)}
className="h-8 pr-8 pl-8 text-xs shadow-none md:text-xs"
className="h-7 pr-7 pl-7 text-xs shadow-none md:text-xs"
/>
{query && (
<Button
variant="ghost"
size="icon-xs"
aria-label="Clear step search"
className="absolute top-1 right-1"
className="absolute top-1/2 right-0.5 -translate-y-1/2"
onClick={() => onQueryChange("")}
>
<X className="size-3" />
</Button>
)}
</div>
<div className="flex items-center gap-1">
<Button
variant={!errorsOnly ? "secondary" : "ghost"}
size="xs"
aria-pressed={!errorsOnly}
onClick={() => onErrorsOnlyChange(false)}
>
All steps
</Button>
<Button
variant={errorsOnly ? "secondary" : "ghost"}
size="xs"
aria-pressed={errorsOnly}
onClick={() => onErrorsOnlyChange(true)}
>
Errors <span className="ml-1 text-muted-foreground">{summary.error_count}</span>
</Button>
{filtering && (
<span className="ml-auto text-xs text-muted-foreground" role="status">
{rows.length} found
</span>
)}
</div>
</div>
<Button
variant={!errorsOnly ? "secondary" : "ghost"}
size="xs"
aria-pressed={!errorsOnly}
onClick={() => onErrorsOnlyChange(false)}
>
All
</Button>
<Button
variant={errorsOnly ? "secondary" : "ghost"}
size="xs"
aria-pressed={errorsOnly}
onClick={() => onErrorsOnlyChange(true)}
>
Errors <span className="ml-1 text-muted-foreground">{summary.error_count}</span>
</Button>
{filtering && (
<span role="status" className="sr-only">
{rows.length} found
</span>
)}
</PaneBar>
<div ref={scrollRef} className="min-h-0 flex-1 overflow-auto py-2">
{rows.length === 0 && (
<div className="px-4 py-8 text-center text-sm text-muted-foreground">
@ -385,7 +385,7 @@ export function SpanTree({
key={row.id}
type="button"
onClick={() => onLoadMore(row.groupId)}
className={cn(rowClass(false), "h-8 text-[13px] tracking-[-0.26px] text-muted-foreground")}
className={cn(rowClass(false), "h-8 text-sm text-muted-foreground")}
>
<Gutters depth={row.depth} guide={guides[i]} />
<span className="ml-0.5 flex items-center gap-2">
@ -413,7 +413,7 @@ export function SpanTree({
})}
</div>
</div>
<div className="flex min-h-8 shrink-0 flex-wrap items-center gap-x-3 gap-y-1 border-t border-border px-3 py-2 text-[10px] text-muted-foreground">
<div className="flex h-10 shrink-0 items-center gap-x-3 overflow-hidden border-t border-border px-3 text-xs text-muted-foreground">
{embedded ? (
<>
<span className="whitespace-nowrap">
@ -441,7 +441,7 @@ export function SpanTree({
function Kbd({ children }: { children: React.ReactNode }) {
return (
<kbd className="rounded-[3px] border border-border border-b-2 bg-muted px-[3px] font-mono text-muted-foreground">
<kbd className="rounded-sm border border-border border-b-2 bg-muted px-[3px] font-mono text-muted-foreground">
{children}
</kbd>
);

View file

@ -11,7 +11,7 @@ export function StatusMark({ status, count, subtle = false }: StatusMarkProps) {
if (status === "error") {
return (
<span
className="inline-flex items-center gap-1 text-[11px] text-destructive"
className="inline-flex items-center gap-1 text-xs text-destructive"
aria-label={`${count ?? 1} error${count === 1 ? "" : "s"}`}
>
<AlertCircle className="size-3" />

View file

@ -27,7 +27,7 @@ const RANGE_LABEL_FORMAT = "MMM D, h:mm A";
export const rangeLabel = (range: TimeWindow): string =>
`${moment(range.startMs).format(RANGE_LABEL_FORMAT)} to ${moment(range.endMs).format(RANGE_LABEL_FORMAT)}`;
const SEGMENT = "inline-flex h-7 items-center gap-1.5 px-2.5 text-[13px] outline-none focus-visible:bg-accent";
const SEGMENT = "inline-flex h-7 items-center gap-1.5 px-2.5 text-sm outline-none focus-visible:bg-accent";
interface TimeRangeControlsProps {
range: TimeWindow;
@ -79,7 +79,7 @@ export function TimeRangeControls({
onValueChange={(value: string) => onRangeHoursChange(Number(value))}
>
{RANGE_PRESETS.map((preset) => (
<DropdownMenuRadioItem key={preset.hours} value={String(preset.hours)} className="text-[13px]">
<DropdownMenuRadioItem key={preset.hours} value={String(preset.hours)} className="text-sm">
{preset.label}
</DropdownMenuRadioItem>
))}

View file

@ -89,7 +89,7 @@ function CopyForAgent({ handoff }: { handoff: TraceHandoff }) {
<Button
variant="outline"
size="xs"
className="h-7 shrink-0 gap-1.5 rounded-md text-[12px] shadow-none"
className="h-7 shrink-0 gap-1.5 rounded-md text-xs shadow-none"
onClick={async () => setCopied(await copyToClipboard(handoff.text, handoff.copied))}
>
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
@ -416,15 +416,15 @@ export function RunView({ traceId, traceRef, selection, accessToken, onBack, emb
}
if (!trace) {
return (
<div className="p-6 text-[12px]" data-testid="run-view-error">
<div className="p-6 text-xs" data-testid="run-view-error">
<button
type="button"
onClick={onBack}
className="mb-4 inline-flex items-center gap-1.5 text-[12px] text-muted-foreground hover:text-foreground"
className="mb-4 inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground"
>
<ArrowLeft className="size-3.5" /> Back to traces
</button>
<h1 className="mb-2 text-[13px] font-medium">Could not load trace</h1>
<h1 className="mb-2 text-sm font-medium">Could not load trace</h1>
<span className="text-muted-foreground">{traceQuery.error?.message ?? "Unknown error"}</span>
<Button variant="outline" size="sm" className="ml-3" onClick={() => void refreshTrace()}>
Retry

View file

@ -227,11 +227,11 @@ function FieldHeader({ runs, range }: { runs: readonly TraceSummary[]; range: Ti
const lastMs = latestStart(runs);
const live = isReceiving(lastMs, now);
return (
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 font-mono text-[11px] leading-none">
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-1 font-mono text-xs leading-none">
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-muted-foreground tabular-nums">
<span className="flex items-center gap-2" role="status" data-testid="traces-last-seen">
<LivePulse live={live} />
<span className={cn("font-semibold", live ? "text-[#0011b3] dark:text-[#8b9bff]" : "text-foreground")}>
<span className={cn("font-semibold", live ? "text-info" : "text-foreground")}>
{live ? "receiving" : "idle"}
</span>
{lastMs !== null && <span>last trace {agoLabel(lastMs, now)}</span>}
@ -241,7 +241,7 @@ function FieldHeader({ runs, range }: { runs: readonly TraceSummary[]; range: Ti
{runs.length === 1 ? "run" : "runs"} from{" "}
<span className="font-semibold text-foreground">{agents.length}</span>{" "}
{agents.length === 1 ? "agent" : "agents"}
{failed > 0 && <span className="text-[#e5484d]"> · {failed} failed</span>}
{failed > 0 && <span className="text-destructive"> · {failed} failed</span>}
</span>
</div>
<div className="flex flex-wrap items-center justify-end gap-x-3 gap-y-1 text-muted-foreground">
@ -277,7 +277,7 @@ function NowEdge() {
function BucketTooltip({ bucket, index }: { bucket: Bucket; index: number }) {
return (
<div
className="pointer-events-none absolute top-full z-floating mt-1 rounded-md border border-border bg-popover px-2.5 py-1.5 font-mono text-[11px] text-popover-foreground shadow-md"
className="pointer-events-none absolute top-full z-floating mt-1 rounded-md border border-border bg-popover px-2.5 py-1.5 font-mono text-xs text-popover-foreground shadow-md"
style={{ left: pct(Math.min(0.8, index / BUCKETS)) }}
role="tooltip"
>
@ -288,7 +288,7 @@ function BucketTooltip({ bucket, index }: { bucket: Bucket; index: number }) {
{bucket.runs} {bucket.runs === 1 ? "run" : "runs"}
{bucket.failed > 0 && `, ${bucket.failed} failed`}
</div>
<div className="text-[#0011b3] dark:text-[#8b9bff]">drag to zoom</div>
<div className="text-info">drag to zoom</div>
</div>
);
}
@ -351,8 +351,7 @@ function SelectionBracket({
const handle = "absolute inset-y-0 w-[2px] cursor-ew-resize bg-[#0011b3] dark:bg-[#8b9bff]";
const tick =
"before:absolute before:top-0 before:h-[2px] before:w-2 before:bg-inherit after:absolute after:bottom-0 after:h-[2px] after:w-2 after:bg-inherit";
const edgeLabel =
"pointer-events-none absolute -bottom-5 font-mono text-[10px] whitespace-nowrap text-[#0011b3] tabular-nums dark:text-[#8b9bff]";
const edgeLabel = "pointer-events-none absolute -bottom-5 font-mono text-xs whitespace-nowrap text-info tabular-nums";
return (
<>
<div
@ -372,7 +371,7 @@ function SelectionBracket({
onPointerDown={onHandleDown("resize-hi")}
/>
{widthPx >= MIN_DURATION_LABEL_PX && (
<span className="pointer-events-none absolute inset-x-0 top-2 text-center font-mono text-[11px] font-semibold text-[#0011b3] dark:text-[#8b9bff]">
<span className="pointer-events-none absolute inset-x-0 top-2 text-center font-mono text-xs font-semibold text-info">
{formatSpan(window.endMs - window.startMs)}
</span>
)}
@ -404,7 +403,7 @@ function TickAxis({ range }: { range: TimeWindow }) {
style={{ left: pct(t), transform: tickShift(t) }}
>
<span className="h-1 w-px bg-border" />
<span className="font-mono text-[10px] whitespace-nowrap text-muted-foreground tabular-nums">
<span className="font-mono text-xs whitespace-nowrap text-muted-foreground tabular-nums">
{moment(range.startMs + (range.endMs - range.startMs) * t).format(format)}
</span>
</div>

View file

@ -131,7 +131,7 @@ function CodeBlock({
}
function FileLabel({ children }: { children: React.ReactNode }) {
return <span className="text-[12.5px] text-foreground">{children}</span>;
return <span className="text-xs text-foreground">{children}</span>;
}
function LineTabs<T extends string>({
@ -155,7 +155,7 @@ function LineTabs<T extends string>({
aria-selected={value === option}
onClick={() => onChange(option)}
className={cn(
"-mb-px inline-flex h-9 items-center gap-1.5 border-b-2 text-[12.5px]",
"-mb-px inline-flex h-9 items-center gap-1.5 border-b-2 text-xs",
value === option
? "border-foreground text-foreground"
: "border-transparent text-muted-foreground hover:text-foreground",
@ -362,7 +362,7 @@ function EndpointValue({ value }: { value: string }) {
type="button"
onClick={() => void copy()}
aria-label={`Copy ${value}`}
className="group inline-flex min-w-0 items-center gap-2 text-left font-mono text-[12.5px] text-foreground"
className="group inline-flex min-w-0 items-center gap-2 text-left font-mono text-xs text-foreground"
>
<span className="break-all">{value}</span>
{copied ? (
@ -387,9 +387,9 @@ function Endpoints({ proxyUrl, children }: { proxyUrl: string; children?: React.
<dl className="mt-3 grid gap-x-6 gap-y-3 rounded-md border bg-muted/30 p-4 md:grid-cols-2">
{otlpEndpoints(proxyUrl).map(([label, value, copyable]) => (
<div key={label} className={cn("min-w-0", label === "Traces endpoint" && "md:col-span-2")}>
<dt className="text-[12px] text-muted-foreground">{label}</dt>
<dt className="text-xs text-muted-foreground">{label}</dt>
<dd className="mt-0.5">
{copyable ? <EndpointValue value={value} /> : <span className="text-[12.5px]">{value}</span>}
{copyable ? <EndpointValue value={value} /> : <span className="text-xs">{value}</span>}
</dd>
</div>
))}

View file

@ -0,0 +1,36 @@
import { RuleTester } from "eslint";
import rule from "../../scripts/eslint-rules/no-arbitrary-design-value.mjs";
const ruleTester = new RuleTester({
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
parserOptions: { ecmaFeatures: { jsx: true } },
},
});
const arbitrary = (code: string, token: string) => ({ code, errors: [{ messageId: "arbitrary", data: { token } }] });
ruleTester.run("no-arbitrary-design-value", rule as never, {
valid: [
'const c = "text-xs leading-tight tracking-wider rounded-sm border";',
'const c = "grid-cols-[minmax(0,1fr)_auto] w-[180px] left-[calc(50%-0.5px)]";',
'const c = "data-[side=right]:sm:max-w-[90vw] [&>p]:mb-0";',
'const c = "ease-[cubic-bezier(0.4,0,0.2,1)]";',
'const el = <div className="text-sm text-muted-foreground" />;',
],
invalid: [
arbitrary('const c = "flex text-[13px]";', "text-[13px]"),
arbitrary('const c = "text-[#e5484d]";', "text-[#e5484d]"),
arbitrary('const c = "tracking-[-0.26px]";', "tracking-[-0.26px]"),
arbitrary('const c = "leading-[1.2]";', "leading-[1.2]"),
arbitrary('const c = "rounded-[4px]";', "rounded-[4px]"),
arbitrary('const c = "rounded-t-[4px]";', "rounded-t-[4px]"),
arbitrary('const c = "border-[0.67px]";', "border-[0.67px]"),
arbitrary('const c = "[border-left-width:1px]";', "[border-left-width:1px]"),
arbitrary('const c = "dark:text-[#8b9bff]";', "dark:text-[#8b9bff]"),
arbitrary('const c = "data-[side=top]:text-[11px]";', "data-[side=top]:text-[11px]"),
arbitrary("const c = `px-2 ${x} text-[10px]`;", "text-[10px]"),
arbitrary('const el = <div className="text-[12px]" />;', "text-[12px]"),
],
});