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 && (
|
{newToolOutputInjections.length > 0 && (
|
||||||
<PopoverSection
|
<PopoverSection
|
||||||
title="Tool Outputs"
|
title="Tool Outputs"
|
||||||
count={newToolOutputInjections.length}
|
count={newToolOutputInjections.reduce(
|
||||||
|
(sum, inj) => sum + inj.toolBreakdown.length,
|
||||||
|
0
|
||||||
|
)}
|
||||||
tokenCount={toolOutputTokens}
|
tokenCount={toolOutputTokens}
|
||||||
>
|
>
|
||||||
{newToolOutputInjections.map((injection) =>
|
{newToolOutputInjections.map((injection) =>
|
||||||
|
|
@ -501,7 +504,10 @@ export const ContextBadge = ({
|
||||||
{newTaskCoordinationInjections.length > 0 && (
|
{newTaskCoordinationInjections.length > 0 && (
|
||||||
<PopoverSection
|
<PopoverSection
|
||||||
title="Task Coordination"
|
title="Task Coordination"
|
||||||
count={newTaskCoordinationInjections.length}
|
count={newTaskCoordinationInjections.reduce(
|
||||||
|
(sum, inj) => sum + inj.breakdown.length,
|
||||||
|
0
|
||||||
|
)}
|
||||||
tokenCount={taskCoordinationTokens}
|
tokenCount={taskCoordinationTokens}
|
||||||
>
|
>
|
||||||
{newTaskCoordinationInjections.map((injection) =>
|
{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 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 { ClaudeMdFilesSection } from './components/ClaudeMdFilesSection';
|
||||||
|
import { FlatInjectionList } from './components/FlatInjectionList';
|
||||||
import { MentionedFilesSection } from './components/MentionedFilesSection';
|
import { MentionedFilesSection } from './components/MentionedFilesSection';
|
||||||
import { RankedInjectionList } from './components/RankedInjectionList';
|
import { RankedInjectionList } from './components/RankedInjectionList';
|
||||||
import { SessionContextHeader } from './components/SessionContextHeader';
|
import { SessionContextHeader } from './components/SessionContextHeader';
|
||||||
|
|
@ -46,8 +52,10 @@ export const SessionContextPanel = ({
|
||||||
selectedPhase,
|
selectedPhase,
|
||||||
onPhaseChange,
|
onPhaseChange,
|
||||||
}: Readonly<SessionContextPanelProps>): React.ReactElement => {
|
}: 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');
|
const [viewMode, setViewMode] = useState<ContextViewMode>('category');
|
||||||
|
// Flat sub-toggle within "By Size" view
|
||||||
|
const [flatMode, setFlatMode] = useState(false);
|
||||||
|
|
||||||
// Track which main sections are expanded
|
// Track which main sections are expanded
|
||||||
const [expandedSections, setExpandedSections] = useState<Set<SectionType>>(
|
const [expandedSections, setExpandedSections] = useState<Set<SectionType>>(
|
||||||
|
|
@ -252,12 +260,46 @@ export const SessionContextPanel = ({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<RankedInjectionList
|
<>
|
||||||
injections={injections}
|
{/* Grouped / Flat sub-toggle */}
|
||||||
onNavigateToTurn={onNavigateToTurn}
|
<div className="flex items-center gap-1 pb-1">
|
||||||
onNavigateToTool={onNavigateToTool}
|
<button
|
||||||
onNavigateToUserGroup={onNavigateToUserGroup}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -836,13 +836,13 @@ function computeContextStats(params: ComputeContextStatsParams): ContextStats {
|
||||||
newCounts.mentionedFiles++;
|
newCounts.mentionedFiles++;
|
||||||
break;
|
break;
|
||||||
case 'tool-output':
|
case 'tool-output':
|
||||||
newCounts.toolOutputs++;
|
newCounts.toolOutputs += injection.toolCount;
|
||||||
break;
|
break;
|
||||||
case 'thinking-text':
|
case 'thinking-text':
|
||||||
newCounts.thinkingText++;
|
newCounts.thinkingText++;
|
||||||
break;
|
break;
|
||||||
case 'task-coordination':
|
case 'task-coordination':
|
||||||
newCounts.taskCoordination++;
|
newCounts.taskCoordination += injection.breakdown.length;
|
||||||
break;
|
break;
|
||||||
case 'user-message':
|
case 'user-message':
|
||||||
newCounts.userMessages++;
|
newCounts.userMessages++;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue