Add agent content tabs: Thinking, Context, Todos - #143
Conversation
Port the agent-observability content views into the OSS packages: - data: hasThinkingContent/hasContextContent guards, parseTodos/hasTodos - ui: ThinkingBadge plus DetailsView Thinking/Context tabs and Todos section, wired into DetailsView with content-aware tab selection - theme: badge-claude-thinking(-foreground) and context-source-conversation tokens - storybook: stories for the three tabs and the badge Tabs and sections render only when the span carries claude_code.* content, so non-Claude spans are unaffected. External JSON (thinking metadata, todos) is parsed behind type guards.
📝 WalkthroughWalkthroughThis PR adds Claude-specific content detection and parsing (thinking text, context metrics, todos) to the data package, new UI components (DetailsViewThinkingTab, DetailsViewContextTab, DetailsViewTodosSection, ThinkingBadge) wired into DetailsView, associated theme tokens, and Storybook stories. ChangesClaude thinking/context/todos feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant DetailsView
participant getTabItems
participant hasThinkingContent
participant hasContextContent
participant TabSelector
DetailsView->>getTabItems: compute tabItems(data)
getTabItems->>hasThinkingContent: check span attributes
getTabItems->>hasContextContent: check span attributes
getTabItems-->>DetailsView: return tabItems list
DetailsView->>DetailsView: useEffect reconciles selected tab
DetailsView->>TabSelector: render tabItems
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
packages/ui/src/components/DetailsView/DetailsView.tsx (1)
133-145: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winConsider lazy initial state to avoid a flash of wrong tab content.
When
defaultTabis"thinking"or"context"but the span lacks that content, the first render shows the wrong tab's empty state before theuseEffectcorrects it to"input-output". Using a lazy initializer eliminates the flash and the need for the effect to fire on mount:♻️ Proposed refactor
- const [tab, setTab] = useState<DetailsViewTab>(defaultTab); + const [tab, setTab] = useState<DetailsViewTab>(() => { + const items = getTabItems(data); + return items.some((item) => item.value === defaultTab) + ? defaultTab + : items[0]?.value ?? "input-output"; + });The
useEffectis still needed for subsequentdatachanges, but the lazy initializer ensures the first render is always correct.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/DetailsView/DetailsView.tsx` around lines 133 - 145, The current tab state initialization in DetailsView uses the raw defaultTab, which can render an empty state for a missing tab before the effect reconciles it. Update the useState setup for tab in DetailsView to use a lazy initializer based on getTabItems(data) so the first render selects an available tab (falling back to the always-present one), while keeping the existing useEffect only for later data changes.packages/ui/src/components/DetailsView/DetailsViewContextTab.tsx (1)
103-106: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
hasTokenBreakdownexcludes cache tokens from its guard.If a span has only
cache_read_input_tokensorcache_creation_input_tokensbut noinput_tokens/output_tokens, the Token Breakdown card won't render even though there is token data to show. Consider including cache tokens in the guard:const hasTokenBreakdown = - inputTokens !== undefined || outputTokens !== undefined; + inputTokens !== undefined || + outputTokens !== undefined || + (cacheReadTokens !== undefined && cacheReadTokens > 0) || + (cacheCreationTokens !== undefined && cacheCreationTokens > 0);This is unlikely in practice (cache tokens typically accompany input/output), but the current guard creates a silent gap.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/DetailsView/DetailsViewContextTab.tsx` around lines 103 - 106, `hasTokenBreakdown` in `DetailsViewContextTab` only checks `inputTokens` and `outputTokens`, so spans with only cache token metrics can be skipped. Update the guard to also consider the cache token fields used in this component so the Token Breakdown card renders whenever any token data is present. Keep the change localized to the `hasTokenBreakdown` condition and the related token data handling in `DetailsViewContextTab`.packages/ui/src/components/ThinkingBadge.tsx (1)
11-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
cn()instead of template literals for className composition.The rest of the codebase uses
cn()fromclassnamesfor composing className strings (e.g.,DetailsViewTodosSection,Badge). Using template literals here is inconsistent and can produce trailing spaces whenclassNameis falsy. Import and usecn()for consistency.♻️ Proposed refactor
import type { ReactElement } from "react"; import { Brain } from "lucide-react"; +import cn from "classnames"; + import { Badge } from "./Badge"; export interface ThinkingBadgeProps { className?: string; } export const ThinkingBadge = ({ className, }: ThinkingBadgeProps): ReactElement => { return ( <Badge label="Thinking" size="4" iconStart={<Brain className="size-3" />} - className={`bg-agentprism-badge-claude-thinking text-agentprism-badge-claude-thinking-foreground ${className || ""}`} + className={cn( + "bg-agentprism-badge-claude-thinking text-agentprism-badge-claude-thinking-foreground", + className, + )} /> ); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/ThinkingBadge.tsx` around lines 11 - 22, The ThinkingBadge component is composing its className with a template literal instead of the shared cn() helper, which is inconsistent with the rest of the codebase and can leave extra spacing when className is empty. Update ThinkingBadge to import cn() and use it to combine the base badge classes with the className prop, keeping the className logic aligned with similar components like Badge and DetailsViewTodosSection.packages/data/src/common/todos.ts (1)
19-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
getAttributeStringis duplicated across packages.The same
getAttributeStringhelper exists here and inpackages/ui/src/components/DetailsView/DetailsViewThinkingTab.tsx(lines 22–27). Consider exporting it from@evilmartians/agent-prism-dataso both packages share a single implementation, preventing drift in attribute-lookup logic.Also applies to: 22-27
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/data/src/common/todos.ts` around lines 19 - 24, The getAttributeString helper is duplicated between common/todos.ts and DetailsViewThinkingTab, so move it to a shared export in `@evilmartians/agent-prism-data` and import it from both places. Keep the implementation in a single source of truth, update the existing getAttributeString function location to be exported, and replace the local copy in DetailsViewThinkingTab with the shared import so attribute lookup logic stays consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/data/src/common/todos.ts`:
- Around line 19-24: The getAttributeString helper is duplicated between
common/todos.ts and DetailsViewThinkingTab, so move it to a shared export in
`@evilmartians/agent-prism-data` and import it from both places. Keep the
implementation in a single source of truth, update the existing
getAttributeString function location to be exported, and replace the local copy
in DetailsViewThinkingTab with the shared import so attribute lookup logic stays
consistent.
In `@packages/ui/src/components/DetailsView/DetailsView.tsx`:
- Around line 133-145: The current tab state initialization in DetailsView uses
the raw defaultTab, which can render an empty state for a missing tab before the
effect reconciles it. Update the useState setup for tab in DetailsView to use a
lazy initializer based on getTabItems(data) so the first render selects an
available tab (falling back to the always-present one), while keeping the
existing useEffect only for later data changes.
In `@packages/ui/src/components/DetailsView/DetailsViewContextTab.tsx`:
- Around line 103-106: `hasTokenBreakdown` in `DetailsViewContextTab` only
checks `inputTokens` and `outputTokens`, so spans with only cache token metrics
can be skipped. Update the guard to also consider the cache token fields used in
this component so the Token Breakdown card renders whenever any token data is
present. Keep the change localized to the `hasTokenBreakdown` condition and the
related token data handling in `DetailsViewContextTab`.
In `@packages/ui/src/components/ThinkingBadge.tsx`:
- Around line 11-22: The ThinkingBadge component is composing its className with
a template literal instead of the shared cn() helper, which is inconsistent with
the rest of the codebase and can leave extra spacing when className is empty.
Update ThinkingBadge to import cn() and use it to combine the base badge classes
with the className prop, keeping the className logic aligned with similar
components like Badge and DetailsViewTodosSection.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8b671b32-e23a-42d4-bf4d-1baab1d59c1a
📒 Files selected for processing (19)
packages/data/src/common/details-tabs.test.tspackages/data/src/common/details-tabs.tspackages/data/src/common/todos.test.tspackages/data/src/common/todos.tspackages/data/src/index.tspackages/storybook/src/stories/DetailsViewContextTab.stories.tsxpackages/storybook/src/stories/DetailsViewThinkingTab.stories.tsxpackages/storybook/src/stories/DetailsViewTodosSection.stories.tsxpackages/storybook/src/stories/ThinkingBadge.stories.tsxpackages/ui/src/components/DetailsView/DetailsView.tsxpackages/ui/src/components/DetailsView/DetailsViewContextTab.tsxpackages/ui/src/components/DetailsView/DetailsViewInputOutputTab.tsxpackages/ui/src/components/DetailsView/DetailsViewThinkingTab.tsxpackages/ui/src/components/DetailsView/DetailsViewTodosSection.tsxpackages/ui/src/components/ThinkingBadge.tsxpackages/ui/src/components/theme/index.tspackages/ui/src/components/theme/theme.csspackages/ui/src/index.tspackages/ui/src/theming/theme.ts
There was a problem hiding this comment.
Pull request overview
Ports Claude-specific agent observability UI into the OSS monorepo by adding content-aware Thinking and Context tabs to DetailsView, plus a Todos section, backed by new packages/data presence guards and a TodoWrite parser.
Changes:
- Added
packages/datahelpers (hasThinkingContent,hasContextContent,parseTodos,hasTodos) withvitestcoverage. - Extended
packages/uiDetailsViewwith conditional tab offering + new tab/section components (Thinking/Context/Todos) and aThinkingBadge. - Updated theming to include new Claude-related tokens and added Storybook stories for the new UI pieces.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ui/src/theming/theme.ts | Adds source-of-truth theme tokens for Claude thinking/context UI elements. |
| packages/ui/src/index.ts | Re-exports new UI components and ?raw sources from the barrel. |
| packages/ui/src/components/ThinkingBadge.tsx | Introduces a new badge component for “Thinking”. |
| packages/ui/src/components/theme/theme.css | Adds generated CSS variables for the new theme tokens. |
| packages/ui/src/components/theme/index.ts | Registers new token names for Tailwind color helpers. |
| packages/ui/src/components/DetailsView/DetailsViewTodosSection.tsx | Adds a Todos section renderer driven by parsed span attributes. |
| packages/ui/src/components/DetailsView/DetailsViewThinkingTab.tsx | Adds a Thinking tab that parses and displays extended-thinking content. |
| packages/ui/src/components/DetailsView/DetailsViewInputOutputTab.tsx | Integrates Todos section into In/Out tab and updates empty-state logic. |
| packages/ui/src/components/DetailsView/DetailsViewContextTab.tsx | Adds a Context tab to display context-window and token/cost stats. |
| packages/ui/src/components/DetailsView/DetailsView.tsx | Makes tabs content-aware and reconciles tab selection when data changes. |
| packages/storybook/src/stories/ThinkingBadge.stories.tsx | Adds Storybook coverage for the ThinkingBadge. |
| packages/storybook/src/stories/DetailsViewTodosSection.stories.tsx | Adds Storybook coverage for Todos section states. |
| packages/storybook/src/stories/DetailsViewThinkingTab.stories.tsx | Adds Storybook coverage for the Thinking tab (with/without metadata). |
| packages/storybook/src/stories/DetailsViewContextTab.stories.tsx | Adds Storybook coverage for the Context tab (full/breakdown/empty). |
| packages/data/src/index.ts | Exposes new details-tabs and todos helpers from the data package entrypoint. |
| packages/data/src/common/todos.ts | Implements guarded parsing and presence detection for claude_code.todos. |
| packages/data/src/common/todos.test.ts | Adds unit tests for todos parsing and cross-vendor behavior. |
| packages/data/src/common/details-tabs.ts | Adds presence guards for Thinking/Context tab offering. |
| packages/data/src/common/details-tabs.test.ts | Adds tests ensuring Claude tabs don’t appear for non-Claude spans. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import type { ReactElement } from "react"; | ||
|
|
||
| import { Brain } from "lucide-react"; | ||
|
|
||
| import { Badge } from "./Badge"; | ||
|
|
||
| export interface ThinkingBadgeProps { | ||
| className?: string; | ||
| } | ||
|
|
||
| export const ThinkingBadge = ({ | ||
| className, | ||
| }: ThinkingBadgeProps): ReactElement => { | ||
| return ( | ||
| <Badge | ||
| label="Thinking" | ||
| size="4" | ||
| iconStart={<Brain className="size-3" />} | ||
| className={`bg-agentprism-badge-claude-thinking text-agentprism-badge-claude-thinking-foreground ${className || ""}`} | ||
| /> | ||
| ); | ||
| }; |
| useEffect(() => { | ||
| if (!tabItems.some((item) => item.value === tab)) { | ||
| setTab(tabItems[0]?.value ?? defaultTab); | ||
| } | ||
| }, [tabItems, tab, defaultTab]); |
| export function hasThinkingContent(data: SpanTabData): boolean { | ||
| return ( | ||
| data.attributes?.some((attr) => attr.key === "claude_code.thinking") ?? | ||
| false | ||
| ); | ||
| } |
| export function hasContextContent(data: SpanTabData): boolean { | ||
| return ( | ||
| data.attributes?.some( | ||
| (attr) => | ||
| attr.key === "claude_code.cumulative_tokens" || | ||
| attr.key === "claude_code.context_fill_percent", | ||
| ) ?? false | ||
| ); | ||
| } |
| const hasContextData = | ||
| cumulativeTokens !== undefined || fillPercent !== undefined; | ||
| const hasTokenBreakdown = | ||
| inputTokens !== undefined || outputTokens !== undefined; | ||
|
|
| const hasOutput = Boolean(data.output); | ||
|
|
||
| if (!hasInput && !hasOutput) { | ||
| if (!hasInput && !hasOutput && !hasTodos(data)) { |
The old StatRow used per-row flex + justify-between, so a long label
("Cumulative tokens") collided with its value and the bold values didn't
line up across rows once a trailing "sub" annotation was present.
Render the stats as a 3-column grid (label / value / sub) and give each row
grid-cols-subgrid, so values right-align in a shared column and the sub
annotations line up in the next one, regardless of label or value width.
- Thinking tab: wrap the level-badge/triggers row and break long words in the extended-thinking body so a long trigger list or unbroken token can't overflow horizontally. - Todos section: let long task content wrap inside its row (min-w-0 + break-words) instead of pushing the row width.
Agent content tabs: Thinking / Context / Todos
Ports the three agent-observability content views from the private
agent-prism-saaslayer into the OSS monorepo: Thinking and Context tabs onDetailsView, plus a Todos section — driven by sharedclaude_code.*presence guards and a TodoWrite parser. Follows the established port pattern: pure logic inpackages/data(kebab-case, colocatedvitest), UI inpackages/ui(named export +?rawsource in the barrel,agentprism-*tokens,classnames), Storybook stories to finish. Built test-first.Screenshots
Thinking tab — level badge, triggers, extended-thinking content
Context tab — context-window position bar (with compaction marker), stat rows, token breakdown, cost
Todos section — mixed statuses (completed / in-progress / pending) with counts

ThinkingBadge

What's included
packages/data(pure, unit-tested)common/details-tabs.ts→hasThinkingContent,hasContextContentcommon/todos.ts→parseTodos,hasTodos,TodoItem,TodoStatuspackages/uiThinkingBadge,DetailsViewThinkingTab,DetailsViewContextTab,DetailsViewTodosSection(todo types imported from@evilmartians/agent-prism-data)DetailsView: the staticTAB_ITEMSlist is now a content-awaregetTabItems(data)— the Thinking / Context tabs are only offered when the span actually carries that contentDetailsViewTodosSectionrenders insideDetailsViewInputOutputTaband returnsnullwhen there are no todos?rawsourcesbadge-claude-thinking,-foreground,context-source-conversation) added to the theming source + generated CSSpackages/storybook— stories for the three tabs (incl. mixed todo statuses) and the badge.Design notes
claude_code.*attributes, so a plain OTLP/Langfuse span never grows Claude-specific tabs.hasContextContentdeliberately does not fire on genericgen_ai.usage.input_tokens(which would otherwise grow a Context tab on any LLM span). Locked by a cross-vendor test.claude_code.thinking_metadataandclaude_code.todos— is parsed tounknownand narrowed with type guards (isTodoItem,parseThinkingMetadata) rather thanas, so malformed payloads degrade gracefully instead of crashing the render.~/types TraceRecordWithSource→ the library'sTraceSpan; Japa.spec.ts→vitest; snake_case → kebab-case filenames. No analytics /SpanRawContext/ IO coupling introduced.hasImagesContentintentionally dropped. The SaaSdetails_tabsalso exportshasImagesContent, but it depends onimage_paths(PR 3). PR 4 is independent, and its criteria cover only thinking/context, so images are left to PR 3.Code review
Reviewed by two independent passes before merge; both converged on the same core findings, all fixed in this branch:
as ThinkingMetadatacast on rawJSON.parsewith a guard that validatesleveland normalizestriggers/disabled(removes a render-crash path).isTodoItem— tightened to verifycontent/activeFormare strings andstatusis a validTodoStatus, soparseTodoscan't return objects that violateTodoItem.DetailsViewnow reconciles the selected tab whendatachanges, so a previously-selected conditional tab that disappears falls back to In/Out instead of showing an orphaned empty state.0..100and a zero/negativecontext_limitguarded, preventing negative /NaNbar widths.Testing
vitest: 363 passed (incl. new coverage for the tightened todos guard)tscclean:packages/data,packages/ui,packages/storybookeslint: 0 errors (theagentprism-*no-custom-classnamewarnings are the repo-wide baseline)storybook build: greenAcceptance criteria
hasThinkingContent,hasContextContent+parseTodos/hasTodos/TodoItem/TodoStatusinpackages/data+vitestThinkingBadge,DetailsViewThinkingTab,DetailsViewContextTab,DetailsViewTodosSectioninpackages/ui(barrel +?raw)tsc+eslintclean,storybook buildgreenReviewer note
The 3 theme tokens were hand-added to the generated
theme/index.ts+theme.cssrather than via thegenerate-theme-filesscript — the generator is currently non-idempotent (it duplicates"primary"intheme/index.ts). The source of truththeming/theme.tsis updated so a future (fixed) regen stays consistent.Summary by CodeRabbit
New Features
Bug Fixes