- Fix React hooks violations: ref updates during render (useDraftPersistence, useChipDraftPersistence, useAttachments), setState in effects across 15+ components, useCallback self-reference TDZ in useResizableColumns - Fix TypeScript lint: remove unnecessary type assertions, replace inline import() annotations with direct imports, remove unused variables/imports - Fix SonarJS issues: prefer-regexp-exec, slow-regex in SubagentResolver, no-misleading-array-reverse in TeamProvisioningService, use-type-alias in ClaudeLogsSection, variable shadowing in ChangeExtractorService - Fix accessibility: associate labels with controls in filter popovers - Fix template expression safety: wrap unknown errors with String() - Fix flaky FileWatcher test: floor instanceCreatedAt to second granularity to match filesystem birthtimeMs resolution on Linux - Replace TODO comments with NOTE where features are intentionally disabled - Remove unused leadContextByTeam from TeamDetailView store selector 62 files changed across main process, renderer, shared types, and hooks. All 1646 tests pass, typecheck clean, 0 lint errors.
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
/**
|
|
* SessionContextPanel - Panel showing all context injections for a session.
|
|
* Displays CLAUDE.md files, mentioned files, and tool outputs in collapsible sections.
|
|
*/
|
|
|
|
import React, { useMemo, useState } from 'react';
|
|
|
|
import {
|
|
COLOR_BORDER,
|
|
COLOR_SURFACE,
|
|
COLOR_SURFACE_OVERLAY,
|
|
COLOR_TEXT_MUTED,
|
|
} from '@renderer/constants/cssVariables';
|
|
import { sumContextInjectionTokens } from '@renderer/utils/contextMath';
|
|
|
|
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';
|
|
import { TaskCoordinationSection } from './components/TaskCoordinationSection';
|
|
import { ThinkingTextSection } from './components/ThinkingTextSection';
|
|
import { ToolOutputsSection } from './components/ToolOutputsSection';
|
|
import { UserMessagesSection } from './components/UserMessagesSection';
|
|
import {
|
|
SECTION_CLAUDE_MD,
|
|
SECTION_MENTIONED_FILES,
|
|
SECTION_TASK_COORDINATION,
|
|
SECTION_THINKING_TEXT,
|
|
SECTION_TOOL_OUTPUTS,
|
|
SECTION_USER_MESSAGES,
|
|
} from './types';
|
|
|
|
import type { ContextViewMode, SectionType, SessionContextPanelProps } from './types';
|
|
import type {
|
|
ClaudeMdContextInjection,
|
|
MentionedFileInjection,
|
|
TaskCoordinationInjection,
|
|
ThinkingTextInjection,
|
|
ToolOutputInjection,
|
|
UserMessageInjection,
|
|
} from '@renderer/types/contextInjection';
|
|
|
|
export const SessionContextPanel = ({
|
|
injections,
|
|
onClose,
|
|
projectRoot,
|
|
onNavigateToTurn,
|
|
onNavigateToTool,
|
|
onNavigateToUserGroup,
|
|
totalSessionTokens,
|
|
sessionMetrics,
|
|
subagentCostUsd,
|
|
onViewReport,
|
|
phaseInfo,
|
|
selectedPhase,
|
|
onPhaseChange,
|
|
}: Readonly<SessionContextPanelProps>): React.ReactElement => {
|
|
// 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>>(
|
|
new Set([
|
|
SECTION_USER_MESSAGES,
|
|
SECTION_CLAUDE_MD,
|
|
SECTION_MENTIONED_FILES,
|
|
SECTION_TOOL_OUTPUTS,
|
|
SECTION_TASK_COORDINATION,
|
|
SECTION_THINKING_TEXT,
|
|
])
|
|
);
|
|
|
|
// Separate injections by category
|
|
const {
|
|
claudeMdInjections,
|
|
mentionedFileInjections,
|
|
toolOutputInjections,
|
|
thinkingTextInjections,
|
|
taskCoordinationInjections,
|
|
userMessageInjections,
|
|
} = useMemo(() => {
|
|
const claudeMd: ClaudeMdContextInjection[] = [];
|
|
const mentionedFiles: MentionedFileInjection[] = [];
|
|
const toolOutputs: ToolOutputInjection[] = [];
|
|
const thinkingText: ThinkingTextInjection[] = [];
|
|
const taskCoordination: TaskCoordinationInjection[] = [];
|
|
const userMessages: UserMessageInjection[] = [];
|
|
|
|
for (const injection of injections) {
|
|
switch (injection.category) {
|
|
case 'claude-md':
|
|
claudeMd.push(injection);
|
|
break;
|
|
case 'mentioned-file':
|
|
mentionedFiles.push(injection);
|
|
break;
|
|
case 'tool-output':
|
|
toolOutputs.push(injection);
|
|
break;
|
|
case 'thinking-text':
|
|
thinkingText.push(injection);
|
|
break;
|
|
case 'task-coordination':
|
|
taskCoordination.push(injection);
|
|
break;
|
|
case 'user-message':
|
|
userMessages.push(injection);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Sort mentioned files and tool outputs by tokens descending
|
|
mentionedFiles.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
toolOutputs.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
// Sort task coordination by tokens descending
|
|
taskCoordination.sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
|
// Sort thinking-text by turn index ascending
|
|
thinkingText.sort((a, b) => a.turnIndex - b.turnIndex);
|
|
// Sort user messages by turn index ascending
|
|
userMessages.sort((a, b) => a.turnIndex - b.turnIndex);
|
|
|
|
return {
|
|
claudeMdInjections: claudeMd,
|
|
mentionedFileInjections: mentionedFiles,
|
|
toolOutputInjections: toolOutputs,
|
|
thinkingTextInjections: thinkingText,
|
|
taskCoordinationInjections: taskCoordination,
|
|
userMessageInjections: userMessages,
|
|
};
|
|
}, [injections]);
|
|
|
|
// Calculate total tokens
|
|
const totalTokens = useMemo(() => sumContextInjectionTokens(injections), [injections]);
|
|
|
|
// Section token counts
|
|
const claudeMdTokens = useMemo(
|
|
() => claudeMdInjections.reduce((sum, inj) => sum + inj.estimatedTokens, 0),
|
|
[claudeMdInjections]
|
|
);
|
|
|
|
const mentionedFilesTokens = useMemo(
|
|
() => mentionedFileInjections.reduce((sum, inj) => sum + inj.estimatedTokens, 0),
|
|
[mentionedFileInjections]
|
|
);
|
|
|
|
const toolOutputsTokens = useMemo(
|
|
() => toolOutputInjections.reduce((sum, inj) => sum + inj.estimatedTokens, 0),
|
|
[toolOutputInjections]
|
|
);
|
|
|
|
const thinkingTextTokens = useMemo(
|
|
() => thinkingTextInjections.reduce((sum, inj) => sum + inj.estimatedTokens, 0),
|
|
[thinkingTextInjections]
|
|
);
|
|
|
|
const taskCoordinationTokens = useMemo(
|
|
() => taskCoordinationInjections.reduce((sum, inj) => sum + inj.estimatedTokens, 0),
|
|
[taskCoordinationInjections]
|
|
);
|
|
|
|
const userMessagesTokens = useMemo(
|
|
() => userMessageInjections.reduce((sum, inj) => sum + inj.estimatedTokens, 0),
|
|
[userMessageInjections]
|
|
);
|
|
|
|
// Toggle section expansion
|
|
const toggleSection = (section: SectionType): void => {
|
|
setExpandedSections((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(section)) {
|
|
next.delete(section);
|
|
} else {
|
|
next.add(section);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="flex h-full flex-col"
|
|
style={{
|
|
backgroundColor: COLOR_SURFACE,
|
|
borderLeft: `1px solid ${COLOR_BORDER}`,
|
|
}}
|
|
>
|
|
<SessionContextHeader
|
|
injectionCount={injections.length}
|
|
totalTokens={totalTokens}
|
|
totalSessionTokens={totalSessionTokens}
|
|
sessionMetrics={sessionMetrics}
|
|
subagentCostUsd={subagentCostUsd}
|
|
onClose={onClose}
|
|
onViewReport={onViewReport}
|
|
phaseInfo={phaseInfo}
|
|
selectedPhase={selectedPhase}
|
|
onPhaseChange={onPhaseChange}
|
|
viewMode={viewMode}
|
|
onViewModeChange={setViewMode}
|
|
/>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 space-y-2 overflow-y-auto p-3">
|
|
{injections.length === 0 ? (
|
|
<div
|
|
className="flex h-full items-center justify-center text-sm"
|
|
style={{ color: COLOR_TEXT_MUTED }}
|
|
>
|
|
No context injections detected in this session
|
|
</div>
|
|
) : viewMode === 'category' ? (
|
|
<>
|
|
<UserMessagesSection
|
|
injections={userMessageInjections}
|
|
tokenCount={userMessagesTokens}
|
|
isExpanded={expandedSections.has(SECTION_USER_MESSAGES)}
|
|
onToggle={() => toggleSection(SECTION_USER_MESSAGES)}
|
|
onNavigateToTurn={onNavigateToTurn}
|
|
/>
|
|
|
|
<ClaudeMdFilesSection
|
|
injections={claudeMdInjections}
|
|
tokenCount={claudeMdTokens}
|
|
isExpanded={expandedSections.has(SECTION_CLAUDE_MD)}
|
|
onToggle={() => toggleSection(SECTION_CLAUDE_MD)}
|
|
projectRoot={projectRoot ?? ''}
|
|
onNavigateToTurn={onNavigateToTurn}
|
|
/>
|
|
|
|
<MentionedFilesSection
|
|
injections={mentionedFileInjections}
|
|
tokenCount={mentionedFilesTokens}
|
|
isExpanded={expandedSections.has(SECTION_MENTIONED_FILES)}
|
|
onToggle={() => toggleSection(SECTION_MENTIONED_FILES)}
|
|
projectRoot={projectRoot}
|
|
onNavigateToTurn={onNavigateToTurn}
|
|
/>
|
|
|
|
<ToolOutputsSection
|
|
injections={toolOutputInjections}
|
|
tokenCount={toolOutputsTokens}
|
|
isExpanded={expandedSections.has(SECTION_TOOL_OUTPUTS)}
|
|
onToggle={() => toggleSection(SECTION_TOOL_OUTPUTS)}
|
|
onNavigateToTurn={onNavigateToTurn}
|
|
/>
|
|
|
|
<TaskCoordinationSection
|
|
injections={taskCoordinationInjections}
|
|
tokenCount={taskCoordinationTokens}
|
|
isExpanded={expandedSections.has(SECTION_TASK_COORDINATION)}
|
|
onToggle={() => toggleSection(SECTION_TASK_COORDINATION)}
|
|
onNavigateToTurn={onNavigateToTurn}
|
|
/>
|
|
|
|
<ThinkingTextSection
|
|
injections={thinkingTextInjections}
|
|
tokenCount={thinkingTextTokens}
|
|
isExpanded={expandedSections.has(SECTION_THINKING_TEXT)}
|
|
onToggle={() => toggleSection(SECTION_THINKING_TEXT)}
|
|
onNavigateToTurn={onNavigateToTurn}
|
|
/>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* 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>
|
|
);
|
|
};
|