From d8ce1ec900be61daf0ed670e9401c1501210dad0 Mon Sep 17 00:00:00 2001 From: axisrow Date: Wed, 7 Oct 2026 12:34:12 +0800 Subject: [PATCH 1/4] feat(tracker): use real group.id for injection ids and navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #53. After #52 two AI groups of one turn share turnIndex (compact or system boundaries tear a turn; aiCount fallback collides too), so the synthetic "ai-${turnIndex}" id collided: duplicate injection ids and wrong-group navigation. Trackers now carry the group's real id (= stable chunk id, unique across re-parses): - contextTracker: turnGroupId = aiGroup.id; injection id generators take the group id instead of turnIndex (tool-output-, loop-, wait-loop-, thinking-text-, task-coord-, user-msg-); reread id follows. - claudeMdTracker: same turnGroupId change; firstSeenTurnIndex (display-only "@Turn N") added next to firstSeenInGroup. - ClaudeMdInjection gains firstSeenTurnIndex so labels survive real uuid ids. Red test (contextTracker.test.ts): two AI groups sharing turnIndex 0 must keep disjoint injection ids, their own aiGroupId, and claude-md display metadata — failed with 'ai-0' expected 'ai-xxx'. Co-Authored-By: Claude Code --- src/renderer/types/claudeMd.ts | 2 + src/renderer/utils/claudeMdTracker.ts | 32 ++++++++--- src/renderer/utils/contextTracker.ts | 62 +++++++++++++--------- test/renderer/utils/contextTracker.test.ts | 44 +++++++++++++++ 4 files changed, 109 insertions(+), 31 deletions(-) diff --git a/src/renderer/types/claudeMd.ts b/src/renderer/types/claudeMd.ts index 9af32b7d..e4d27edf 100644 --- a/src/renderer/types/claudeMd.ts +++ b/src/renderer/types/claudeMd.ts @@ -48,6 +48,8 @@ export interface ClaudeMdInjection { estimatedTokens: number; /** ID of the AI group where this injection was first seen */ firstSeenInGroup: string; + /** turnIndex (0-based) of that group — display label only ("@Turn N"), not an identity */ + firstSeenTurnIndex: number; } // ============================================================================= diff --git a/src/renderer/utils/claudeMdTracker.ts b/src/renderer/utils/claudeMdTracker.ts index e9a29b9b..549fce91 100644 --- a/src/renderer/utils/claudeMdTracker.ts +++ b/src/renderer/utils/claudeMdTracker.ts @@ -315,7 +315,8 @@ export function detectClaudeMdFromFilePath(filePath: string, projectRoot: string export function createGlobalInjections( projectRoot: string, aiGroupId: string, - tokenData?: Record + tokenData?: Record, + turnIndex: number = 0 ): ClaudeMdInjection[] { const injections: ClaudeMdInjection[] = []; @@ -337,6 +338,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: enterpriseTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -353,6 +355,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: userTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -370,6 +373,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: projectTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } // Also add the .claude folder variant @@ -383,6 +387,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: projectAltTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -398,6 +403,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: projectRulesTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -413,6 +419,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: projectLocalTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -428,6 +435,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: userRulesTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -444,6 +452,7 @@ export function createGlobalInjections( isGlobal: true, estimatedTokens: autoMemoryTokens, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }); } @@ -453,7 +462,11 @@ export function createGlobalInjections( /** * Create an injection entry for a directory-specific CLAUDE.md. */ -function createDirectoryInjection(path: string, aiGroupId: string): ClaudeMdInjection { +function createDirectoryInjection( + path: string, + aiGroupId: string, + turnIndex: number +): ClaudeMdInjection { return { id: generateInjectionId(path), path, @@ -462,6 +475,7 @@ function createDirectoryInjection(path: string, aiGroupId: string): ClaudeMdInje isGlobal: false, estimatedTokens: DEFAULT_ESTIMATED_TOKENS, firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }; } @@ -508,10 +522,16 @@ function computeClaudeMdStats(params: ComputeClaudeMdStatsParams): ComputeClaude const newInjections: ClaudeMdInjection[] = []; // For the first group, add global injections - // Use "ai-N" format for firstSeenInGroup to enable turn navigation in SessionClaudeMdPanel - const turnGroupId = `ai-${aiGroup.turnIndex}`; + // firstSeenInGroup carries the real group id (navigation identity); display + // "Turn N" labels come from turnIndex fields + const turnGroupId = aiGroup.id; if (isFirstGroup) { - const globalInjections = createGlobalInjections(projectRoot, turnGroupId, tokenData); + const globalInjections = createGlobalInjections( + projectRoot, + turnGroupId, + tokenData, + aiGroup.turnIndex + ); for (const injection of globalInjections) { if (!previousPaths.has(injection.path)) { newInjections.push(injection); @@ -564,7 +584,7 @@ function computeClaudeMdStats(params: ComputeClaudeMdStatsParams): ComputeClaude } // Create directory injection - const injection = createDirectoryInjection(claudeMdPath, turnGroupId); + const injection = createDirectoryInjection(claudeMdPath, turnGroupId, aiGroup.turnIndex); newInjections.push(injection); previousPaths.add(claudeMdPath); } diff --git a/src/renderer/utils/contextTracker.ts b/src/renderer/utils/contextTracker.ts index ef9e8822..d77694be 100644 --- a/src/renderer/utils/contextTracker.ts +++ b/src/renderer/utils/contextTracker.ts @@ -106,45 +106,46 @@ function generateMentionedFileId(path: string): string { } /** - * Generate a unique ID for a tool output injection. + * Generate a unique ID for a tool output injection. Keyed by the group's real + * id (stable chunk id) — two AI groups of one turn share turnIndex but never id. */ -function generateToolOutputId(turnIndex: number): string { - return `tool-output-ai-${turnIndex}`; +function generateToolOutputId(groupId: string): string { + return `tool-output-${groupId}`; } /** * Generate unique ID for thinking-text injection. */ -function generateThinkingTextId(turnIndex: number): string { - return `thinking-text-ai-${turnIndex}`; +function generateThinkingTextId(groupId: string): string { + return `thinking-text-${groupId}`; } /** * Generate unique ID for task coordination injection. */ -function generateTaskCoordinationId(turnIndex: number): string { - return `task-coord-ai-${turnIndex}`; +function generateTaskCoordinationId(groupId: string): string { + return `task-coord-${groupId}`; } /** * Generate unique ID for user message injection. */ -function generateUserMessageId(turnIndex: number): string { - return `user-msg-ai-${turnIndex}`; +function generateUserMessageId(groupId: string): string { + return `user-msg-${groupId}`; } /** * Generate unique ID for loop injection. */ -function generateLoopId(turnIndex: number): string { - return `loop-ai-${turnIndex}`; +function generateLoopId(groupId: string): string { + return `loop-${groupId}`; } /** * Generate unique ID for wait-loop injection. */ -function generateWaitLoopId(turnIndex: number): string { - return `wait-loop-ai-${turnIndex}`; +function generateWaitLoopId(groupId: string): string { + return `wait-loop-${groupId}`; } /** @@ -338,7 +339,7 @@ function aggregateToolOutputs( let loop: LoopInjection | null = null; if (loopTokens > 0) { loop = { - id: generateLoopId(turnIndex), + id: generateLoopId(aiGroupId), category: 'loop', turnIndex, aiGroupId, @@ -354,7 +355,7 @@ function aggregateToolOutputs( return { toolOutput: { - id: generateToolOutputId(turnIndex), + id: generateToolOutputId(aiGroupId), category: 'tool-output', turnIndex, aiGroupId, @@ -486,7 +487,7 @@ function aggregateWaitLoopRounds( if (rounds.length < WAIT_LOOP_MIN_TICKS) return null; return { - id: generateWaitLoopId(turnIndex), + id: generateWaitLoopId(aiGroupId), category: 'wait-loop', turnIndex, aiGroupId, @@ -599,7 +600,7 @@ function aggregateTaskCoordination( } return { - id: generateTaskCoordinationId(turnIndex), + id: generateTaskCoordinationId(aiGroupId), category: 'task-coordination', turnIndex, aiGroupId, @@ -633,7 +634,7 @@ function createUserMessageInjection( const textPreview = text.length > 80 ? text.slice(0, 80) + '…' : text; return { - id: generateUserMessageId(turnIndex), + id: generateUserMessageId(aiGroupId), category: 'user-message', turnIndex, aiGroupId, @@ -687,7 +688,7 @@ function aggregateThinkingText( } return { - id: generateThinkingTextId(turnIndex), + id: generateThinkingTextId(aiGroupId), category: 'thinking-text', turnIndex, aiGroupId, @@ -865,7 +866,11 @@ function normalizeForComparison(input: string): string { /** * Create a directory injection for a CLAUDE.md file discovered via file paths. */ -function createDirectoryInjection(path: string, aiGroupId: string): ClaudeMdInjection { +function createDirectoryInjection( + path: string, + aiGroupId: string, + turnIndex: number +): ClaudeMdInjection { return { id: generateInjectionId(path), path, @@ -874,6 +879,7 @@ function createDirectoryInjection(path: string, aiGroupId: string): ClaudeMdInje isGlobal: false, estimatedTokens: 500, // Default estimated tokens firstSeenInGroup: aiGroupId, + firstSeenTurnIndex: turnIndex, }; } @@ -899,12 +905,18 @@ function computeContextStats(params: ComputeContextStatsParams): ComputeContextS const newInjections: ContextInjection[] = []; - // Use "ai-N" format for firstSeenInGroup to enable turn navigation - const turnGroupId = `ai-${aiGroup.turnIndex}`; + // firstSeenInGroup / aiGroupId carry the real group id — stable chunk id, + // unique even when two groups share a turnIndex (issue #53) + const turnGroupId = aiGroup.id; // a) For FIRST group only: Add CLAUDE.md global injections if (isFirstGroup) { - const globalInjections = createGlobalInjections(projectRoot, turnGroupId, claudeMdTokenData); + const globalInjections = createGlobalInjections( + projectRoot, + turnGroupId, + claudeMdTokenData, + aiGroup.turnIndex + ); for (const injection of globalInjections) { if (!previousPaths.has(injection.path)) { newInjections.push(wrapClaudeMdInjection(injection)); @@ -966,13 +978,13 @@ function computeContextStats(params: ComputeContextStatsParams): ComputeContextS continue; } // Use validated token count from directoryTokenData - const injection = createDirectoryInjection(claudeMdPath, turnGroupId); + const injection = createDirectoryInjection(claudeMdPath, turnGroupId, aiGroup.turnIndex); injection.estimatedTokens = fileInfo.estimatedTokens; newInjections.push(wrapClaudeMdInjection(injection)); previousPaths.add(claudeMdPath); } else { // Fallback: if no directoryTokenData provided, create with default tokens (legacy behavior) - const injection = createDirectoryInjection(claudeMdPath, turnGroupId); + const injection = createDirectoryInjection(claudeMdPath, turnGroupId, aiGroup.turnIndex); newInjections.push(wrapClaudeMdInjection(injection)); previousPaths.add(claudeMdPath); } diff --git a/test/renderer/utils/contextTracker.test.ts b/test/renderer/utils/contextTracker.test.ts index 956c2e19..58ebed5c 100644 --- a/test/renderer/utils/contextTracker.test.ts +++ b/test/renderer/utils/contextTracker.test.ts @@ -464,3 +464,47 @@ describe('resolveContextTargetAiGroupId', () => { expect(resolveContextTargetAiGroupId(items, stats, null, null)).toBe('a'); }); }); + +describe('contextTracker injection identity on group.id (issue #53)', () => { + // post-#52 shape: a compact/system boundary tears one turn into two AI + // groups sharing the same turnIndex + it('two AI groups of one turn keep distinct injection ids and their own aiGroupId', () => { + const items = [ + userGroup(), + aiGroup('ai-xxx', 0, toolCall('t1', '/src/a.ts', 1000), [ + assistantMsg({ input: 1000, cacheRead: 5000, output: 100 }, ['t1']), + ]), + aiGroup('ai-yyy', 0, toolCall('t2', '/src/b.ts', 1000), [ + assistantMsg({ input: 1000, cacheRead: 5000, output: 100 }, ['t2']), + ]), + ]; + + const stats = lastStats(items); + const first = stats.get('ai-xxx')!; + const second = stats.get('ai-yyy')!; + + // every injection carrying aiGroupId points at its OWN group, not "ai-" + for (const [groupId, entry] of [ + ['ai-xxx', first], + ['ai-yyy', second], + ] as const) { + for (const inj of entry.newInjections) { + if ('aiGroupId' in inj) expect(inj.aiGroupId).toBe(groupId); + } + } + + // no shared ids between the two groups of the same turn + const firstIds = new Set(first.newInjections.map((inj) => inj.id)); + for (const inj of second.newInjections) { + expect(firstIds.has(inj.id)).toBe(false); + } + + // claude-md keeps its display metadata: real first-seen group + turn label + const claudeMd = first.newInjections.find((inj) => inj.category === 'claude-md'); + expect(claudeMd).toBeDefined(); + if (claudeMd?.category === 'claude-md') { + expect(claudeMd.firstSeenInGroup).toBe('ai-xxx'); + expect(claudeMd.firstSeenTurnIndex).toBe(0); + } + }); +}); From 9bff19a5eb7700955e2ccdf68d5c23cf700943d5 Mon Sep 17 00:00:00 2001 From: axisrow Date: Wed, 7 Oct 2026 12:35:29 +0800 Subject: [PATCH 2/4] refactor(panel): navigate by group.id instead of turn index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #53, panel half. SessionContextPanel callbacks (onNavigateToTurn / onNavigateToTool / onNavigateToUserGroup) take the AI group's id instead of turnIndex; ChatHistory resolves the target by group.id, so a click from the LATER group of a split turn lands in that group, not the first one sharing the index. - Sections/items pass injection.aiGroupId (or firstSeenInGroup for claude-md/mentioned-file); clickability gate is a non-empty id. - FlatInjectionList rows carry groupId; RankedInjectionList swaps getInjectionTurnIndex for getInjectionGroupId. - parseTurnIndex dropped; formatFirstSeen formats a turnIndex number — with uuid ids the "ai-N" parse was already unfixable. - DirectoryTree nodes carry firstSeenTurnIndex for the "@Turn N" label. Burn-header nav tests now assert the callback receives the group id string; ClaudeMdItem test covers a click with a uuid id (previously not clickable: parseTurnIndex returned -1). Co-Authored-By: Claude Code --- src/renderer/components/chat/ChatHistory.tsx | 46 +++++++++---------- .../DirectoryTree/DirectoryTreeNode.tsx | 14 +++--- .../DirectoryTree/buildDirectoryTree.ts | 1 + .../DirectoryTree/types.ts | 1 + .../components/ClaudeMdFilesSection.tsx | 2 +- .../components/ClaudeMdSection.tsx | 2 +- .../components/FlatInjectionList.tsx | 35 +++++++------- .../components/LoopSection.tsx | 8 ++-- .../components/MentionedFilesSection.tsx | 2 +- .../components/RankedInjectionList.tsx | 43 ++++++++--------- .../components/RereadSection.tsx | 4 +- .../components/TaskCoordinationSection.tsx | 2 +- .../components/ThinkingTextSection.tsx | 2 +- .../components/ToolOutputsSection.tsx | 2 +- .../components/UserMessagesSection.tsx | 2 +- .../components/WaitLoopSection.tsx | 4 +- .../items/ClaudeMdItem.tsx | 14 +++--- .../items/MentionedFileItem.tsx | 9 ++-- .../items/TaskCoordinationItem.tsx | 9 ++-- .../items/ThinkingTextItem.tsx | 9 ++-- .../items/ToolOutputItem.tsx | 9 ++-- .../items/UserMessageItem.tsx | 9 ++-- .../chat/SessionContextPanel/types.ts | 12 ++--- .../SessionContextPanel/utils/pathParsing.ts | 19 ++------ .../renderer/components/burnHeaderNav.test.ts | 43 ++++++++++++++--- 25 files changed, 161 insertions(+), 142 deletions(-) diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx index 670e8358..255f4d25 100644 --- a/src/renderer/components/chat/ChatHistory.tsx +++ b/src/renderer/components/chat/ChatHistory.tsx @@ -412,32 +412,32 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { [registerAIGroupRef] ); - // Handler to navigate to a specific turn (AI group) from CLAUDE.md panel + // Handler to navigate to a specific turn (AI group) by its id from context panel const handleNavigateToTurn = useCallback( - (turnIndex: number, opts?: { flashHeader?: boolean }) => { + (groupId: string, opts?: { flashHeader?: boolean }) => { if (!conversation) return; const targetItem = conversation.items.find( - (item) => item.type === 'ai' && item.group.turnIndex === turnIndex + (item) => item.type === 'ai' && item.group.id === groupId ); if (targetItem?.type !== 'ai') return; const run = async (): Promise => { - const groupId = targetItem.group.id; + const targetGroupId = targetItem.group.id; // navigating to a turn means reading it — expand the collapsed group - expandAIGroup(groupId); + expandAIGroup(targetGroupId); // the header with burn pills must be on screen — align the group top - await ensureGroupVisible(groupId, opts?.flashHeader ? 'start' : 'center'); - const element = aiGroupRefs.current.get(groupId); + await ensureGroupVisible(targetGroupId, opts?.flashHeader ? 'start' : 'center'); + const element = aiGroupRefs.current.get(targetGroupId); if (!element) return; element.scrollIntoView({ behavior: 'smooth', block: opts?.flashHeader ? 'start' : 'center', }); - setHighlightedGroupId(groupId); + setHighlightedGroupId(targetGroupId); setIsNavigationHighlight(true); if (opts?.flashHeader) { - setHeaderFlashGroupId(groupId); + setHeaderFlashGroupId(targetGroupId); if (headerFlashTimerRef.current) clearTimeout(headerFlashTimerRef.current); headerFlashTimerRef.current = setTimeout(() => { setHeaderFlashGroupId(null); @@ -458,12 +458,12 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { [conversation, ensureGroupVisible, expandAIGroup, setHighlightedGroupId] ); - // Handler to navigate to a user message group (preceding the AI group at turnIndex) + // Handler to navigate to the user message group preceding the AI group with this id const handleNavigateToUserGroup = useCallback( - (turnIndex: number) => { + (groupId: string) => { if (!conversation) return; const aiItemIndex = conversation.items.findIndex( - (item) => item.type === 'ai' && item.group.turnIndex === turnIndex + (item) => item.type === 'ai' && item.group.id === groupId ); if (aiItemIndex < 0) return; @@ -472,13 +472,13 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { if (prevItem?.type !== 'user') return; const run = async (): Promise => { - const groupId = prevItem.group.id; - await ensureGroupVisible(groupId); - const element = chatItemRefs.current.get(groupId); + const targetGroupId = prevItem.group.id; + await ensureGroupVisible(targetGroupId); + const element = chatItemRefs.current.get(targetGroupId); if (!element) return; element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - setHighlightedGroupId(groupId); + setHighlightedGroupId(targetGroupId); setIsNavigationHighlight(true); if (navigationHighlightTimerRef.current) { clearTimeout(navigationHighlightTimerRef.current); @@ -494,21 +494,21 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { [conversation, ensureGroupVisible, setHighlightedGroupId] ); - // Handler to navigate to a specific tool within a turn from context panel + // Handler to navigate to a specific tool within an AI group from context panel const handleNavigateToTool = useCallback( - (turnIndex: number, toolUseId: string) => { + (groupId: string, toolUseId: string) => { if (!conversation) return; const targetItem = conversation.items.find( - (item) => item.type === 'ai' && item.group.turnIndex === turnIndex + (item) => item.type === 'ai' && item.group.id === groupId ); if (targetItem?.type !== 'ai') return; const run = async (): Promise => { - const groupId = targetItem.group.id; - await ensureGroupVisible(groupId); + const targetGroupId = targetItem.group.id; + await ensureGroupVisible(targetGroupId); // Set group + tool highlight immediately - setHighlightedGroupId(groupId); + setHighlightedGroupId(targetGroupId); setIsNavigationHighlight(true); setContextNavToolUseId(toolUseId); @@ -522,7 +522,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { } // Scroll to tool element, or fall back to AI group - const scrollTarget = toolElement ?? aiGroupRefs.current.get(groupId); + const scrollTarget = toolElement ?? aiGroupRefs.current.get(targetGroupId); if (scrollTarget) { scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' }); } diff --git a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx index 28f06a05..94e5454f 100644 --- a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx +++ b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx @@ -9,14 +9,14 @@ import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssV import { ChevronRight } from 'lucide-react'; import { formatTokens } from '../utils/formatting'; -import { formatFirstSeen, parseTurnIndex } from '../utils/pathParsing'; +import { formatFirstSeen } from '../utils/pathParsing'; import type { TreeNode } from './types'; interface DirectoryTreeNodeProps { node: TreeNode; depth?: number; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const DirectoryTreeNode = ({ @@ -34,8 +34,8 @@ export const DirectoryTreeNode = ({ }); if (node.isFile) { - const turnIndex = node.firstSeenInGroup ? parseTurnIndex(node.firstSeenInGroup) : -1; - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = node.firstSeenInGroup ?? ''; + const isClickable = onNavigateToTurn && groupId.length > 0; return (
onNavigateToTurn(turnIndex)} + onClick={() => onNavigateToTurn(groupId)} > - @{formatFirstSeen(node.firstSeenInGroup)} + @{formatFirstSeen(node.firstSeenTurnIndex ?? -1)} ) : ( - @{formatFirstSeen(node.firstSeenInGroup)} + @{formatFirstSeen(node.firstSeenTurnIndex ?? -1)} ))}
diff --git a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/buildDirectoryTree.ts b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/buildDirectoryTree.ts index 48e38d43..2f2ebb5c 100644 --- a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/buildDirectoryTree.ts +++ b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/buildDirectoryTree.ts @@ -36,6 +36,7 @@ export function buildDirectoryTree( isFile: isLast && part === 'CLAUDE.md', tokens: isLast ? injection.estimatedTokens : undefined, firstSeenInGroup: isLast ? injection.firstSeenInGroup : undefined, + firstSeenTurnIndex: isLast ? injection.firstSeenTurnIndex : undefined, children: new Map(), }); } diff --git a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/types.ts b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/types.ts index 386c5e7f..d28352df 100644 --- a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/types.ts +++ b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/types.ts @@ -8,5 +8,6 @@ export interface TreeNode { isFile: boolean; tokens?: number; firstSeenInGroup?: string; + firstSeenTurnIndex?: number; children: Map; } diff --git a/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdFilesSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdFilesSection.tsx index f3e38b4f..7904c4d3 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdFilesSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdFilesSection.tsx @@ -18,7 +18,7 @@ interface ClaudeMdFilesSectionProps { isExpanded: boolean; onToggle: () => void; projectRoot: string; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ClaudeMdFilesSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdSection.tsx index 90c665ab..e6942d23 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/ClaudeMdSection.tsx @@ -18,7 +18,7 @@ interface ClaudeMdSubSectionProps { injections: ClaudeMdContextInjection[]; isDirectory: boolean; projectRoot: string; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ClaudeMdSubSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx index 1555e334..a2dc0d85 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/FlatInjectionList.tsx @@ -10,7 +10,6 @@ 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'; @@ -39,7 +38,7 @@ interface FlatRow { label: string; description: string; tokens: number; - turnIndex: number; + groupId: string; toolUseId?: string; isError?: boolean; copyPath?: string; @@ -48,9 +47,9 @@ interface FlatRow { interface FlatInjectionListProps { injections: ContextInjection[]; - onNavigateToTurn?: (turnIndex: number) => void; - onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; - onNavigateToUserGroup?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; + onNavigateToTool?: (groupId: string, toolUseId: string) => void; + onNavigateToUserGroup?: (groupId: string) => void; } // ============================================================================= @@ -71,7 +70,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: tool.toolName, description: `Turn ${inj.turnIndex + 1}`, tokens: tool.tokenCount, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, toolUseId: tool.toolUseId, isError: tool.isError, navigationType: tool.toolUseId ? 'tool' : 'turn', @@ -84,7 +83,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: `${inj.toolCount} tool${inj.toolCount !== 1 ? 's' : ''}`, description: `Turn ${inj.turnIndex + 1}`, tokens: inj.estimatedTokens, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, navigationType: 'turn', }); } @@ -98,7 +97,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: item.type === 'thinking' ? 'Thinking' : 'Text', description: `Turn ${inj.turnIndex + 1}`, tokens: item.tokenCount, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, navigationType: 'turn', }); } @@ -112,7 +111,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: item.toolName ?? item.label, description: `Turn ${inj.turnIndex + 1}`, tokens: item.tokenCount, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, navigationType: 'turn', }); } @@ -125,7 +124,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: inj.displayName || inj.path, description: '', tokens: inj.estimatedTokens, - turnIndex: parseTurnIndex(inj.firstSeenInGroup), + groupId: inj.firstSeenInGroup, copyPath: inj.path, navigationType: 'turn', }); @@ -138,7 +137,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: inj.displayName, description: '', tokens: inj.estimatedTokens, - turnIndex: inj.firstSeenTurnIndex, + groupId: inj.firstSeenInGroup, copyPath: inj.path, navigationType: 'turn', }); @@ -152,7 +151,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: `${item.key} ×${item.count}`, description: `Turn ${inj.turnIndex + 1}`, tokens: item.tokenCount, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, toolUseId: item.toolUseId, navigationType: item.toolUseId ? 'tool' : 'turn', }); @@ -166,7 +165,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: `${inj.roundCount} quiet round${inj.roundCount !== 1 ? 's' : ''}`, description: `Turn ${inj.turnIndex + 1}`, tokens: inj.estimatedTokens, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, navigationType: 'turn', }); break; @@ -178,7 +177,7 @@ function flattenInjections(injections: ContextInjection[]): FlatRow[] { label: inj.textPreview, description: '', tokens: inj.estimatedTokens, - turnIndex: inj.turnIndex, + groupId: inj.aiGroupId, navigationType: 'user-group', }); break; @@ -210,13 +209,13 @@ export const FlatInjectionList = ({ }; const handleClick = (): void => { - if (row.turnIndex < 0) return; + if (!row.groupId) return; if (row.navigationType === 'tool' && row.toolUseId && onNavigateToTool) { - onNavigateToTool(row.turnIndex, row.toolUseId); + onNavigateToTool(row.groupId, row.toolUseId); } else if (row.navigationType === 'user-group' && onNavigateToUserGroup) { - onNavigateToUserGroup(row.turnIndex); + onNavigateToUserGroup(row.groupId); } else if (onNavigateToTurn) { - onNavigateToTurn(row.turnIndex); + onNavigateToTurn(row.groupId); } }; diff --git a/src/renderer/components/chat/SessionContextPanel/components/LoopSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/LoopSection.tsx index 578cead6..ccc05488 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/LoopSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/LoopSection.tsx @@ -16,8 +16,8 @@ interface LoopSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; - onNavigateToTurn?: (turnIndex: number, opts?: { flashHeader?: boolean }) => void; + onNavigateToTool?: (groupId: string, toolUseId: string) => void; + onNavigateToTurn?: (groupId: string, opts?: { flashHeader?: boolean }) => void; } export const LoopSection = ({ @@ -76,9 +76,9 @@ export const LoopSection = ({ type="button" onClick={() => { if (item.toolUseId && onNavigateToTool) { - onNavigateToTool(injection.turnIndex, item.toolUseId); + onNavigateToTool(injection.aiGroupId, item.toolUseId); } else if (onNavigateToTurn) { - onNavigateToTurn(injection.turnIndex, { flashHeader: true }); + onNavigateToTurn(injection.aiGroupId, { flashHeader: true }); } }} className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-left" diff --git a/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx index 75dd31a1..f829d544 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/MentionedFilesSection.tsx @@ -16,7 +16,7 @@ interface MentionedFilesSectionProps { isExpanded: boolean; onToggle: () => void; projectRoot?: string; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const MentionedFilesSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx index c5653aa7..eb126c99 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/RankedInjectionList.tsx @@ -13,7 +13,6 @@ import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssV import { ChevronRight } from 'lucide-react'; import { formatTokens } from '../utils/formatting'; -import { parseTurnIndex } from '../utils/pathParsing'; import type { ContextInjection, ToolOutputInjection } from '@renderer/types/contextInjection'; @@ -39,9 +38,9 @@ const CATEGORY_COLORS: Record void; - onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; - onNavigateToUserGroup?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; + onNavigateToTool?: (groupId: string, toolUseId: string) => void; + onNavigateToUserGroup?: (groupId: string) => void; } // ============================================================================= @@ -71,20 +70,14 @@ function getInjectionDescription(injection: ContextInjection): string { } } -function getInjectionTurnIndex(injection: ContextInjection): number { +/** Navigation identity: the AI group this injection belongs to (real group id). */ +function getInjectionGroupId(injection: ContextInjection): string { switch (injection.category) { case 'claude-md': - return parseTurnIndex(injection.firstSeenInGroup); case 'mentioned-file': - return injection.firstSeenTurnIndex; - case 'tool-output': - case 'thinking-text': - case 'task-coordination': - case 'user-message': - case 'loop': - case 'wait-loop': - case 'reread': - return injection.turnIndex; + return injection.firstSeenInGroup; + default: + return injection.aiGroupId; } } @@ -106,8 +99,8 @@ const ToolOutputRankedItem = ({ onNavigateToTool, }: Readonly<{ injection: ToolOutputInjection; - onNavigateToTurn?: (turnIndex: number) => void; - onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; + onNavigateToTurn?: (groupId: string) => void; + onNavigateToTool?: (groupId: string, toolUseId: string) => void; }>): React.ReactElement => { const [expanded, setExpanded] = useState(false); const hasBreakdown = injection.toolBreakdown.length > 0; @@ -125,8 +118,8 @@ const ToolOutputRankedItem = ({ if (hasBreakdown) { setExpanded(!expanded); } else if (onNavigateToTurn) { - const turnIndex = getInjectionTurnIndex(injection); - if (turnIndex >= 0) onNavigateToTurn(turnIndex); + const groupId = getInjectionGroupId(injection); + if (groupId) onNavigateToTurn(groupId); } }} className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5" @@ -166,9 +159,9 @@ const ToolOutputRankedItem = ({ key={`${tool.toolName}-${idx}`} onClick={() => { if (tool.toolUseId && onNavigateToTool) { - onNavigateToTool(injection.turnIndex, tool.toolUseId); + onNavigateToTool(injection.aiGroupId, tool.toolUseId); } else if (onNavigateToTurn) { - onNavigateToTurn(injection.turnIndex); + onNavigateToTurn(injection.aiGroupId); } }} className="flex w-full items-center gap-2 rounded px-2 py-0.5 text-left text-xs transition-colors hover:bg-white/5" @@ -244,13 +237,13 @@ export const RankedInjectionList = ({ const copyPath = getCopyablePath(inj); const handleClick = (): void => { - const turnIndex = getInjectionTurnIndex(inj); - if (turnIndex < 0) return; + const groupId = getInjectionGroupId(inj); + if (!groupId) return; // User messages → navigate to user group; others → navigate to AI group if (inj.category === 'user-message' && onNavigateToUserGroup) { - onNavigateToUserGroup(turnIndex); + onNavigateToUserGroup(groupId); } else if (onNavigateToTurn) { - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); } }; diff --git a/src/renderer/components/chat/SessionContextPanel/components/RereadSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/RereadSection.tsx index a0880c0e..e49903ad 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/RereadSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/RereadSection.tsx @@ -15,7 +15,7 @@ interface RereadSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTurn?: (turnIndex: number, opts?: { flashHeader?: boolean }) => void; + onNavigateToTurn?: (groupId: string, opts?: { flashHeader?: boolean }) => void; } export const RereadSection = ({ @@ -41,7 +41,7 @@ export const RereadSection = ({ type="button" onClick={() => { if (onNavigateToTurn) { - onNavigateToTurn(injection.turnIndex, { flashHeader: true }); + onNavigateToTurn(injection.aiGroupId, { flashHeader: true }); } }} className="flex w-full cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5" diff --git a/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx index 508eccdb..43aab947 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/TaskCoordinationSection.tsx @@ -15,7 +15,7 @@ interface TaskCoordinationSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const TaskCoordinationSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/ThinkingTextSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/ThinkingTextSection.tsx index 88d6f333..b00d4a28 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/ThinkingTextSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/ThinkingTextSection.tsx @@ -15,7 +15,7 @@ interface ThinkingTextSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ThinkingTextSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/ToolOutputsSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/ToolOutputsSection.tsx index 57de5672..a2f7bb3c 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/ToolOutputsSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/ToolOutputsSection.tsx @@ -15,7 +15,7 @@ interface ToolOutputsSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ToolOutputsSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/UserMessagesSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/UserMessagesSection.tsx index 76f4914a..f0bb5413 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/UserMessagesSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/UserMessagesSection.tsx @@ -15,7 +15,7 @@ interface UserMessagesSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const UserMessagesSection = ({ diff --git a/src/renderer/components/chat/SessionContextPanel/components/WaitLoopSection.tsx b/src/renderer/components/chat/SessionContextPanel/components/WaitLoopSection.tsx index edf55886..ad8693f3 100644 --- a/src/renderer/components/chat/SessionContextPanel/components/WaitLoopSection.tsx +++ b/src/renderer/components/chat/SessionContextPanel/components/WaitLoopSection.tsx @@ -17,7 +17,7 @@ interface WaitLoopSectionProps { tokenCount: number; isExpanded: boolean; onToggle: () => void; - onNavigateToTurn?: (turnIndex: number, opts?: { flashHeader?: boolean }) => void; + onNavigateToTurn?: (groupId: string, opts?: { flashHeader?: boolean }) => void; } export const WaitLoopSection = ({ @@ -72,7 +72,7 @@ export const WaitLoopSection = ({ type="button" onClick={() => { if (onNavigateToTurn) { - onNavigateToTurn(injection.turnIndex, { flashHeader: true }); + onNavigateToTurn(injection.aiGroupId, { flashHeader: true }); } }} className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 text-left" diff --git a/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx index 6b25072b..f44d51b4 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx @@ -8,14 +8,14 @@ import { CopyablePath } from '@renderer/components/common/CopyablePath'; import { resolveAbsolutePath, shortenDisplayPath } from '@renderer/utils/pathDisplay'; import { formatTokens } from '../utils/formatting'; -import { formatFirstSeen, parseTurnIndex } from '../utils/pathParsing'; +import { formatFirstSeen } from '../utils/pathParsing'; import type { ClaudeMdContextInjection } from '@renderer/types/contextInjection'; interface ClaudeMdItemProps { injection: ClaudeMdContextInjection; projectRoot?: string; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ClaudeMdItem = ({ @@ -23,8 +23,8 @@ export const ClaudeMdItem = ({ projectRoot, onNavigateToTurn, }: Readonly): React.ReactElement => { - const turnIndex = parseTurnIndex(injection.firstSeenInGroup); - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = injection.firstSeenInGroup; + const isClickable = onNavigateToTurn && groupId.length > 0; const displayPath = shortenDisplayPath(injection.path, projectRoot); const absolutePath = resolveAbsolutePath(injection.path, projectRoot); @@ -55,9 +55,9 @@ export const ClaudeMdItem = ({ font: 'inherit', fontSize: '12px', }} - onClick={() => onNavigateToTurn(turnIndex)} + onClick={() => onNavigateToTurn(groupId)} > - @{formatFirstSeen(injection.firstSeenInGroup)} + @{formatFirstSeen(injection.firstSeenTurnIndex)} ) : ( - @{formatFirstSeen(injection.firstSeenInGroup)} + @{formatFirstSeen(injection.firstSeenTurnIndex)} )} diff --git a/src/renderer/components/chat/SessionContextPanel/items/MentionedFileItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/MentionedFileItem.tsx index 8f7449a2..e7dbce74 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/MentionedFileItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/MentionedFileItem.tsx @@ -15,7 +15,7 @@ import type { MentionedFileInjection } from '@renderer/types/contextInjection'; interface MentionedFileItemProps { injection: MentionedFileInjection; projectRoot?: string; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const MentionedFileItem = ({ @@ -24,7 +24,8 @@ export const MentionedFileItem = ({ onNavigateToTurn, }: Readonly): React.ReactElement => { const turnIndex = injection.firstSeenTurnIndex; - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = injection.firstSeenInGroup; + const isClickable = onNavigateToTurn && groupId.length > 0; const displayPath = shortenDisplayPath(injection.path, projectRoot); const absolutePath = resolveAbsolutePath(injection.path, projectRoot); @@ -65,10 +66,10 @@ export const MentionedFileItem = ({ textDecorationStyle: 'dotted' as const, textUnderlineOffset: '2px', }} - onClick={() => onNavigateToTurn(turnIndex)} + onClick={() => onNavigateToTurn(groupId)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); } }} > diff --git a/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx index 62832646..b5f0a536 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/TaskCoordinationItem.tsx @@ -13,7 +13,7 @@ import type { TaskCoordinationInjection } from '@renderer/types/contextInjection interface TaskCoordinationItemProps { injection: TaskCoordinationInjection; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const TaskCoordinationItem = ({ @@ -22,7 +22,8 @@ export const TaskCoordinationItem = ({ }: Readonly): React.ReactElement => { const [expanded, setExpanded] = useState(false); const turnIndex = injection.turnIndex; - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = injection.aiGroupId; + const isClickable = onNavigateToTurn && groupId.length > 0; const hasBreakdown = injection.breakdown.length > 0; const containerContent = ( @@ -47,12 +48,12 @@ export const TaskCoordinationItem = ({ }} onClick={(e) => { e.stopPropagation(); - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); } }} > diff --git a/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx index 92be27b0..255e0efb 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ThinkingTextItem.tsx @@ -13,7 +13,7 @@ import type { ThinkingTextInjection } from '@renderer/types/contextInjection'; interface ThinkingTextItemProps { injection: ThinkingTextInjection; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ThinkingTextItem = ({ @@ -22,7 +22,8 @@ export const ThinkingTextItem = ({ }: Readonly): React.ReactElement => { const [expanded, setExpanded] = useState(false); const turnIndex = injection.turnIndex; - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = injection.aiGroupId; + const isClickable = onNavigateToTurn && groupId.length > 0; return (
@@ -56,12 +57,12 @@ export const ThinkingTextItem = ({ }} onClick={(e) => { e.stopPropagation(); - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); } }} > diff --git a/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx index fe46a522..2f6e0fba 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ToolOutputItem.tsx @@ -15,7 +15,7 @@ import type { ToolOutputInjection } from '@renderer/types/contextInjection'; interface ToolOutputItemProps { injection: ToolOutputInjection; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const ToolOutputItem = ({ @@ -24,7 +24,8 @@ export const ToolOutputItem = ({ }: Readonly): React.ReactElement => { const [expanded, setExpanded] = useState(false); const turnIndex = injection.turnIndex; - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = injection.aiGroupId; + const isClickable = onNavigateToTurn && groupId.length > 0; const hasBreakdown = injection.toolBreakdown.length > 0; const containerContent = ( @@ -49,12 +50,12 @@ export const ToolOutputItem = ({ }} onClick={(e) => { e.stopPropagation(); - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); } }} > diff --git a/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx index ce3219fa..a6976634 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/UserMessageItem.tsx @@ -13,7 +13,7 @@ import type { UserMessageInjection } from '@renderer/types/contextInjection'; interface UserMessageItemProps { injection: UserMessageInjection; - onNavigateToTurn?: (turnIndex: number) => void; + onNavigateToTurn?: (groupId: string) => void; } export const UserMessageItem = ({ @@ -21,7 +21,8 @@ export const UserMessageItem = ({ onNavigateToTurn, }: Readonly): React.ReactElement => { const turnIndex = injection.turnIndex; - const isClickable = onNavigateToTurn && turnIndex >= 0; + const groupId = injection.aiGroupId; + const isClickable = onNavigateToTurn && groupId.length > 0; return (
@@ -38,10 +39,10 @@ export const UserMessageItem = ({ textDecorationStyle: 'dotted' as const, textUnderlineOffset: '2px', }} - onClick={() => onNavigateToTurn(turnIndex)} + onClick={() => onNavigateToTurn(groupId)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { - onNavigateToTurn(turnIndex); + onNavigateToTurn(groupId); } }} > diff --git a/src/renderer/components/chat/SessionContextPanel/types.ts b/src/renderer/components/chat/SessionContextPanel/types.ts index 804f2ae1..51346479 100644 --- a/src/renderer/components/chat/SessionContextPanel/types.ts +++ b/src/renderer/components/chat/SessionContextPanel/types.ts @@ -16,12 +16,12 @@ export interface SessionContextPanelProps { onClose?: () => void; /** Project root for relative path display */ projectRoot?: string; - /** Click Turn N to navigate to that turn; flashHeader lights the group header (burn pills) */ - onNavigateToTurn?: (turnIndex: number, opts?: { flashHeader?: boolean }) => void; - /** Navigate to a specific tool within a turn by toolUseId */ - onNavigateToTool?: (turnIndex: number, toolUseId: string) => void; - /** Navigate to the user message group preceding the AI group at turnIndex */ - onNavigateToUserGroup?: (turnIndex: number) => void; + /** Navigate to the AI group by its id; flashHeader lights the group header (burn pills) */ + onNavigateToTurn?: (groupId: string, opts?: { flashHeader?: boolean }) => void; + /** Navigate to a specific tool within the group by toolUseId */ + onNavigateToTool?: (groupId: string, toolUseId: string) => void; + /** Navigate to the user message group preceding the AI group with this id */ + onNavigateToUserGroup?: (groupId: string) => void; /** Total session tokens (input + output + cache) for comparison */ totalSessionTokens?: number; /** Phase information for phase selector */ diff --git a/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts b/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts index a31426fa..11ced646 100644 --- a/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts +++ b/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts @@ -3,21 +3,10 @@ */ /** - * Format the firstSeenInGroup value into a human-readable string. - * Converts "ai-0" -> "Turn 1", "ai-1" -> "Turn 2", etc. + * Format a turn index (0-based) into a human-readable label. + * 0 -> "Turn 1"; negative (unknown) -> ''. */ -export function formatFirstSeen(groupId: string): string { - const turnIndex = parseTurnIndex(groupId); - if (turnIndex < 0) return groupId; +export function formatFirstSeen(turnIndex: number): string { + if (turnIndex < 0) return ''; return `Turn ${turnIndex + 1}`; } - -/** - * Extract turn index from groupId. Returns -1 if invalid. - * "ai-0" -> 0, "ai-1" -> 1, etc. - */ -export function parseTurnIndex(groupId: string): number { - const match = /^ai-(\d+)$/.exec(groupId); - if (!match) return -1; - return parseInt(match[1], 10); -} diff --git a/test/renderer/components/burnHeaderNav.test.ts b/test/renderer/components/burnHeaderNav.test.ts index 24ff9ea9..a9acdabe 100644 --- a/test/renderer/components/burnHeaderNav.test.ts +++ b/test/renderer/components/burnHeaderNav.test.ts @@ -10,16 +10,21 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +import { ClaudeMdItem } from '../../../src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem'; import { LoopSection } from '../../../src/renderer/components/chat/SessionContextPanel/components/LoopSection'; import { WaitLoopSection } from '../../../src/renderer/components/chat/SessionContextPanel/components/WaitLoopSection'; -import type { LoopInjection, WaitLoopInjection } from '@renderer/types/contextInjection'; +import type { + ClaudeMdContextInjection, + LoopInjection, + WaitLoopInjection, +} from '@renderer/types/contextInjection'; const waitInjection = { id: 'wait-loop-ai-0', category: 'wait-loop', turnIndex: 0, - aiGroupId: 'ai-0', + aiGroupId: 'ai-11111111-uuid', estimatedTokens: 4_284_616, roundCount: 58, rounds: [{ uuid: 'r1', index: 7, outputTokens: 118, billed: 4_284_616 }], @@ -29,7 +34,7 @@ const loopInjection = { id: 'loop-ai-0', category: 'loop', turnIndex: 0, - aiGroupId: 'ai-0', + aiGroupId: 'ai-11111111-uuid', estimatedTokens: 2300, breakdown: [{ key: 'Edit|/Users/x/prompt_test.go', count: 3, tokenCount: 938 }], rounds: [{ uuid: 'r2', index: 9, billed: 2300, keys: ['Edit|/Users/x/prompt_test.go'] }], @@ -89,7 +94,7 @@ describe('burn navigation targets the aggregate', () => { clickEntryByTitle(host, 'Turn 1'); - expect(onNavigateToTurn).toHaveBeenCalledWith(0, { flashHeader: true }); + expect(onNavigateToTurn).toHaveBeenCalledWith('ai-11111111-uuid', { flashHeader: true }); unmount(); }); @@ -109,7 +114,7 @@ describe('burn navigation targets the aggregate', () => { clickEntryByTitle(host, 'Edit|/Users/x/prompt_test.go'); - expect(onNavigateToTurn).toHaveBeenCalledWith(0, { flashHeader: true }); + expect(onNavigateToTurn).toHaveBeenCalledWith('ai-11111111-uuid', { flashHeader: true }); expect(onNavigateToTool).not.toHaveBeenCalled(); unmount(); }); @@ -130,8 +135,34 @@ describe('burn navigation targets the aggregate', () => { clickEntryByTitle(host, 'Edit|/Users/x/prompt_test.go'); - expect(onNavigateToTool).toHaveBeenCalledWith(0, 'toolu-1'); + expect(onNavigateToTool).toHaveBeenCalledWith('ai-11111111-uuid', 'toolu-1'); expect(onNavigateToTurn).not.toHaveBeenCalled(); unmount(); }); + + it('ClaudeMdItem navigates by its real first-seen group id', async () => { + const onNavigateToTurn = vi.fn(); + const injection = { + id: 'cm-1', + category: 'claude-md', + path: '/proj/CLAUDE.md', + source: 'project-memory', + displayName: 'CLAUDE.md', + isGlobal: true, + estimatedTokens: 5000, + firstSeenInGroup: 'ai-22222222-uuid', + firstSeenTurnIndex: 3, + } as unknown as ClaudeMdContextInjection; + const { host, unmount } = await mount( + React.createElement(ClaudeMdItem, { + injection, + onNavigateToTurn, + }) + ); + + clickEntryByTitle(host, '@Turn 4'); + + expect(onNavigateToTurn).toHaveBeenCalledWith('ai-22222222-uuid'); + unmount(); + }); }); From 7c2951835101cfd21f2772641d6e0db574f36457 Mon Sep 17 00:00:00 2001 From: axisrow Date: Wed, 7 Oct 2026 12:37:20 +0800 Subject: [PATCH 3/4] chore(test): cap vitest workers at n-1 explicitly Pin poolOptions.threads (minThreads 1, maxThreads cpus-1) so worker count does not depend on vitest defaults and the suite cannot saturate the machine. Co-Authored-By: Claude Code --- vitest.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vitest.config.ts b/vitest.config.ts index b5d338ef..88b3ed24 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vitest/config'; +import os from 'node:os'; import { resolve } from 'path'; export default defineConfig({ @@ -8,6 +9,13 @@ export default defineConfig({ testTimeout: 15000, setupFiles: ['./test/setup.ts'], include: ['test/**/*.test.ts'], + pool: 'threads', + poolOptions: { + threads: { + minThreads: 1, + maxThreads: Math.max(1, os.cpus().length - 1), + }, + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], From 63eb888847aa7077e8ee9a9de4d32f1299491e60 Mon Sep 17 00:00:00 2001 From: axisrow Date: Wed, 7 Oct 2026 14:33:04 +0800 Subject: [PATCH 4/4] fix(review): harden group-id migration per code-review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the #53 migration from /code-review on PR #56: - vitest.critical.config.ts: mirror the n-1 worker cap — test:coverage:critical would otherwise spawn uncapped workers, the exact load that hung the machine. - ClaudeMdItem: guard firstSeenTurnIndex with ?? -1 like DirectoryTreeNode — an injection without the field rendered "@Turn NaN" instead of degrading. - createGlobalInjections: turnIndex becomes a required parameter (was `= 0`); both callers already pass aiGroup.turnIndex, the default silently mislabeled a future caller as "@Turn 1". - Stale doc examples in contextInjection.ts: ids are now "-" / real group ids, not "ai-0". - burnHeaderNav fixtures: injection ids follow the real scheme ("wait-loop-"), so an id-scheme regression can't keep them green. - formatFirstSeen moved into utils/formatting.ts; pathParsing.ts deleted — it parsed no paths after parseTurnIndex's removal. Co-Authored-By: Claude Code --- .../DirectoryTree/DirectoryTreeNode.tsx | 3 +- .../items/ClaudeMdItem.tsx | 7 ++--- .../SessionContextPanel/utils/formatting.ts | 9 ++++++ .../SessionContextPanel/utils/pathParsing.ts | 12 -------- src/renderer/types/contextInjection.ts | 30 +++++++++---------- src/renderer/utils/claudeMdTracker.ts | 8 ++--- src/renderer/utils/contextTracker.ts | 4 +-- .../renderer/components/burnHeaderNav.test.ts | 4 +-- vitest.critical.config.ts | 8 +++++ 9 files changed, 44 insertions(+), 41 deletions(-) delete mode 100644 src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts diff --git a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx index 94e5454f..abd4060d 100644 --- a/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx +++ b/src/renderer/components/chat/SessionContextPanel/DirectoryTree/DirectoryTreeNode.tsx @@ -8,8 +8,7 @@ import { CopyablePath } from '@renderer/components/common/CopyablePath'; import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables'; import { ChevronRight } from 'lucide-react'; -import { formatTokens } from '../utils/formatting'; -import { formatFirstSeen } from '../utils/pathParsing'; +import { formatFirstSeen, formatTokens } from '../utils/formatting'; import type { TreeNode } from './types'; diff --git a/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx b/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx index f44d51b4..b21a811d 100644 --- a/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx +++ b/src/renderer/components/chat/SessionContextPanel/items/ClaudeMdItem.tsx @@ -7,8 +7,7 @@ import React from 'react'; import { CopyablePath } from '@renderer/components/common/CopyablePath'; import { resolveAbsolutePath, shortenDisplayPath } from '@renderer/utils/pathDisplay'; -import { formatTokens } from '../utils/formatting'; -import { formatFirstSeen } from '../utils/pathParsing'; +import { formatFirstSeen, formatTokens } from '../utils/formatting'; import type { ClaudeMdContextInjection } from '@renderer/types/contextInjection'; @@ -57,7 +56,7 @@ export const ClaudeMdItem = ({ }} onClick={() => onNavigateToTurn(groupId)} > - @{formatFirstSeen(injection.firstSeenTurnIndex)} + @{formatFirstSeen(injection.firstSeenTurnIndex ?? -1)} ) : ( - @{formatFirstSeen(injection.firstSeenTurnIndex)} + @{formatFirstSeen(injection.firstSeenTurnIndex ?? -1)} )}
diff --git a/src/renderer/components/chat/SessionContextPanel/utils/formatting.ts b/src/renderer/components/chat/SessionContextPanel/utils/formatting.ts index 364b7bab..a11b1270 100644 --- a/src/renderer/components/chat/SessionContextPanel/utils/formatting.ts +++ b/src/renderer/components/chat/SessionContextPanel/utils/formatting.ts @@ -4,3 +4,12 @@ // Re-export from shared module for backwards compatibility export { formatTokensCompact as formatTokens } from '@shared/utils/tokenFormatting'; + +/** + * Format a turn index (0-based) into a human-readable label. + * 0 -> "Turn 1"; negative (unknown) -> ''. + */ +export function formatFirstSeen(turnIndex: number): string { + if (turnIndex < 0) return ''; + return `Turn ${turnIndex + 1}`; +} diff --git a/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts b/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts deleted file mode 100644 index 11ced646..00000000 --- a/src/renderer/components/chat/SessionContextPanel/utils/pathParsing.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Path parsing utilities for SessionContextPanel. - */ - -/** - * Format a turn index (0-based) into a human-readable label. - * 0 -> "Turn 1"; negative (unknown) -> ''. - */ -export function formatFirstSeen(turnIndex: number): string { - if (turnIndex < 0) return ''; - return `Turn ${turnIndex + 1}`; -} diff --git a/src/renderer/types/contextInjection.ts b/src/renderer/types/contextInjection.ts index 53eb5ec8..81972dec 100644 --- a/src/renderer/types/contextInjection.ts +++ b/src/renderer/types/contextInjection.ts @@ -37,7 +37,7 @@ export interface MentionedFileInjection { estimatedTokens: number; /** Turn index where this file was first mentioned */ firstSeenTurnIndex: number; - /** AI group ID (e.g., "ai-0") where this file was first seen, for navigation */ + /** AI group ID (e.g., "ai-") where this file was first seen, for navigation */ firstSeenInGroup: string; /** Whether the file exists on disk */ exists: boolean; @@ -81,13 +81,13 @@ export interface ToolTokenBreakdown { * Multiple tools may execute in one turn; this aggregates their token contributions. */ export interface ToolOutputInjection { - /** Unique identifier (e.g., "tool-output-ai-0") */ + /** Unique identifier (e.g., "tool-output-") */ id: string; /** Discriminator for type narrowing */ category: 'tool-output'; /** Turn index where these tool outputs occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Total estimated tokens from all tools in this turn */ estimatedTokens: number; @@ -116,13 +116,13 @@ export interface ThinkingTextBreakdown { * Aggregates all thinking blocks and text outputs within one AI response turn. */ export interface ThinkingTextInjection { - /** Unique identifier (e.g., "thinking-text-ai-0") */ + /** Unique identifier (e.g., "thinking-text-") */ id: string; /** Discriminator for type narrowing */ category: 'thinking-text'; /** Turn index where this content occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Total estimated tokens from thinking + text in this turn */ estimatedTokens: number; @@ -140,13 +140,13 @@ export interface ThinkingTextInjection { * provides a more complete picture of what consumes tokens. */ export interface UserMessageInjection { - /** Unique identifier (e.g., "user-msg-ai-0") */ + /** Unique identifier (e.g., "user-msg-") */ id: string; /** Discriminator for type narrowing */ category: 'user-message'; /** Turn index where this user message occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Estimated token count for the user message content */ estimatedTokens: number; @@ -178,13 +178,13 @@ export interface TaskCoordinationBreakdown { * from generic tool outputs. */ export interface TaskCoordinationInjection { - /** Unique identifier (e.g., "task-coord-ai-0") */ + /** Unique identifier (e.g., "task-coord-") */ id: string; /** Discriminator for type narrowing */ category: 'task-coordination'; /** Turn index where these task coordination items occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Total estimated tokens from all task coordination items in this turn */ estimatedTokens: number; @@ -215,13 +215,13 @@ export interface LoopTokenBreakdown { * Calls 2..N of a streak land here; the first call stays in tool-output. */ export interface LoopInjection { - /** Unique identifier (e.g., "loop-ai-0") */ + /** Unique identifier (e.g., "loop-") */ id: string; /** Discriminator for type narrowing */ category: 'loop'; /** Turn index where these repeats occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Total estimated tokens from repeat calls in this turn */ estimatedTokens: number; @@ -254,13 +254,13 @@ export interface LoopRoundInfo { * wait_loop findings. */ export interface WaitLoopInjection { - /** Unique identifier (e.g., "wait-loop-ai-0") */ + /** Unique identifier (e.g., "wait-loop-") */ id: string; /** Discriminator for type narrowing */ category: 'wait-loop'; /** Turn index where these quiet rounds occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Total billed context re-read by quiet rounds in this turn */ estimatedTokens: number; @@ -295,13 +295,13 @@ export interface WaitRoundInfo { * the panel's "Visible" totals. */ export interface RereadInjection { - /** Unique identifier (e.g., "reread-ai-0") */ + /** Unique identifier (e.g., "reread-") */ id: string; /** Discriminator for type narrowing */ category: 'reread'; /** Turn index where this spend occurred */ turnIndex: number; - /** AI group ID for navigation (e.g., "ai-0") */ + /** AI group ID for navigation (e.g., "ai-") */ aiGroupId: string; /** Input-side billed tokens across all requests of the turn */ estimatedTokens: number; diff --git a/src/renderer/utils/claudeMdTracker.ts b/src/renderer/utils/claudeMdTracker.ts index 549fce91..20c3b1eb 100644 --- a/src/renderer/utils/claudeMdTracker.ts +++ b/src/renderer/utils/claudeMdTracker.ts @@ -315,8 +315,8 @@ export function detectClaudeMdFromFilePath(filePath: string, projectRoot: string export function createGlobalInjections( projectRoot: string, aiGroupId: string, - tokenData?: Record, - turnIndex: number = 0 + turnIndex: number, + tokenData?: Record ): ClaudeMdInjection[] { const injections: ClaudeMdInjection[] = []; @@ -529,8 +529,8 @@ function computeClaudeMdStats(params: ComputeClaudeMdStatsParams): ComputeClaude const globalInjections = createGlobalInjections( projectRoot, turnGroupId, - tokenData, - aiGroup.turnIndex + aiGroup.turnIndex, + tokenData ); for (const injection of globalInjections) { if (!previousPaths.has(injection.path)) { diff --git a/src/renderer/utils/contextTracker.ts b/src/renderer/utils/contextTracker.ts index d77694be..908ee423 100644 --- a/src/renderer/utils/contextTracker.ts +++ b/src/renderer/utils/contextTracker.ts @@ -914,8 +914,8 @@ function computeContextStats(params: ComputeContextStatsParams): ComputeContextS const globalInjections = createGlobalInjections( projectRoot, turnGroupId, - claudeMdTokenData, - aiGroup.turnIndex + aiGroup.turnIndex, + claudeMdTokenData ); for (const injection of globalInjections) { if (!previousPaths.has(injection.path)) { diff --git a/test/renderer/components/burnHeaderNav.test.ts b/test/renderer/components/burnHeaderNav.test.ts index a9acdabe..2795d850 100644 --- a/test/renderer/components/burnHeaderNav.test.ts +++ b/test/renderer/components/burnHeaderNav.test.ts @@ -21,7 +21,7 @@ import type { } from '@renderer/types/contextInjection'; const waitInjection = { - id: 'wait-loop-ai-0', + id: 'wait-loop-ai-11111111-uuid', category: 'wait-loop', turnIndex: 0, aiGroupId: 'ai-11111111-uuid', @@ -31,7 +31,7 @@ const waitInjection = { } as WaitLoopInjection; const loopInjection = { - id: 'loop-ai-0', + id: 'loop-ai-11111111-uuid', category: 'loop', turnIndex: 0, aiGroupId: 'ai-11111111-uuid', diff --git a/vitest.critical.config.ts b/vitest.critical.config.ts index c32070bf..a8dd2bcb 100644 --- a/vitest.critical.config.ts +++ b/vitest.critical.config.ts @@ -1,3 +1,4 @@ +import os from 'node:os'; import { resolve } from 'path'; import { defineConfig } from 'vitest/config'; @@ -7,6 +8,13 @@ export default defineConfig({ environment: 'happy-dom', setupFiles: ['./test/setup.ts'], include: ['test/**/*.test.ts'], + pool: 'threads', + poolOptions: { + threads: { + minThreads: 1, + maxThreads: Math.max(1, os.cpus().length - 1), + }, + }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html'],