Skip to content

feat: add transcript conversation view to AI agent details#2333

Merged
weeco merged 8 commits intomasterfrom
ms/transcript-reconstruct-history
Mar 24, 2026
Merged

feat: add transcript conversation view to AI agent details#2333
weeco merged 8 commits intomasterfrom
ms/transcript-reconstruct-history

Conversation

@weeco
Copy link
Copy Markdown
Contributor

@weeco weeco commented Mar 23, 2026

Summary

  • Add transcripts tab to the AI agent details page showing conversation history derived from OTel trace data
  • Conversation list with status, turn count, token usage, and duration
  • Conversation detail page with two view modes: chat (linear) and detailed (three-column with metrics)
  • Add is_reconstructed field to TranscriptTurn proto for turns restored from accumulated chat history when earlier spans are evicted
  • Reconstructed turns rendered with reduced opacity, dashed borders, and a history separator
  • Clarify turn_count counts user turns (interactions), not all messages

Test plan

  • Verify transcripts tab appears on agent details page and lists conversations
  • Verify conversation detail page renders in both chat and detailed view modes
  • Verify reconstructed turns show visual distinction (dashed borders, "Restored from history" label)
  • Verify tool calls display correctly with status indicators and latency bars
  • Verify turn count in list view matches interaction count in detail view

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 23, 2026

