Three views: a trace list, the trace detail waterfall with its correlated
transcript, and an aggregate query explorer. Vanilla JS in a go:embed'd
directory -- no bundler, no npm, and no way for the API and UI to drift.
Several fixes came directly from screenshotting the running app in Chrome:
- model.Span had no JSON tags, so Go marshalled SpanID while the client read
span_id. The inspector sat on "Loading..." forever. Tagged the model and
made Kind/Status marshal as names, so the client keeps no parallel enum.
- replaceChildren stringifies null rather than skipping it, rendering a
literal "null" in the trace header.
- Cost was empty for every trace: it is reported on the api_request LOG
record, never on a span, and the list scanned only spans.
- Every tool call rendered three times, because Claude Code emits tool,
tool.execution, and tool.blocked_on_user for one logical call. Wrapper
spans that restate their parent are now folded, with a count on the parent.
- The list showed "claude_code.interaction" on all 40 rows while the prompt
that distinguishes runs sat in a subtitle. The prompt now leads.
- A keyless log record was placed by timestamp containment, filing the user's
prompt under whichever model call started at the same microsecond. It now
prefers its own span_id, which for keyless records is the right signal.
- The Explore chart plotted the first numeric column, so a "slowest tools"
query rendered call counts. Results now carry the ORDER BY measure.
- An unnamed category (the wrapper spans) crushed every named bar; unnamed
rows are left to the table with a note.