agent-ecosystem/src/renderer/components/chat/SessionContextPanel/index.tsx
iliya 2ceed41e00 fix: resolve all CI lint errors and flaky test
- 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.
2026-03-05 21:09:45 +02:00

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>
);
};