Super-AI-Components

Run Inspector

Span detail: I/O, tokens, cost, errors

Error: Provider timed out after 30s

Retried by: Call LLM (retry) — Succeeded

What it is

The span detail an engineer opens from a trace: the raw input and output of one step of an agent run, its cost and cache status, and — when it failed — what was retried and whether the retry worked. It's what turns "the run failed" into a bug report someone can act on.

Why it matters

A pretty-printed payload that can't leave the page isn't a debugging tool, it's a screenshot. This is the surface every tracing product converges on for exactly that reason: LangSmith and Vercel's AI SDK observability both pair a readable I/O view with a raw copy path, because the point of opening a span is usually to paste its exact input into a bug report or a retry, not to admire the formatting.

Observed in: Tracing tools

Anatomy

  1. 1run-inspectorRoot wrapper around the tab set.
  2. 2run-inspector-tabsThe tab list — Input, Output, Metadata, Error.
  3. 3run-inspector-input-panelRaw request payload, pretty-printed and copyable.
  4. 4run-inspector-output-panelRaw response payload, pretty-printed and copyable.
  5. 5run-inspector-metadata-panelModel, latency, tokens, cost, and cache hit/miss.
  6. 6run-inspector-error-panelThe error, if any, plus what was retried and how it went.

How to use it

Reach for it wherever a host already has the richer per-run record a trace row's own TraceSpan doesn't carry — I/O, tokens, cost, cache hit/miss. Pass `input` (always) and `output` (once the run has one); both render pretty-printed with a Copy control that carries the exact JSON shown, never a summarized version. Put `cacheHit` next to `cost` in `metadata` rather than surfacing it anywhere else — cache state is usually the biggest lever on spend, and this component keeps it in the same glance as the number it affects. For the error tab, pass `retriedAttempt` when this run itself retried an earlier one, and `retriedBy` when a later run retried this one — both accept `{ id, name, status }`, the same shape N4 `trace-timeline` exports as `TraceSpanRetryOutcome`, so a host already driving N4's `renderDetail(span, retriedBy)` callback can hand `retriedBy` straight through.

Do

  • Always pair a pretty-printed input/output block with a copy control that carries the exact JSON, not a paraphrase.

    Input

    {
      "model": "gpt-4o-mini",
      "messages": [
        {
          "role": "user",
          "content": "Summarize the ticket."
        }
      ]
    }
  • Put cache hit/miss inside the same cost row, not a separate badge somewhere else on the panel.

    Model
    gpt-4o-mini
    Latency
    410ms
    Tokens in
    96
    Tokens out
    41
    Cost
    0.03 creditsCache hit
  • State the retry outcome in visible text on the error tab — what was retried, and whether it worked.

    Error: Provider timed out after 30s

    Retried by: Call LLM (retry) — Succeeded

Don't

  • Don't ship a pretty-printed payload with no copy affordance — uncopyable JSON can't go in a bug report, which is the entire point of this tab.

    Input

    {
      "model": "gpt-4o-mini",
      "messages": [
        {
          "role": "user",
          "content": "Summarize the ticket."
        }
      ]
    }

    Wrong: pretty-printed but no copy affordance. A reader has to hand-transcribe the payload into a bug report — RunInspector's own JSON panes always pair the block with a Copy control.

  • Don't put cache status on a row of its own, away from cost — it's the biggest lever on spend and belongs beside the number it moves.

    Cost0.03 credits
    Somewhere else entirelyCache hit

    Wrong: cache state on its own row, disconnected from cost. RunInspector renders the cache badge inside the cost stat itself — the lever and its biggest input stay in the same glance.

Accessibility

Keyboard
  • Three tab stops on the Input and Output tabs, two on Metadata and Error: the tab list is one stop whatever the tab count (roving tabindex), the open panel is a second because Base UI gives it `tabIndex=0`, and the Copy button is a third — present only when there is a payload to copy.
  • Arrow Left and Right move the highlight along the tab list and wrap at the ends. They do **not** switch the panel: this Base UI version defaults `activateOnFocus` to `false`, so arrowing highlights a tab and Enter or Space activates it. Home and End jump to the first and last tab.
  • The `pre` holding the JSON scrolls at `max-h-80` and is not focusable, and nothing inside it is either. A payload longer than the box cannot be scrolled from the keyboard — the panel's own tab stop sits outside the scroll container, so it scrolls nothing. Compare `settings-shell`, which puts `tabIndex={0}` on its equivalent `pre` for exactly this reason.
  • There is no Escape handler and no shortcut for copying. Copy is reachable only by Tab.
Screen reader
  • The tab list is a real `tablist` named "Run detail" by default (`tabsLabel`), with each panel wired to its tab by Base UI, so a panel announces its section name when entered.
  • The Error tab changes its own name when there is an error: the visible word "Error" goes `aria-hidden` and an `sr-only` "Error, this run failed" takes over, so the red dot is never the only carrier of that fact. With no error it is plainly "Error".
  • Metadata is a description list — `Model`, `Latency`, `Tokens in`, `Tokens out`, `Cost` — so each label and value announce as a pair. Cache state rides inside the Cost value rather than as its own term, which is the point of the layout but does mean it is announced as part of the cost row, not as a row of its own.
  • The Copy button's accessible name is pinned by `aria-label` to "Copy input JSON" / "Copy output JSON" and deliberately does not flip to "Copied" — a focused button's name change is not reliably re-announced. The trade is that for the 1.5 seconds the visible word reads "Copied", the visible label is no longer contained in the accessible name.
  • Nothing announces that a copy happened. There is no live region on this control, so the confirmation is sighted-only; if the clipboard write rejects, or `navigator.clipboard` is absent, the button simply never changes and nothing reports the failure.
  • Switching tabs is silent beyond the panel's own name. The payload itself is plain text in a `pre` with no role, so a large JSON body is read as an undifferentiated run of characters.
Focus
  • Activating a tab leaves focus on the tab, as it should — the panel swaps beneath it and is reached with the next Tab.
  • Nothing unmounts a focused control in normal use: the Copy button disappears only if `output` goes from defined to `undefined`, which is a backwards transition for a run.
  • The tabs and the Copy button ship visible `focus-visible` rings from the vendored primitives. The panel does not: `TabsContent` sets `outline-none` and adds no ring, so the second tab stop in this component is invisible when focused.

Watch out for

  • Copying a formatted summary instead of the raw payload. RunInspector's copy control always serializes the actual input/output value with JSON.stringify(value, null, 2) — never a display-only string a caller constructed separately, which could drift from what's on screen.
  • Inferring the error tab's retry outcome from error alone. A run can have retriedBy set with no error of its own (it succeeded, but only after a prior attempt failed) — pass the retry props explicitly rather than trying to derive them.
  • Wiring a vertical tab list through the vendored ui/tabs.tsx wrapper and expecting vertical arrow-key navigation or aria-orientation="vertical" — that wrapper re-emits orientation only as data-orientation, so a vertical rail built from it silently keeps horizontal keyboard behavior. RunInspector sidesteps this by staying on the wrapper's horizontal default; a vertical layout needs Base UI's Tabs composed directly.
  • Building retriedAttempt by re-deriving it from a full span list at every render instead of a single lookup by span.retryOf — the same one-line cost N4's own computeRetriedBy was written to avoid paying repeatedly for the forward direction.

Installation

npx shadcn@latest add https://super-ai-components.vercel.app/r/run-inspector.json

Requires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.