Skip to content

feat(cli): full conversation preview in the trajectory browser - #382

Open
forrestjgq wants to merge 11 commits into
mainfrom
feat/trajectory_full_preview
Open

feat(cli): full conversation preview in the trajectory browser#382
forrestjgq wants to merge 11 commits into
mainfrom
feat/trajectory_full_preview

Conversation

@forrestjgq

Copy link
Copy Markdown
Contributor

Summary

Space on an attempt row in the raven trajectory browser previously
showed two truncated per-turn preview lines. This PR replaces it with an
interactive full-conversation viewer:

  • Data layer raven/trajectory/conversation.py: rebuilds an attempt's
    span snapshot into a causally ordered stream of labeled records (User
    input, LLM input/output, Tool input/output, skill, memory, and
    subagent events). Event-time semantics (inputs at span start, outputs
    at span end) plus nesting depth from parentSpanId keep the causal
    order. Full content comes from artifact files (paths restricted to
    the trace store, streamed 512 KiB cap per file); span previews are
    only a visible fallback. ERROR spans and expected-but-unreadable
    payloads always yield a placeholder record. LLM inputs omit only the
    item-by-item verified common message prefix against the previous call
    (omissions and rewrites are announced); tool_calls of unexecuted
    tools keep their full arguments.
  • Rendering: kind-colored labels aligned to one column, hanging-indent
    bodies with word-aware CJK-safe wrapping, degraded/error/meta note
    lines, single-pass turn separators (interleaved traces are never
    regrouped), and a stacked layout on narrow terminals that never drops
    characters.
  • Interactive viewer (prompt_toolkit full-screen, entered for every
    preview on a terminal): scrolling, a muted bottom bar with an
    'h for help' hint, filter/collapsed state and the scroll range (laid
    out by display width, never overflowing any terminal), a scrollable
    help page reachable in full on small screens, a global collapse (s:
    bodies over five display lines fold to five plus an ellipsis; error
    and degradation notes never fold), and a label filter (%: real input
    line, case-insensitive, empty clears; filtered-out turn groups lose
    their separators). Resizes re-wrap and re-judge folding live; the
    content caret is hidden (only the filter input shows one). q/Esc
    returns to the list; Ctrl+C cancels the whole browser from any viewer
    state. A rebuild failure falls back to the legacy per-turn preview;
    without a terminal the preview degrades to direct printing.
  • Every dynamic field passes the sanitization gates (control
    characters, rich markup, and ANSI never reach the terminal). Adds the
    Conversation Record term to CONTEXT.md.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • uv run pytest tests/test_trajectory_conversation.py -q -> 45 passed

  • uv run pytest tests/test_cli_preview_viewer.py -q -> 23 passed
    (assertions run against really rendered screens)

  • uv run pytest tests/test_cli_trajectory_browse.py -q -> 153 passed

  • uv run pytest -q (full) -> 7232 passed, 46 skipped; 11 failed +
    20 errors reproduce identically (same node ids) on the base branch in
    a clean worktree - environment baseline, not introduced here

  • uv run ruff check / ruff format --check on changed files -> clean

  • make check-large-files -> pass

  • Manual: rendered 6 real local attempts at widths 72/120/30 with zero
    over-width lines (largest attempt 7060 lines in 0.35s); drove the
    real TUI in a pty at normal and 44x12 sizes (viewer, help paging,
    collapse, %llm filtering, Ctrl+C cancel)

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Read/display side only; no persisted format changes. The untrusted
    input surface (artifact content, typed filter input) is contained by
    the store-prefix path check, the 512 KiB streamed cap, and per-field
    sanitization.

  • User-visible change: the Space preview is now a full-screen viewer;
    the legacy rendering remains as the automatic fallback when the
    rebuild fails, and non-terminal environments print directly.

  • Rollback: revert this PR, or restore the previous _preview_screen
    call site to get the old preview back.

  • Security impact considered

  • Backward compatibility considered

  • Rollback path is clear for risky changes

Related Issues

N/A

