Skip to content

feat(render): show durations, error messages, and LLM details in the span tree#61

Open
dark-sorceror wants to merge 6 commits into
mainfrom
feat/span-tree-details
Open

feat(render): show durations, error messages, and LLM details in the span tree#61
dark-sorceror wants to merge 6 commits into
mainfrom
feat/span-tree-details

Conversation

@dark-sorceror

@dark-sorceror dark-sorceror commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #54

The traces get span tree — the centerpiece of the human view — used to render exactly two things per span: name and an [ok]/[error] marker. No duration, no error text, no model/token/cost, even though all of that was already sitting in the payload the CLI had just fetched. A human asking "why was this trace slow" or "what actually failed" got zero signal from the tree and had to drop to --json | jq.

This PR surfaces that data directly in the tree: per-span durations (right-aligned), error status_message beneath failing spans, and a compact model/token/cost line under LLM spans. --json output is untouched — this is a human-view-only change.

How it works

Each span line now computes its own duration from span_start_time/span_end_time via a shared elapsedMs helper (lifted out of traces/get.ts into util/index.ts so the tree renderer can use it too, without duplicating the zone-less-UTC parsing logic). A still-running span (no span_end_time) falls back to elapsed-so-far against a now timestamp the caller passes in — traces get computes now once and threads it through both the header's live-duration display and renderTree, so the two never drift apart from separate Date.now() reads.

Error spans append their trimmed, width-truncated status_message as a line directly beneath the span, indented to the child prefix and colored the same red as the marker (or plain when color is off). LLM spans (detected by presence of model_name) get a dim model · tokens · cost line, e.g. claude-sonnet-4-6 · 7.4k tok · $0.0309 — tokens prefer total_tokens, falling back to input_tokens + output_tokens; tokens and cost are each omitted individually when unknown so a span with only a model name still renders that much.

The renderer stays a pure, dependency-free string transform: no Date.now() calls, no I/O — width and now are both passed in by the caller.

Before

parallel_orchestrator [ok]
├─ fetch_url [ok]
├─ async_sub_agent [error]
│  └─ failing_tool [error]
├─ async_sub_agent [ok]
│  ├─ fetch_url [ok]
│  └─ fetch_url [ok]
└─ fetch_url [ok]

After

parallel_orchestrator [ok]                                                  7.4s
├─ fetch_url [ok]                                                           1.7s
├─ async_sub_agent [error]                                                 708ms
│  ValueError: Intentional simulated API failure!
│  └─ failing_tool [error]                                                 507ms
│     ValueError: Intentional simulated API failure!
├─ async_sub_agent [ok]                                                     3.2s
│  ├─ fetch_url [ok]                                                        2.4s
│  └─ fetch_url [ok]                                                        2.8s
└─ fetch_url [ok]                                                           2.5s

What's included

Renderer (src/render/tree.ts)

  • SpanLike extended with span_end_time, status_message, model_name, input_tokens/output_tokens/total_tokens, cost
  • RenderTreeOptions.width (default 80) — durations right-aligned to this column, error messages truncated to it
  • RenderTreeOptions.now — ISO "now" instant for elapsed-so-far duration on still-running spans; omitted means no duration is shown rather than reading a fresh clock
  • Per-span duration line (end time, or elapsed-so-far against now), right-aligned with a minimum 1-space gap
  • Error status_message line beneath [error] spans, red when color is on, truncated via the existing (previously-dead) truncate() helper
  • Compact model · N tok · $cost detail line beneath LLM spans, dim when color is on

Command (src/commands/traces/get.ts)

  • Computes a single nowIso and threads it into both the header's live-duration calculation and renderTree's now option, so they can't disagree
  • Passes width: process.stdout.columns ?? 80 into renderTree

Shared utility (src/util/index.ts)

  • elapsedMs lifted out of traces/get.ts (unchanged behavior) so tree.ts can share it instead of reimplementing zone-less-UTC-safe duration math

Tests

  • tests/render/tree.test.ts — duration formatting/right-alignment (including the 80-column default and the minimum-1-space-gap edge case), elapsed-so-far via options.now, error status_message with/without a message and on non-error spans, message truncation, LLM detail (total_tokens preferred over input+output sum, partial data, dim styling, no ANSI when color is off)
  • tests/commands/traces-get.test.ts — end-to-end coverage that runGet surfaces duration, error message, and LLM detail lines in the rendered output
  • tests/util.test.tselapsedMs unit tests (zone-less UTC handling, null end, unparseable input, negative-duration guard)

Test plan

  • vitest — 617/617 passing, including new renderer tests for duration alignment, error message with/without status_message, missing end time, and NO_COLOR/non-TTY output
  • typecheck clean
  • lint clean
  • format clean
  • Live e2e against real traces: durations right-aligned to column 80 at every depth
  • Live e2e: error spans show ValueError: Intentional simulated API failure! beneath the failing span (see "After" tree above, from a real trace)
  • Live e2e: LLM spans show claude-sonnet-4-6 · 7.4k tok · $0.0309-style detail
  • Live e2e: --json output byte-identical to the base binary on both test traces
  • Live e2e: zero ANSI escapes in piped (non-TTY) output

Summary by cubic

Show per-span durations, error messages, and compact LLM details in the traces get span tree. Makes slow or failing spans easy to spot; --json is unchanged.

  • New Features

    • Right-align per-span durations to terminal width; uses elapsed-so-far for running spans.
    • Print error status_message under failing spans, colored when enabled; truncation keeps the line within the terminal width and drops the suffix when it can’t fit.
    • Add a dim LLM detail line: model · N tok · $cost with sensible fallbacks.
  • Refactors

    • Moved elapsedMs to src/util/index.ts and share it across the header and tree.
    • Renderer takes width and ISO now; no I/O or clock reads. traces get computes one now and passes it with terminal columns to renderTree to avoid drift.

Written for commit f5957cd. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 6 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/render/tree.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/render/tree.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

traces get span tree shows only name + status — no durations, no error messages, no model/token/cost

1 participant