Skip to content

Add agent content tabs: Thinking, Context, Todos - #143

Open
Valyay wants to merge 3 commits into
mainfrom
pr4/agent-content-tabs
Open

Add agent content tabs: Thinking, Context, Todos#143
Valyay wants to merge 3 commits into
mainfrom
pr4/agent-content-tabs

Conversation

@Valyay

@Valyay Valyay commented Jul 8, 2026

Copy link
Copy Markdown
Member

Agent content tabs: Thinking / Context / Todos

Ports the three agent-observability content views from the private agent-prism-saas layer into the OSS monorepo: Thinking and Context tabs on DetailsView, plus a Todos section — driven by shared claude_code.* presence guards and a TodoWrite parser. Follows the established port pattern: pure logic in packages/data (kebab-case, colocated vitest), UI in packages/ui (named export + ?raw source in the barrel, agentprism-* tokens, classnames), Storybook stories to finish. Built test-first.

Screenshots

Thinking tab — level badge, triggers, extended-thinking content

thinking-tab

Context tab — context-window position bar (with compaction marker), stat rows, token breakdown, cost

context-tab context-breakdown

Todos section — mixed statuses (completed / in-progress / pending) with counts
todos-section

ThinkingBadge
thinking-badge

What's included

packages/data (pure, unit-tested)

  • common/details-tabs.tshasThinkingContent, hasContextContent
  • common/todos.tsparseTodos, hasTodos, TodoItem, TodoStatus

packages/ui

  • New components: ThinkingBadge, DetailsViewThinkingTab, DetailsViewContextTab, DetailsViewTodosSection (todo types imported from @evilmartians/agent-prism-data)
  • DetailsView: the static TAB_ITEMS list is now a content-aware getTabItems(data) — the Thinking / Context tabs are only offered when the span actually carries that content
  • DetailsViewTodosSection renders inside DetailsViewInputOutputTab and returns null when there are no todos
  • All four exported from the barrel with ?raw sources
  • 3 new theme tokens (badge-claude-thinking, -foreground, context-source-conversation) added to the theming source + generated CSS

packages/storybook — stories for the three tabs (incl. mixed todo statuses) and the badge.

Design notes

  • Cross-vendor safety. Every guard keys strictly on claude_code.* attributes, so a plain OTLP/Langfuse span never grows Claude-specific tabs. hasContextContent deliberately does not fire on generic gen_ai.usage.input_tokens (which would otherwise grow a Context tab on any LLM span). Locked by a cross-vendor test.
  • Boundary parsing, no casts. External JSON — claude_code.thinking_metadata and claude_code.todos — is parsed to unknown and narrowed with type guards (isTodoItem, parseThinkingMetadata) rather than as, so malformed payloads degrade gracefully instead of crashing the render.
  • Coupling strips (per the port convention). ~/types TraceRecordWithSource → the library's TraceSpan; Japa .spec.tsvitest; snake_case → kebab-case filenames. No analytics / SpanRawContext / IO coupling introduced.
  • Scope — hasImagesContent intentionally dropped. The SaaS details_tabs also exports hasImagesContent, but it depends on image_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:

  • Thinking-metadata parsing — replaced an as ThinkingMetadata cast on raw JSON.parse with a guard that validates level and normalizes triggers/disabled (removes a render-crash path).
  • isTodoItem — tightened to verify content/activeForm are strings and status is a valid TodoStatus, so parseTodos can't return objects that violate TodoItem.
  • Stale tab stateDetailsView now reconciles the selected tab when data changes, so a previously-selected conditional tab that disappears falls back to In/Out instead of showing an orphaned empty state.
  • Context-fill clamp — fill percent clamped to 0..100 and a zero/negative context_limit guarded, preventing negative / NaN bar widths.

Testing

  • vitest: 363 passed (incl. new coverage for the tightened todos guard)
  • tsc clean: packages/data, packages/ui, packages/storybook
  • eslint: 0 errors (the agentprism-* no-custom-classname warnings are the repo-wide baseline)
  • storybook build: green

Acceptance criteria

  • hasThinkingContent, hasContextContent + parseTodos / hasTodos / TodoItem / TodoStatus in packages/data + vitest
  • ThinkingBadge, DetailsViewThinkingTab, DetailsViewContextTab, DetailsViewTodosSection in packages/ui (barrel + ?raw)
  • Each tab/section shown only when its content is present
  • Storybook stories for the three tabs (incl. mixed todo statuses)
  • tsc + eslint clean, storybook build green

Reviewer note

The 3 theme tokens were hand-added to the generated theme/index.ts + theme.css rather than via the generate-theme-files script — the generator is currently non-idempotent (it duplicates "primary" in theme/index.ts). The source of truth theming/theme.ts is updated so a future (fixed) regen stays consistent.

Summary by CodeRabbit

  • New Features

    • Added new Details View tabs for thinking content and context metrics when available.
    • Added a tasks section in the input/output view to show parsed todos and their status counts.
    • Added a “Thinking” badge for spans with extended thinking content.
  • Bug Fixes

    • Details View now shows only relevant tabs and automatically switches away from unavailable tabs.
    • Empty states now better handle spans that contain tasks, thinking, or context data.

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.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Claude thinking/context/todos feature

