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:
parent
7a264a882c
commit
2fcf111f77
4 changed files with 310 additions and 12 deletions
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
|
|
|
|||
Loading…
Reference in a new issue