forrestjgq1982 and others added 11 commits September 9, 2026 02:19
Add raven/trajectory/conversation.py: attempt_conversation rebuilds an
attempt's span snapshot into a flat, causally ordered stream of labeled
ConversationRecord entries (User input, LLM input/output, Tool
input/output, skill/memory/subagent events). Inputs sort at span start,
outputs at span end; ties break by phase, then by nesting depth derived
from parentSpanId. Full content is read from artifact files (paths
restricted to the trace store, 512 KiB cap) with span previews as a
visible fallback; ERROR spans and unreadable expected payloads always
yield a record instead of disappearing. LLM inputs omit only the
verified common message prefix within one (traceId, parentSpanId) chain
and announce omissions and rewrites. Document the Conversation Record
term in CONTEXT.md.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Bound artifact reads to the 512 KiB cap plus one byte instead of reading
whole files first, and reject non-regular files before opening them.
Emit a Turn marker record for body-less session.turn roots so a turn's
identity and start time survive an empty body. Render thinking_blocks as
reasoning evidence when reasoning_content is absent, without duplicating
identical text. Always emit a separate malformed-record notice so a
readable payload cannot make a span with corrupt status or attributes
read as fully OK.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
A session.turn whose root carries only an output (old or damaged logs)
produced no input-phase record, so the turn's own start time was
unaddressable for rendering. Emit the Turn marker whenever the root
yields no User input record, not only when the body is fully empty.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
An ERROR is a completion event: attach it to the span's last
output-phase record instead of whatever was emitted last, so the Turn
start marker never carries an end-of-turn failure. A span without any
output-phase record gets a completion placeholder at endTime.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Space on an attempt row now renders the causally ordered record stream
from raven.trajectory.conversation instead of the two truncated per-turn
preview lines: kind-colored labels aligned to one column, hanging-indent
bodies wrapped by display width (CJK-aware), degraded/error/meta notes,
and turn separators inserted in a single pass over the ordered stream so
interleaved traces are never regrouped. Narrow terminals switch to a
stacked layout that never drops characters. Streams taller than the
screen page through less when an interactive pager is confirmed (both
stdio ends a tty, capable TERM, less on PATH); the subprocess is judged
by its exit code and only a clean pager run skips the press-any-key
waiter. Every dynamic field passes the sanitization gates before layout.
A rebuild failure falls back to the legacy per-turn preview.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Ctrl+C while paging now closes the pipe, terminates and reaps the pager
(killing it after a bounded wait), then raises the browser-wide cancel
instead of unwinding as a raw interrupt; the early-quit BrokenPipe path
also releases the pipe before the exit code decides. Turn separators are
pre-wrapped like every other line, so the produced line count is the
true screen usage that drives the pager threshold, and the minimum
layout width is an explicit 2-cell floor (a wide character cannot be
split) instead of a silent widening to 4.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Real-trace self-testing showed two readability gaps: bodies were
hard-wrapped mid-word (display-width only), and an LLM call repeated the
same model/token note on its input, thinking, and output records. Wrap
now breaks after the last space when one exists (hard wrap remains for
unbroken runs; the width contract is unchanged), and consecutive records
of one span sharing a meta show it once, on the last of them. Adds the
pager-threshold boundary test (exactly height minus two lines prints
directly, one more pages) and a three-trace interleave render snapshot.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Span ids are only unique within one trace, so a merged attempt can
interleave records whose span ids and meta match across traces; the
consecutive-meta suppression now also requires the same trace id, so
each trace keeps its own note.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
Replace the external less pager with a prompt_toolkit full-screen
viewer: the new bottom bar shows a muted 'h for help' hint plus filter
and collapse state, h opens an aligned key-reference page, s globally
folds any body longer than five display lines (judged after wrapping)
down to five plus a dim ellipsis while never hiding degraded/error/meta
notes, and % opens a real input line that filters records by label,
case-insensitively (empty input clears; groups left empty lose their
turn separators). The viewer passes its actual content width to the
line builder on every render, so a resize re-wraps and re-judges
folding, and Ctrl+C is the global cancel in all three states (viewing,
help, filter input), surfacing as the browser-wide cancel. Rich lines
convert to prompt_toolkit fragments with whole-line styles merged
explicitly. Without a terminal the preview degrades to direct printing
with the press-any-key waiter; the less subprocess management and its
tests are removed.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
The bottom bar now budgets by display width: the scroll range and the
'h for help' hint always survive, variable fields (title, filter,
collapsed) shrink first with the first non-fitting one clipped, the
hint compacts to 'h' before dropping, and a final clip keeps the bar
inside any terminal (CJK filters measured by cells, not len). The help
page became a scrollable line flow: scroll keys page it, any other key
returns to the preview at its kept position, long entries wrap at
narrow widths, and its own offset feeds the bar's range. Tests assert
against really rendered screens (renderer frames and a synchronous
layout render), covering collapse+filter on 40 columns, long and CJK
filters, and small-screen help reachability.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>
The focused content control painted its caret on the first character of
the view and the help page (covering the P of 'Preview keys'). The
control keeps focus for a valid layout but no longer shows a cursor;
the filter input line still does, where typing happens.

Co-authored-by: Claude (claude-fable-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

Reviewed the complete github/main...HEAD diff at 0c4efc30bb95, including AGENTS.md/CLAUDE.md and the Runtime glossary, the conversation rebuild and viewer implementations, their callers and trace/artifact producers, attempt grouping, commit history, backward compatibility/fallback behavior, test changes, and the TUI/Runtime architecture boundary. I found no weakened tests or concrete defect worth raising.

Verification:

  • uv run pytest tests/test_trajectory_conversation.py tests/test_cli_preview_viewer.py tests/test_cli_trajectory_browse.py -q on Python 3.12: 221 passed.
  • uv run ruff check ... and uv run ruff format --check ... on the six changed Python/test files: passed.
  • The underlying check-large-files command for github/main..HEAD: passed (make itself is unavailable in this environment).
  • Full uv run pytest -q on Python 3.13 reached 7,287 passed, 43 skipped, and one failure, then exited 139 during shutdown. The failure reproduces alone under Python 3.12 in tests/test_cli_theme.py::test_bold_accent_renders_styled_not_bare; that test, its implementation, and dependency files are unchanged by this PR. GitHub's Python 3.12 unit job is green.

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.

3 participants