Layer / File(s) Summary
Data helpers: tab-detection and todo parsing
packages/data/src/common/details-tabs.ts, packages/data/src/common/todos.ts, packages/data/src/common/*.test.ts, packages/data/src/index.ts
Adds hasThinkingContent/hasContextContent presence checks and parseTodos/hasTodos parsing with type guards, tests, and package re-exports.
Thinking tab and badge UI
packages/ui/src/components/DetailsView/DetailsViewThinkingTab.tsx, packages/ui/src/components/ThinkingBadge.tsx, packages/storybook/src/stories/DetailsViewThinkingTab.stories.tsx, packages/storybook/src/stories/ThinkingBadge.stories.tsx
Renders extended thinking content with metadata badge/level, standalone thinking badge atom, and corresponding stories.
Context tab UI
packages/ui/src/components/DetailsView/DetailsViewContextTab.tsx, packages/storybook/src/stories/DetailsViewContextTab.stories.tsx
Renders context window position, token breakdown, and cost cards from typed span attributes, with stories for full/partial/empty data.
Todos section UI and input/output tab integration
packages/ui/src/components/DetailsView/DetailsViewTodosSection.tsx, packages/ui/src/components/DetailsView/DetailsViewInputOutputTab.tsx, packages/storybook/src/stories/DetailsViewTodosSection.stories.tsx
Renders a Tasks list from parsed todos, integrates it into the input/output tab's empty-state logic and layout, with stories.
DetailsView tab wiring
packages/ui/src/components/DetailsView/DetailsView.tsx
Dynamically computes available tabs (including "thinking"/"context") based on span content and reconciles selected tab state.
Theme tokens and exports
packages/ui/src/components/theme/*, packages/ui/src/theming/theme.ts, packages/ui/src/index.ts
Adds Claude-specific color tokens/CSS variables and exports new components from the package barrel.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main addition of Thinking, Context, and Todos agent content tabs.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pr4/agent-content-tabs

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (4)
packages/ui/src/components/DetailsView/DetailsView.tsx (1)

133-145: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider lazy initial state to avoid a flash of wrong tab content.

When defaultTab is "thinking" or "context" but the span lacks that content, the first render shows the wrong tab's empty state before the useEffect corrects 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 useEffect is still needed for subsequent data changes, 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

hasTokenBreakdown excludes cache tokens from its guard.

If a span has only cache_read_input_tokens or cache_creation_input_tokens but no input_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 win

Use cn() instead of template literals for className composition.

The rest of the codebase uses cn() from classnames for composing className strings (e.g., DetailsViewTodosSection, Badge). Using template literals here is inconsistent and can produce trailing spaces when className is falsy. Import and use cn() 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

getAttributeString is duplicated across packages.

The same getAttributeString helper exists here and in packages/ui/src/components/DetailsView/DetailsViewThinkingTab.tsx (lines 22–27). Consider exporting it from @evilmartians/agent-prism-data so 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

📥 Commits

Reviewing files that changed from the base of the PR and between dd8b685 and 6aa56b3.

📒 Files selected for processing (19)
  • packages/data/src/common/details-tabs.test.ts
  • packages/data/src/common/details-tabs.ts
  • packages/data/src/common/todos.test.ts
  • packages/data/src/common/todos.ts
  • packages/data/src/index.ts
  • packages/storybook/src/stories/DetailsViewContextTab.stories.tsx
  • packages/storybook/src/stories/DetailsViewThinkingTab.stories.tsx
  • packages/storybook/src/stories/DetailsViewTodosSection.stories.tsx
  • packages/storybook/src/stories/ThinkingBadge.stories.tsx
  • packages/ui/src/components/DetailsView/DetailsView.tsx
  • packages/ui/src/components/DetailsView/DetailsViewContextTab.tsx
  • packages/ui/src/components/DetailsView/DetailsViewInputOutputTab.tsx
  • packages/ui/src/components/DetailsView/DetailsViewThinkingTab.tsx
  • packages/ui/src/components/DetailsView/DetailsViewTodosSection.tsx
  • packages/ui/src/components/ThinkingBadge.tsx
  • packages/ui/src/components/theme/index.ts
  • packages/ui/src/components/theme/theme.css
  • packages/ui/src/index.ts
  • packages/ui/src/theming/theme.ts

Copilot AI 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.

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/data helpers (hasThinkingContent, hasContextContent, parseTodos, hasTodos) with vitest coverage.
  • Extended packages/ui DetailsView with conditional tab offering + new tab/section components (Thinking/Context/Todos) and a ThinkingBadge.
  • 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.

Comment on lines +1 to +22
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 || ""}`}
/>
);
};
Comment on lines +141 to +145
useEffect(() => {
if (!tabItems.some((item) => item.value === tab)) {
setTab(tabItems[0]?.value ?? defaultTab);
}
}, [tabItems, tab, defaultTab]);
Comment on lines +10 to +15
export function hasThinkingContent(data: SpanTabData): boolean {
return (
data.attributes?.some((attr) => attr.key === "claude_code.thinking") ??
false
);
}
Comment on lines +23 to +31
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
);
}
Comment on lines +103 to +107
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)) {
Valyay added 2 commits July 8, 2026 23:06
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.
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