Skip to content

feat(ui): finding → analysis trace entry points and the agent-trace viewer - #2082

Draft
GabrielDrapor wants to merge 24 commits into
feat/agent-trace-emit-prfrom
feat/agent-trace-ui-pr
Draft

feat(ui): finding → analysis trace entry points and the agent-trace viewer#2082
GabrielDrapor wants to merge 24 commits into
feat/agent-trace-emit-prfrom
feat/agent-trace-ui-pr

Conversation

@GabrielDrapor

Copy link
Copy Markdown
Collaborator

Makes the traces reachable. Four entry points:

  • Runs table — the Finding ID becomes clickable, styled like the Run ID and Trace ID beside it, opening the analysis trace in the same in-page viewer panel the page already uses. It stays plain text while the analysis is running, when tracing failed, and for findings analysed before tracing existed; the hover title says which.
  • Alert panel — a View analysis trace button swaps the viewer to the analysis trace; ← Back to trace returns.
  • Sidebar — every finished assistant reply gets a View trace link next to its token count, opening the turn's trace in a sheet over the current page. Links appear only when trace_status = available.
  • Viewer — an Agent badge in the header, and span names that read as what the agent did.

The invariant to review hardest: analysis traces never appear in the Traces list, and the viewer for customer traces is unchanged. The snapshot test of the user-trace viewer is taken in its own commit before the changes, so the diff proves it stayed byte-identical.

Closes #2064. Part of #2058.

Supersedes #2073, which GitHub auto-closed during a stack force-push and refuses to reopen. Review history lives on #2073.

GabrielDrapor added a commit that referenced this pull request Sep 1, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found and verified against the latest diff

Confidence score: 3/5

  • In frontend/ui/src/features/ai-assistant/components/message-list.tsx, streamed assistant messages do not receive the final traceId or traceStatus, so the completed turn lacks its View trace link; handle the final trace SSE event and annotate the current message.
  • In frontend/ui/src/features/ai-assistant/components/ai-assistant-panel.tsx, tool-step Open span clicks store spanId but pass only traceId to the sheet, so the viewer opens at the trace root instead of the requested span; thread spanId through AgentTraceSh….
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="frontend/ui/src/features/ai-assistant/components/message-list.tsx">

<violation number="1" location="frontend/ui/src/features/ai-assistant/components/message-list.tsx:487">
P2: After a streamed turn completes, the new `View trace` link is absent because the live message never receives `traceId` or `traceStatus`. Handle the final `trace` SSE event and annotate the current assistant message, or reload the session messages when the run finishes.</violation>
</file>

<file name="frontend/ui/src/features/ai-assistant/components/ai-assistant-panel.tsx">

<violation number="1" location="frontend/ui/src/features/ai-assistant/components/ai-assistant-panel.tsx:250">
P2: Clicking a tool step's `Open span` link loses the requested span because this callback stores `spanId` but the sheet receives only `traceId`, opening the viewer at the trace root. Thread `spanId` through `AgentTraceSheet` and initialize or select that span in the viewer.</violation>
</file>

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

