From 2fcf111f77e9fa14ce58eb7651af390e5d2fb63f Mon Sep 17 00:00:00 2001 From: matt Date: Fri, 20 Feb 2026 13:27:34 +0900 Subject: [PATCH] feat: enhance ContextBadge and SessionContextPanel with new FlatInjectionList view - Updated ContextBadge to display the total count of tool outputs and task coordination items based on their breakdowns. - Introduced FlatInjectionList component for a denested view of injections, allowing users to toggle between grouped and flat views in SessionContextPanel. - Added state management for flat view toggle and integrated FlatInjectionList into the existing layout. --- src/renderer/components/chat/ContextBadge.tsx | 10 +- .../components/FlatInjectionList.tsx | 250 ++++++++++++++++++ .../chat/SessionContextPanel/index.tsx | 58 +++- src/renderer/utils/contextTracker.ts | 4 +- 4 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx diff --git a/src/renderer/components/chat/ContextBadge.tsx b/src/renderer/components/chat/ContextBadge.tsx index 737ee229..ba62943c 100644 --- a/src/renderer/components/chat/ContextBadge.tsx +++ b/src/renderer/components/chat/ContextBadge.tsx @@ -478,7 +478,10 @@ export const ContextBadge = ({ {newToolOutputInjections.length > 0 && ( sum + inj.toolBreakdown.length, + 0 + )} tokenCount={toolOutputTokens} > {newToolOutputInjections.map((injection) => @@ -501,7 +504,10 @@ export const ContextBadge = ({ {newTaskCoordinationInjections.length > 0 && ( sum + inj.breakdown.length, + 0 + )} tokenCount={taskCoordinationTokens} > {newTaskCoordinationInjections.map((injection) => diff --git a/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx new file mode 100644 index 00000000..cb2d9e39 --- /dev/null +++ b/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx @@ -0,0 +1,250 @@ +/** + * FlatInjectionList - Completely denested view where every individual tool call, + * thinking block, and coordination item is its own row, sorted by token size descending. + * Makes it obvious whether a single large tool or many small ones are consuming tokens. + */ + +import React, { useMemo } from 'react'; + +import { CopyButton } from '@renderer/components/common/CopyButton'; +import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; + +import { formatTokens } from '../utils/formatting'; +import { parseTurnIndex } from '../utils/pathParsing'; + +import type { ContextInjection } from '@renderer/types/contextInjection'; + +// ============================================================================= +// Constants +// ============================================================================= + +const CATEGORY_COLORS: Record = { + 'claude-md': { bg: 'rgba(99, 102, 241, 0.15)', text: '#818cf8', label: 'CLAUDE.md' }, + 'mentioned-file': { bg: 'rgba(52, 211, 153, 0.15)', text: '#34d399', label: 'File' }, + 'tool-output': { bg: 'rgba(251, 191, 36, 0.15)', text: '#fbbf24', label: 'Tool' }, + 'thinking-text': { bg: 'rgba(167, 139, 250, 0.15)', text: '#a78bfa', label: 'Thinking' }, + 'task-coordination': { bg: 'rgba(251, 146, 60, 0.15)', text: '#fb923c', label: 'Team' }, + 'user-message': { bg: 'rgba(96, 165, 250, 0.15)', text: '#60a5fa', label: 'User' }, +}; + +// ============================================================================= +// Types +// ============================================================================= + +interface FlatRow { + key: string; + category: string; + label: string; + description: string; + tokens: number; + turnIndex: number; + toolUseId?: string; + isError?: boolean; + copyPath?: string; + navigationType: 'tool' | 'turn' | 'user-group'; +} + +interface FlatInjectionListProps { + injections: ContextInjection[]; + onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; + onNavigateToUserGroup?: (turnIndex: number) => void; +} + +// ============================================================================= +// Helpers +// ============================================================================= + +function flattenInjections(injections: ContextInjection[]): FlatRow[] { + const rows: FlatRow[] = []; + + for (const inj of injections) { + switch (inj.category) { + case 'tool-output': + if (inj.toolBreakdown.length > 0) { + for (const tool of inj.toolBreakdown) { + rows.push({ + key: `${inj.id}-${tool.toolName}-${tool.toolUseId ?? rows.length}`, + category: 'tool-output', + label: tool.toolName, + description: `Turn ${inj.turnIndex + 1}`, + tokens: tool.tokenCount, + turnIndex: inj.turnIndex, + toolUseId: tool.toolUseId, + isError: tool.isError, + navigationType: tool.toolUseId ? 'tool' : 'turn', + }); + } + } else { + rows.push({ + key: inj.id, + category: 'tool-output', + label: `${inj.toolCount} tool${inj.toolCount !== 1 ? 's' : ''}`, + description: `Turn ${inj.turnIndex + 1}`, + tokens: inj.estimatedTokens, + turnIndex: inj.turnIndex, + navigationType: 'turn', + }); + } + break; + + case 'thinking-text': + for (const item of inj.breakdown) { + rows.push({ + key: `${inj.id}-${item.type}`, + category: 'thinking-text', + label: item.type === 'thinking' ? 'Thinking' : 'Text', + description: `Turn ${inj.turnIndex + 1}`, + tokens: item.tokenCount, + turnIndex: inj.turnIndex, + navigationType: 'turn', + }); + } + break; + + case 'task-coordination': + for (const item of inj.breakdown) { + rows.push({ + key: `${inj.id}-${item.type}-${item.label}`, + category: 'task-coordination', + label: item.toolName ?? item.label, + description: `Turn ${inj.turnIndex + 1}`, + tokens: item.tokenCount, + turnIndex: inj.turnIndex, + navigationType: 'turn', + }); + } + break; + + case 'claude-md': + rows.push({ + key: inj.id, + category: 'claude-md', + label: inj.displayName || inj.path, + description: '', + tokens: inj.estimatedTokens, + turnIndex: parseTurnIndex(inj.firstSeenInGroup), + copyPath: inj.path, + navigationType: 'turn', + }); + break; + + case 'mentioned-file': + rows.push({ + key: inj.id, + category: 'mentioned-file', + label: inj.displayName, + description: '', + tokens: inj.estimatedTokens, + turnIndex: inj.firstSeenTurnIndex, + copyPath: inj.path, + navigationType: 'turn', + }); + break; + + case 'user-message': + rows.push({ + key: inj.id, + category: 'user-message', + label: inj.textPreview, + description: '', + tokens: inj.estimatedTokens, + turnIndex: inj.turnIndex, + navigationType: 'user-group', + }); + break; + } + } + + return rows.sort((a, b) => b.tokens - a.tokens); +} + +// ============================================================================= +// Component +// ============================================================================= + +export const FlatInjectionList = ({ + injections, + onNavigateToTurn, + onNavigateToTool, + onNavigateToUserGroup, +}: Readonly): React.ReactElement => { + const rows = useMemo(() => flattenInjections(injections), [injections]); + + return ( +
+ {rows.map((row) => { + const categoryInfo = CATEGORY_COLORS[row.category] ?? { + bg: 'rgba(161, 161, 170, 0.15)', + text: '#a1a1aa', + label: row.category, + }; + + const handleClick = (): void => { + if (row.turnIndex < 0) return; + if (row.navigationType === 'tool' && row.toolUseId && onNavigateToTool) { + onNavigateToTool(row.turnIndex, row.toolUseId); + } else if (row.navigationType === 'user-group' && onNavigateToUserGroup) { + onNavigateToUserGroup(row.turnIndex); + } else if (onNavigateToTurn) { + onNavigateToTurn(row.turnIndex); + } + }; + + const displayText = row.description + ? `${row.label} \u2014 ${row.description}` + : row.label; + + return ( +
+ + {/* Copy path button for CLAUDE.md and File items */} + {row.copyPath && ( + + + + )} +
+ ); + })} +
+ ); +}; diff --git a/src/renderer/components/chat/SessionContextPanel/index.tsx b/src/renderer/components/chat/SessionContextPanel/index.tsx index 28c540e2..19f8cc79 100644 --- a/src/renderer/components/chat/SessionContextPanel/index.tsx +++ b/src/renderer/components/chat/SessionContextPanel/index.tsx @@ -5,9 +5,15 @@ import React, { useMemo, useState } from 'react'; -import { COLOR_BORDER, COLOR_SURFACE, COLOR_TEXT_MUTED } from '@renderer/constants/cssVariables'; +import { + COLOR_BORDER, + COLOR_SURFACE, + COLOR_SURFACE_OVERLAY, + COLOR_TEXT_MUTED, +} from '@renderer/constants/cssVariables'; import { ClaudeMdFilesSection } from './components/ClaudeMdFilesSection'; +import { FlatInjectionList } from './components/FlatInjectionList'; import { MentionedFilesSection } from './components/MentionedFilesSection'; import { RankedInjectionList } from './components/RankedInjectionList'; import { SessionContextHeader } from './components/SessionContextHeader'; @@ -46,8 +52,10 @@ export const SessionContextPanel = ({ selectedPhase, onPhaseChange, }: Readonly): React.ReactElement => { - // View mode: category sections or flat ranked list + // View mode: category sections or ranked list const [viewMode, setViewMode] = useState('category'); + // Flat sub-toggle within "By Size" view + const [flatMode, setFlatMode] = useState(false); // Track which main sections are expanded const [expandedSections, setExpandedSections] = useState>( @@ -252,12 +260,46 @@ export const SessionContextPanel = ({ /> ) : ( - + <> + {/* Grouped / Flat sub-toggle */} +
+ + +
+ {flatMode ? ( + + ) : ( + + )} + )} diff --git a/src/renderer/utils/contextTracker.ts b/src/renderer/utils/contextTracker.ts index 021afb5a..78eb4986 100644 --- a/src/renderer/utils/contextTracker.ts +++ b/src/renderer/utils/contextTracker.ts @@ -836,13 +836,13 @@ function computeContextStats(params: ComputeContextStatsParams): ContextStats { newCounts.mentionedFiles++; break; case 'tool-output': - newCounts.toolOutputs++; + newCounts.toolOutputs += injection.toolCount; break; case 'thinking-text': newCounts.thinkingText++; break; case 'task-coordination': - newCounts.taskCoordination++; + newCounts.taskCoordination += injection.breakdown.length; break; case 'user-message': newCounts.userMessages++;