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.
This commit is contained in:
matt 2026-02-20 13:27:34 +09:00
parent 7a264a882c
commit 2fcf111f77
4 changed files with 310 additions and 12 deletions

View file

@ -478,7 +478,10 @@ export const ContextBadge = ({
{newToolOutputInjections.length > 0 && (
<PopoverSection
title="Tool Outputs"
count={newToolOutputInjections.length}
count={newToolOutputInjections.reduce(
(sum, inj) => sum + inj.toolBreakdown.length,
0
)}
tokenCount={toolOutputTokens}
>
{newToolOutputInjections.map((injection) =>
@ -501,7 +504,10 @@ export const ContextBadge = ({
{newTaskCoordinationInjections.length > 0 && (
<PopoverSection
title="Task Coordination"
count={newTaskCoordinationInjections.length}
count={newTaskCoordinationInjections.reduce(
(sum, inj) => sum + inj.breakdown.length,
0
)}
tokenCount={taskCoordinationTokens}
>
{newTaskCoordinationInjections.map((injection) =>

View file

@ -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<string, { bg: string; text: string; label: string }> = {
'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<FlatInjectionListProps>): React.ReactElement => {
const rows = useMemo(() => flattenInjections(injections), [injections]);
return (
<div className="space-y-0.5">
{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 (
<div key={row.key} className="flex items-center gap-0.5">
<button
onClick={handleClick}
className="flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
>
{/* Category pill */}
<span
className="shrink-0 rounded px-1.5 py-0.5 text-[9px] font-medium"
style={{ backgroundColor: categoryInfo.bg, color: categoryInfo.text }}
>
{categoryInfo.label}
</span>
{/* Description */}
<span
className="min-w-0 flex-1 truncate text-xs"
style={{ color: COLOR_TEXT_SECONDARY }}
>
{displayText}
</span>
{/* Error badge */}
{row.isError && (
<span
className="shrink-0 rounded px-1 py-0.5"
style={{
backgroundColor: 'rgba(239, 68, 68, 0.15)',
color: '#ef4444',
fontSize: '10px',
}}
>
error
</span>
)}
{/* Token count */}
<span
className="shrink-0 text-xs font-medium tabular-nums"
style={{ color: COLOR_TEXT_MUTED }}
>
{formatTokens(row.tokens)}
</span>
</button>
{/* Copy path button for CLAUDE.md and File items */}
{row.copyPath && (
<span className="shrink-0">
<CopyButton text={row.copyPath} inline />
</span>
)}
</div>
);
})}
</div>
);
};

View file

@ -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<SessionContextPanelProps>): React.ReactElement => {
// View mode: category sections or flat ranked list
// View mode: category sections or ranked list
const [viewMode, setViewMode] = useState<ContextViewMode>('category');
// Flat sub-toggle within "By Size" view
const [flatMode, setFlatMode] = useState(false);
// Track which main sections are expanded
const [expandedSections, setExpandedSections] = useState<Set<SectionType>>(
@ -252,12 +260,46 @@ export const SessionContextPanel = ({
/>
</>
) : (
<RankedInjectionList
injections={injections}
onNavigateToTurn={onNavigateToTurn}
onNavigateToTool={onNavigateToTool}
onNavigateToUserGroup={onNavigateToUserGroup}
/>
<>
{/* Grouped / Flat sub-toggle */}
<div className="flex items-center gap-1 pb-1">
<button
onClick={() => setFlatMode(false)}
className="rounded px-1.5 py-0.5 text-[10px] transition-colors"
style={{
backgroundColor: !flatMode ? 'rgba(99, 102, 241, 0.2)' : COLOR_SURFACE_OVERLAY,
color: !flatMode ? '#818cf8' : COLOR_TEXT_MUTED,
}}
>
Grouped
</button>
<button
onClick={() => setFlatMode(true)}
className="rounded px-1.5 py-0.5 text-[10px] transition-colors"
style={{
backgroundColor: flatMode ? 'rgba(99, 102, 241, 0.2)' : COLOR_SURFACE_OVERLAY,
color: flatMode ? '#818cf8' : COLOR_TEXT_MUTED,
}}
>
Flat
</button>
</div>
{flatMode ? (
<FlatInjectionList
injections={injections}
onNavigateToTurn={onNavigateToTurn}
onNavigateToTool={onNavigateToTool}
onNavigateToUserGroup={onNavigateToUserGroup}
/>
) : (
<RankedInjectionList
injections={injections}
onNavigateToTurn={onNavigateToTurn}
onNavigateToTool={onNavigateToTool}
onNavigateToUserGroup={onNavigateToUserGroup}
/>
)}
</>
)}
</div>
</div>

View file

@ -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++;