<span title="Estimated cost">${msg.costUsd.toFixed(4)}</span>
</>
)}
{msg.traceId && msg.traceStatus === "available" && onOpenTrace && (

@cubic-dev-ai cubic-dev-ai Bot Sep 1, 2026

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.

P2: After a streamed turn completes, the new View trace link is absent because the live message never receives traceId or traceStatus. Handle the final trace SSE event and annotate the current assistant message, or reload the session messages when the run finishes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/ui/src/features/ai-assistant/components/message-list.tsx, line 487:

<comment>After a streamed turn completes, the new `View trace` link is absent because the live message never receives `traceId` or `traceStatus`. Handle the final `trace` SSE event and annotate the current assistant message, or reload the session messages when the run finishes.</comment>

<file context>
@@ -460,6 +484,19 @@ function UsageFooter({ msg }: { msg: AIMessage }) {
           <span title="Estimated cost">${msg.costUsd.toFixed(4)}</span>
         </>
       )}
+      {msg.traceId && msg.traceStatus === "available" && onOpenTrace && (
+        <>
+          <span>&middot;</span>
</file context>
Fix with cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 5ba2f54. The stream parser only read data: lines, so the agent's NAMED trace event (event: trace) was dropped; it now tracks the event name and annotates the live assistant message with traceId/traceStatus, so the View trace link appears without a reload. Covered by two new use-ai-stream tests (available and disabled outcomes).

<MessageList
messages={messages}
sessionStreaming={isStreaming}
onOpenTrace={(traceId, spanId) => setOpenTrace({ traceId, spanId })}

@cubic-dev-ai cubic-dev-ai Bot Sep 1, 2026

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.

P2: Clicking a tool step's Open span link loses the requested span because this callback stores spanId but the sheet receives only traceId, opening the viewer at the trace root. Thread spanId through AgentTraceSheet and initialize or select that span in the viewer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/ui/src/features/ai-assistant/components/ai-assistant-panel.tsx, line 250:

<comment>Clicking a tool step's `Open span` link loses the requested span because this callback stores `spanId` but the sheet receives only `traceId`, opening the viewer at the trace root. Thread `spanId` through `AgentTraceSheet` and initialize or select that span in the viewer.</comment>

<file context>
@@ -238,7 +244,11 @@ export function AiAssistantPanel({
+          <MessageList
+            messages={messages}
+            sessionStreaming={isStreaming}
+            onOpenTrace={(traceId, spanId) => setOpenTrace({ traceId, spanId })}
+          />
         )}
</file context>
Fix with cubic

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct within this PR's diff, and implemented by the stacked #2083: MessageList's onOpenTrace already captures spanId here, and #2083 threads it through AgentTraceSheet as initialSpanId with the deep-link selection logic (plus its own tests). Nothing further to change at this layer.

Comment thread frontend/ui/src/features/traces/components/TraceViewerPanel.tsx
Comment thread frontend/ui/src/features/detectors/components/detector-runs-table.tsx Outdated
Comment thread frontend/ui/src/features/detectors/components/detector-runs-table.tsx Outdated
GabrielDrapor added a commit that referenced this pull request Sep 1, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
@GabrielDrapor
GabrielDrapor force-pushed the feat/agent-trace-emit-pr branch from 4143fd4 to f141930 Compare September 1, 2026 09:52
GabrielDrapor added a commit that referenced this pull request Sep 2, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
@GabrielDrapor
GabrielDrapor force-pushed the feat/agent-trace-emit-pr branch from f141930 to f15c1f3 Compare September 2, 2026 02:10
GabrielDrapor added a commit that referenced this pull request Sep 2, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
GabrielDrapor added a commit that referenced this pull request Sep 2, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
GabrielDrapor added a commit that referenced this pull request Sep 2, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo

@cubic-dev-ai cubic-dev-ai Bot 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.

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

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread frontend/ui/src/features/ai-assistant/hooks/use-ai-stream.test.tsx
GabrielDrapor added a commit that referenced this pull request Sep 2, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
@GabrielDrapor
GabrielDrapor force-pushed the feat/agent-trace-emit-pr branch from f15c1f3 to c3185b4 Compare September 2, 2026 11:25
GabrielDrapor and others added 15 commits September 2, 2026 19:27
…e in-page viewer

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DopjNH21gLaDtWeeMDEHX
…es (user viewer snapshot unchanged)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DopjNH21gLaDtWeeMDEHX
…t-panel swap shows it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DopjNH21gLaDtWeeMDEHX
…trace sheet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DopjNH21gLaDtWeeMDEHX
The header id stayed on the user trace while the panel had swapped to the
RCA's agent trace (badge said Agent, id said otherwise). Use effectiveTraceId.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DopjNH21gLaDtWeeMDEHX
The three id columns each open something different — Run ID the run's
self-trace, Trace ID the scanned customer trace, Finding ID the RCA's analysis
trace — but the handler sat on the inner button, which is only as wide as the id
text. Clicking the padding beside a short id fell through to the row handler, so
aiming at a Finding ID and missing opened the scanned trace instead of the
analysis: the wrong one of three, with no way to tell it had happened.

Moves the handler onto the <td> so the whole cell routes to its own id, via one
IdCell that replaces the three near-identical cell bodies. The inner button stays
for keyboard users and its click bubbles to the same handler, so a cell fires
exactly once. Cells with nothing to open still fall through to the row.

Co-Authored-By: Claude <noreply@anthropic.com>
… bounds

Four review findings on the surfaces this PR adds.

**A tool step could open another turn's trace.** Resolving the step's trace by
scanning forward for the next assistant bubble runs past the turn boundary when
a tool-only run produces none, attaching the step to the *following* turn's
trace — a different trace, which does not contain that span. The search now
stops at the next user message, and the link is offered only when that turn's
trace is `available`, the same gate the turn's own "View trace" link uses. A new
test file covers all three cases.

**The embedded viewer ignored the sheet.** TraceViewerPanel is a fixed
70%-viewport overlay, so nesting it in the drawer painted over the page rather
than filling it; the AI Assistant button was also inert, since the sheet
hard-codes `aiPanelOpen: false` while `setAiPanelOpen` still reached the outer
panel. An explicit `embedded` prop drops the fixed positioning and hides that
control. The non-embedded class string is written out unchanged rather than
composed — the user-trace snapshot test compares this markup byte for byte, and
caught a reordering that was visually identical.

**A tooltip claimed a finding predated tracing.** Absent execution status means
different things depending on whether an analysis ran at all; `rca_status`
distinguishes them, so a never-analyzed finding now reads "not analyzed" and a
failed one says so, instead of both claiming they ran before tracing existed.

Co-Authored-By: Claude <noreply@anthropic.com>
… a metadata chip

The header 'View analysis trace' button read heavier than the action warrants;
the entry point now sits with the User/Session navigation chips (Langfuse-style)
as 'Analysis: RCA trace'. Only rendered when the finding's RCA trace is
available, so the chip row on ordinary user traces is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
… target them

A detector self-trace or an agent (RCA) trace can never carry findings (the
judge's read asserts source = 'user' server-side), so the tab only ever rendered
empty. Falls back to the tree view if the tab was active when the viewer swaps
to the agent trace.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
…-table tooltip states apart

- The stream parser only read data: lines, so the NAMED trace event the agent
  emits at run end was silently dropped and the live message never grew its
  View trace link until a reload.
- The finding tooltip now distinguishes failed from disabled trace status, and
  an absent rca_status (enrichment lookup failed) from an explicit null (no
  analysis ran).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
… swap resets selection

- 'Root cause analysis ›' replaces the vaguer 'Analysis: RCA trace' chip; the
  agent-trace view gains an 'Analyzed trace: <id> ›' return chip and the header
  drops the '← Back to trace' button.
- Selection/collapse reset is keyed on the EFFECTIVE trace id, so a customer
  span selected before the swap no longer renders (or fetches I/O) against the
  agent trace.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
The trace event is a separately enqueued chunk; asserting after the text delta
alone passed whether or not the disabled event was consumed.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
GabrielDrapor and others added 9 commits September 2, 2026 19:33
The runs proxy read `detector_rcas.latest_execution_id`, which #2070 dropped in
favour of the append-only executions table. Read the finding's executions
newest-attempt first and apply the same rule as the RCA route: the status is
the current attempt's, the trace link is the newest attempt whose export
landed (so a pending retry does not hide a working trace), falling back to
the current attempt's pending/failed/disabled status.

`TraceStatus` is now exported from @traceroot/core (#2072); the ai-assistant
types import it from there instead of reaching into the detectors hook module.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
"Open in new tab" while viewing an RCA agent trace (the Alert chip swap, or
the sidebar sheet) sends ?traceId=<agent id>&source=agent to the traces page,
which ignored the param and pinned source="user" — the popped-out tab asked
for the agent trace under the customer scope and 404'd with "Error loading
trace". Open the deep-linked id under the agent scope, and only while the
selection is that id: the list stays customer-only and navigating to a row
drops back to "user".

Also exercises the viewMode fallback effect (Detectors tab active when the
tab disappears) through the prop change that reaches it in production.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
…ts trace

A tool step looked for the first assistant bubble after it, but the trace
outcome is stamped on the run's last text segment only (persister and live
hook alike). In a text → tool → text turn the segment right after the first
step carries nothing, so that step silently lost "Open span". Look for the
bubble that carries an available trace instead.

Also renders and clicks the per-turn "View trace" footer link, which no test
reached before (the assistant fixture set no usage, so the footer never
mounted).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
…ctor-run clock

The 404 branch fired for source=agent but kept the detector self-trace copy,
and the detectors page fed it the detector run's timestamp for a finding's
agent trace too. The RCA starts after the run and takes minutes, so any
ingest lag on an available analysis trace opened from the Findings tab read
as "No trace was recorded for this run — the self-trace export didn't reach
the backend". Every entry point into an agent trace gates on the execution's
traceStatus being "available" — the export already succeeded — so a 404 is
ingest lag: say so, in analysis terms, and withhold the run timestamp for a
finding's trace.

The detectors page tests now run the real id helpers (the mock did not export
agentTraceId, so the finding path would have thrown) and cover the Finding
cell click and the ?traceId=&source=agent deep link.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
The name from an `event:` line was cleared only after a successful
JSON.parse of the next `data:` line, and never on the blank line that ends
the frame. A dataless named event, or one whose data is not JSON (the
agent's `event: error` can carry a raw string), left the name set and the
next plain data line was read as the trace payload and dropped. Reset on
the blank line and consume the name before parsing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
…e finding tooltip, gate agentTraceId on availability

Ablation results from the #2082 review:

- The sheet shadowed an exported `LayoutContext` with a no-op registerAiHost
  and aiPanelOpen: false, but the same PR already gives the viewer an
  `embedded` prop that means exactly that. Gate registerAiHost() and the
  nested AiAssistantPanel on `embedded`, un-export the context, drop the
  Provider. The sheet's interception test (against a stand-in viewer) is
  replaced by a stronger one on the real panel: embedded + aiPanelOpen: true
  → registerAiHost never called, no nested panel, no AI button.
- The Finding ID tooltip was a 10-branch nested ternary in JSX with one
  branch (`execution_trace_status != null`) unreachable by type. Extract
  describeFindingTraceTitle and drop the dead branch.
- agentTraceId() fell back to the dashless finding id, which the click path
  could never reach (only an available status makes the cell clickable) and
  which, for a legacy finding, named a trace that does not exist. Return the
  execution's trace id only once its export landed, so the ?source=agent deep
  link shares the cell's gate.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
Since #2071 the persister stores a tool step's result as text (serialised,
then redacted and truncated) and records why a result was not kept
(`withheld`), whether the capture was cut (`truncated`) and the real output
size (`outputBytes`). The sidebar still JSON.stringified the metadata value,
so a reloaded step showed a one-line escaped string where the live stream
had shown a pretty-printed object, and a withheld `bash` result reloaded as a
bubble with no output and no explanation.

Parse an intact JSON result back into the value the live stream rendered (a
truncated one cannot be valid JSON, a plain-string result never was; both
stay text), and render the capture verdict:
  [output withheld: not-allowlisted · 44 bytes]
  [captured I/O truncated · 90,000 bytes of output]

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
- Say in code that setAiContext({ traceId }) uses the customer id on purpose
  while the agent trace is on screen (the assistant's tools read customer
  traffic only).
- Key the "Analyzed trace" back-chip on what is displayed (effectiveTraceId
  !== traceId) rather than on the swap intent, so an RCA refetch that drops
  the agent trace cannot leave a back-chip to the trace itself.
- Skip the findings lookup for an internal trace opened directly; detectors
  never target those, so the answer is always empty.
- Pass no onOpenTrace to the message list when there is no projectId — the
  sheet does not mount without one, so the links opened nothing.
- Give the agent-trace drawer an sr-only description (Radix warned), and
  move the sheet test's stray `it` into its describe.
- Note why the live SSE URL carries no source while the cache key does.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
… user and session

The chrome snapshot mocks SpanInfoPanel away, and its chip row is the one
place the agent-trace work touched the customer path. Pin the real panel for
an RCA-less customer trace carrying user_id + session_id: User:/Session:
chips, nothing beside them. The header states the intended scope — a
customer trace with an available RCA gains the "Root cause analysis" chip on
purpose. The existing byte-compare stays untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
GabrielDrapor added a commit that referenced this pull request Sep 2, 2026
…-closed ui and span-focus PRs

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DXLChvn8HPA42wsuDUFYo
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.

1 participant