The latest Buf updates on your PR. Results from workflow Buf CI / validate (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedMar 24, 2026, 4:45 PM

@weeco weeco force-pushed the ms/transcript-reconstruct-history branch from 3ee4eb9 to 9849ec9 Compare March 23, 2026 12:29
@weeco weeco changed the title transcript: add is_reconstructed field and render restored history feat: add transcript conversation view to AI agent details Mar 23, 2026
@weeco weeco requested a review from malinskibeniamin March 23, 2026 13:01
@weeco
Copy link
Copy Markdown
Contributor Author

weeco commented Mar 23, 2026

@claude Review the PR

@claude
Copy link
Copy Markdown

claude bot commented Mar 23, 2026

Claude finished @weeco's task in 2m 21s —— View job


PR Review

  • Gather context and read changed files
  • Review proto changes
  • Review frontend components
  • Review routing and API layer
  • Summarize findings

Overall this is well-structured, follows existing patterns, and the feature is clearly useful. The code is clean and readable. A few issues worth addressing:

Issues

1. System prompt not expandable in Chat view (conversation-detail-page.tsx:402)

In ChatView, the system prompt is rendered as a raw <p> tag with no truncation:

<p className="mt-2 text-muted-foreground text-sm leading-relaxed">{systemPrompt}</p>

But in ThreeColumnView it uses <ExpandableText maxLength={200} ... />. For agents with long system prompts (common in production), the Chat view would require scrolling past a wall of text before seeing any conversation. Should use ExpandableText here too. Fix this →

2. toolCallId may be empty — unstable React key (conversation-detail-page.tsx:335)

In ThreeColumnView, tool calls are keyed by tool.toolCallId:

{allToolCalls.map((tool) => (
  <ToolCallItem key={tool.toolCallId} ... />
))}

The proto definition has tool_call_id with only max_len = 128 (no min_len, not REQUIRED), so it can be an empty string. Multiple tools with empty IDs would get duplicate keys. Same issue at conversation-detail-page.tsx:448 in ChatView. Consider using the array index as fallback: key={tool.toolCallId || index}. Fix this →

3. No empty state when a transcript has zero turns (conversation-detail-page.tsx:607-611)

If the API returns a transcript with an empty turns array, both ThreeColumnView and ChatView render nothing — no message to the user. Should add an empty state, at minimum a short "No turns recorded" note. Fix this →

4. pageSize: -1 fetches all conversations (transcript.ts:35)

const request = create(ListTranscriptsRequestSchema, {
  agentId: input.agentId,
  pageSize: -1,  // Disables pagination — fetches everything
});

This is fine for now since client-side filtering needs all the data, but for an agent that's been running for months this could return hundreds or thousands of conversations in a single payload. Worth adding a comment explaining the intentional choice and tracking a follow-up to add server-side pagination + filtering when the list grows too large.

5. Math.max spread on potentially large array (conversation-detail-page.tsx:230)

const maxLatency = Math.max(...allToolCalls.map(toolCallLatencyMs), 0);

The spread can hit the JS argument stack limit if there are many tool calls. Prefer reduce:

const maxLatency = allToolCalls.reduce((max, tc) => Math.max(max, toolCallLatencyMs(tc)), 0);

Minor in practice (tool calls rarely number in the thousands), but a cheap fix.


Minor Observations

  • TRANSCRIPT_TURN_ROLE_SYSTEM not handled in groupTurnsIntoInteractions (conversation-detail-page.tsx:191). The system prompt is returned separately in GetTranscriptResponse, so system role turns likely won't appear in the turns array — but if they ever do, they'd be silently dropped. Could add an explicit SYSTEM guard if needed.

  • Breadcrumb shows agent ID, not display name (conversation-detail-page.tsx:521): { title: summary?.agentId ?? id, ... } — shows the raw agent ID in the breadcrumb since TranscriptSummary doesn't include a display name. Might be jarring for human-readable names, but there's nothing to link to here without an extra fetch.

  • No auto-refresh for RUNNING conversations — the transcripts list has a manual refresh button. For an active conversation, the user has to refresh manually. A short poll interval when any transcript has RUNNING status would improve the experience, though this is an enhancement not a bug.

  • Route fix (/agents/$id/agents/$id/) in the tab components is correct — aligns all tabs to use the trailing-slash route consistently.

  • Proto change (is_reconstructed field 10 on TranscriptTurn) is well-documented with a clear field comment explaining the semantics. The proto CI confirms no breaking changes.

@weeco
Copy link
Copy Markdown
Contributor Author

weeco commented Mar 23, 2026

@claude re-review

@claude
Copy link
Copy Markdown

claude bot commented Mar 23, 2026

Claude finished @weeco's task in 53s —— View job


PR Re-Review

  • Gather context
  • Read changed files
  • Check if previous review issues were addressed
  • Summarize findings

All 5 issues from the previous review have been addressed in commit 459482f:

# Issue Status
1 System prompt not expandable in Chat view ✅ Fixed — conversation-detail-page.tsx:402 now uses <ExpandableText maxLength={400} .../>
2 toolCallId empty → unstable React key ✅ Fixed — both ThreeColumnView (line 335) and ChatView (line 457) use tool.toolCallId || \tool-${toolIndex}``
3 No empty state for zero turns ✅ Fixed — lines 607-611 render MessagesSquare icon + "No turns recorded"
4 Math.max spread on large array ✅ Fixed — line 230 uses allToolCalls.reduce(...)
5 pageSize: -1 undocumented intent ✅ Fixed — transcript.ts:35 adds // Fetch all; TODO: add server-side pagination for large conversation histories

The code is clean, well-structured, and ready to merge. No new issues found.

@weeco weeco requested a review from malinskibeniamin March 23, 2026 13:41
Copy link
Copy Markdown
Contributor

@malinskibeniamin malinskibeniamin left a comment

Choose a reason for hiding this comment

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

@claude review this PR. Check comments already left behind

@claude
Copy link
Copy Markdown

claude bot commented Mar 24, 2026

Claude finished @malinskibeniamin's task in 2m 38s —— View job


PR Re-Review

  • Gather context
  • Read changed files
  • Check malinskibeniamin's comments
  • Summarize findings

Addressing each comment from @malinskibeniamin:

transcript-utils.ts → move to time utils or use a package

Already resolved in commit 7cf9322. The custom transcript-utils.ts file was removed and the code now imports directly from the existing components/pages/transcripts/utils/transcript-formatters.ts. Both ai-agent-transcripts-tab.tsx and conversation-detail-page.tsx use:

import { formatDuration as formatDurationMs, formatTimestamp as formatTimestampMs } from 'components/pages/transcripts/utils/transcript-formatters';

✅ This comment is already addressed.


$conversationId.tsx:30 — prefetch when feature flag disabled?

Valid question. The loader unconditionally prefetches the transcript data:

loader: ({ context: { queryClient, dataplaneTransport }, params: { id, conversationId } }) => {
  queryClient.prefetchQuery(...); // always runs
},

Looking at ai-agent-details-page.tsx, the Transcripts tab itself has no feature flag guard — it's always rendered. So if there's no feature flag on the tab, there's no reason to add one on the loader either. If a feature flag is added in the future to gate the transcripts tab, the loader should be updated at the same time.

The concern is valid as a future-proofing note but there's no feature flag system in scope here yet. Worth a comment in the loader or a follow-up ticket when the feature flag mechanism is defined.


ai-agent-transcripts-tab.tsx:64 — useMemo unnecessary for frequently changing data

const transcripts = useMemo(() => data?.transcripts ?? [], [data?.transcripts]);

@malinskibeniamin is correct that this memo has limited benefit. It only provides reference stability for the [] fallback when data?.transcripts is undefined — every actual refetch replaces data?.transcripts with a new array reference anyway, which triggers filteredTranscripts to recompute regardless. The memo adds cognitive overhead without meaningful performance gain.

Suggested simplification — inline the fallback directly in filteredTranscripts:

const filteredTranscripts = useMemo(() => {
  const all = data?.transcripts ?? [];
  const query = searchQuery.toLowerCase();
  return all.filter((t) => { ... });
}, [data?.transcripts, searchQuery, statusFilter]);

And remove the transcripts memo entirely. Note: the transcripts variable is also used in the empty-state check transcripts.length > 0 on line 142, so that reference would need to be adjusted too.

Fix this →


ai-agent-transcripts-tab.tsx:173 — calculation in JSX, move to utility

{transcript.startTime ? formatTimestampMs(timestampDate(transcript.startTime).getTime()) : '—'}

Agreed — this multi-step call (proto → Date → ms → formatted string) is business logic that doesn't belong inline in JSX. A utility like this already fits in transcript-formatters.ts:

export const formatTranscriptTimestamp = (timestamp: Timestamp | undefined): string =>
  timestamp ? formatTimestamp(timestampDate(timestamp).getTime()) : '—';

This would also benefit conversation-detail-page.tsx which has similar patterns.

Fix this →


ai-agent-transcripts-tab.tsx:183 — utility method for token formatting

{transcript.usage?.totalTokens != null
  ? Number(transcript.usage.totalTokens).toLocaleString()
  : '—'}

Also valid. The Number(bigint).toLocaleString() pattern + em-dash fallback is repeated and warrants a utility. Something like:

export const formatTokenCount = (tokens: bigint | number | undefined | null): string =>
  tokens != null ? Number(tokens).toLocaleString() : '—';

This matches the existing formatTokensWithUnit utility in transcript-formatters.ts but without the unit label — could be consolidated or added as a variant.

Fix this →


ai-agent-transcripts-tab.tsx:94 — use shared <Empty /> component

The tab already has an empty state (lines 138-144 with Inbox icon), which is good. However, @malinskibeniamin suggests using a shared <Empty /> component for consistency. I couldn't find a generic <Empty /> component in the agents/details directory — the closest is MCPEmpty which is MCP-specific. @malinskibeniamin, could you point to the specific Empty component you're referring to? If it's not yet added to the codebase, it may need to be created first.

The current inline empty state (Inbox + text) is functional and reasonable, but replacing it with a shared component would improve consistency once that component exists.


Additional observations (not in existing comments)

  • dataUpdatedAt timestamp format (line 121): new Date(dataUpdatedAt).toLocaleTimeString(...) is another inline formatting call that could be a utility, consistent with the other feedback above.

  • statusToFilterKey function (lines 43-54): This could live in a shared location or alongside ConversationStatusBadge since both map TranscriptStatus to string representations.

  • The rest of the code is clean and well-structured. The previous 5 issues from the prior review were all fixed.

@malinskibeniamin
Copy link
Copy Markdown
Contributor

@weeco weeco requested a review from malinskibeniamin March 24, 2026 14:44
@weeco weeco enabled auto-merge (squash) March 24, 2026 15:12
weeco added 8 commits March 24, 2026 17:44
Add is_reconstructed to TranscriptTurn proto so the frontend can
visually distinguish turns restored from accumulated chat history vs
turns backed by live OTel spans.

Frontend renders reconstructed turns with reduced opacity, dashed
borders, a history separator, and hides unavailable metrics.
Add transcripts tab to agent details page with conversation list,
detail view (chat + detailed modes), status badges, and query hook.
Restructure agent routes to support nested transcript pages.
React Compiler cannot handle BigIntLiteral expressions (0n).
Use plain 0 since the value is immediately wrapped in Number().
- Use ExpandableText for system prompt in ChatView (was raw <p>)
- Add index fallback for tool call React keys (toolCallId can be empty)
- Add empty state when transcript has zero turns
- Replace Math.max spread with reduce to avoid stack limit risk
- Add TODO comment on pageSize: -1 for future pagination
Delete transcript-utils.ts and use the existing formatDuration and
formatTimestamp from transcripts/utils/transcript-formatters.ts with
buf wkt converters (durationMs, timestampDate) at call sites.
- Gate route prefetch on TracingService feature support
- Remove unnecessary useMemo on transcript list (data changes per refresh)
- Replace custom empty state with shared Empty compound component
- Extract formatProtoTimestamp, formatProtoTime, formatProtoDuration,
  formatTokenCount utilities to keep JSX dumb
- Replace all inline proto timestamp/duration/token formatting with utils
@weeco weeco force-pushed the ms/transcript-reconstruct-history branch from 1290799 to 1bf5e2f Compare March 24, 2026 16:45
@weeco weeco merged commit 35da739 into master Mar 24, 2026
23 checks passed
@weeco weeco deleted the ms/transcript-reconstruct-history branch March 24, 2026 16:53
weeco added a commit that referenced this pull request Mar 24, 2026
The generated route tree was not updated after the agent transcript
conversation route was added in #2333.
weeco added a commit that referenced this pull request Mar 24, 2026
The generated route tree was not updated after the agent transcript
conversation route was added in #2333.
weeco added a commit that referenced this pull request Mar 24, 2026
The generated route tree was not updated after the agent transcript
conversation route was added in #2333.
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.

2 participants