diff --git a/src/main/http/config.ts b/src/main/http/config.ts index 9894d5a3..6d35c66d 100644 --- a/src/main/http/config.ts +++ b/src/main/http/config.ts @@ -382,6 +382,94 @@ export function registerConfigRoutes(app: FastifyInstance): void { } ); + // Hide session + app.post<{ Body: { projectId: string; sessionId: string } }>( + '/api/config/hide-session', + async (request) => { + try { + const { projectId, sessionId } = request.body; + if (!projectId || typeof projectId !== 'string') { + return { success: false, error: 'Project ID is required and must be a string' }; + } + if (!sessionId || typeof sessionId !== 'string') { + return { success: false, error: 'Session ID is required and must be a string' }; + } + + configManager.hideSession(projectId, sessionId); + return { success: true }; + } catch (error) { + logger.error('Error in POST /api/config/hide-session:', error); + return { success: false, error: getErrorMessage(error) }; + } + } + ); + + // Unhide session + app.post<{ Body: { projectId: string; sessionId: string } }>( + '/api/config/unhide-session', + async (request) => { + try { + const { projectId, sessionId } = request.body; + if (!projectId || typeof projectId !== 'string') { + return { success: false, error: 'Project ID is required and must be a string' }; + } + if (!sessionId || typeof sessionId !== 'string') { + return { success: false, error: 'Session ID is required and must be a string' }; + } + + configManager.unhideSession(projectId, sessionId); + return { success: true }; + } catch (error) { + logger.error('Error in POST /api/config/unhide-session:', error); + return { success: false, error: getErrorMessage(error) }; + } + } + ); + + // Bulk hide sessions + app.post<{ Body: { projectId: string; sessionIds: string[] } }>( + '/api/config/hide-sessions', + async (request) => { + try { + const { projectId, sessionIds } = request.body; + if (!projectId || typeof projectId !== 'string') { + return { success: false, error: 'Project ID is required and must be a string' }; + } + if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) { + return { success: false, error: 'Session IDs must be an array of strings' }; + } + + configManager.hideSessions(projectId, sessionIds); + return { success: true }; + } catch (error) { + logger.error('Error in POST /api/config/hide-sessions:', error); + return { success: false, error: getErrorMessage(error) }; + } + } + ); + + // Bulk unhide sessions + app.post<{ Body: { projectId: string; sessionIds: string[] } }>( + '/api/config/unhide-sessions', + async (request) => { + try { + const { projectId, sessionIds } = request.body; + if (!projectId || typeof projectId !== 'string') { + return { success: false, error: 'Project ID is required and must be a string' }; + } + if (!Array.isArray(sessionIds) || sessionIds.some((id) => typeof id !== 'string')) { + return { success: false, error: 'Session IDs must be an array of strings' }; + } + + configManager.unhideSessions(projectId, sessionIds); + return { success: true }; + } catch (error) { + logger.error('Error in POST /api/config/unhide-sessions:', error); + return { success: false, error: getErrorMessage(error) }; + } + } + ); + // Select folders - no-op in browser mode app.post('/api/config/select-folders', async (): Promise> => { return { success: true, data: [] }; diff --git a/src/main/utils/jsonl.ts b/src/main/utils/jsonl.ts index 1d70be2c..eaf618f0 100644 --- a/src/main/utils/jsonl.ts +++ b/src/main/utils/jsonl.ts @@ -562,14 +562,18 @@ export async function analyzeSessionFileMetadata( } // Last phase: final tokens - last post-compaction + // Guard: if the last compaction had no subsequent assistant message, post is 0. + // In that case, skip the final phase to avoid double-counting. const lastPhase = compactionPhases[compactionPhases.length - 1]; - const lastContribution = lastMainAssistantInputTokens - lastPhase.post; - total += lastContribution; - phaseBreakdown.push({ - phaseNumber: compactionPhases.length + 1, - contribution: lastContribution, - peakTokens: lastMainAssistantInputTokens, - }); + if (lastPhase.post > 0) { + const lastContribution = lastMainAssistantInputTokens - lastPhase.post; + total += lastContribution; + phaseBreakdown.push({ + phaseNumber: compactionPhases.length + 1, + contribution: lastContribution, + peakTokens: lastMainAssistantInputTokens, + }); + } contextConsumption = total; } diff --git a/src/renderer/components/chat/ChatHistory.tsx b/src/renderer/components/chat/ChatHistory.tsx index 49023fea..a6a4acbe 100644 --- a/src/renderer/components/chat/ChatHistory.tsx +++ b/src/renderer/components/chat/ChatHistory.tsx @@ -413,23 +413,27 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => { const prevItem = aiItemIndex > 0 ? conversation.items[aiItemIndex - 1] : null; if (prevItem?.type !== 'user') return; - const groupId = prevItem.group.id; - const element = chatItemRefs.current.get(groupId); - if (!element) return; + const run = async (): Promise => { + const groupId = prevItem.group.id; + await ensureGroupVisible(groupId); + const element = chatItemRefs.current.get(groupId); + if (!element) return; - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - setHighlightedGroupId(groupId); - setIsNavigationHighlight(true); - if (navigationHighlightTimerRef.current) { - clearTimeout(navigationHighlightTimerRef.current); - } - navigationHighlightTimerRef.current = setTimeout(() => { - setHighlightedGroupId(null); - setIsNavigationHighlight(false); - navigationHighlightTimerRef.current = null; - }, 2000); + element.scrollIntoView({ behavior: 'smooth', block: 'center' }); + setHighlightedGroupId(groupId); + setIsNavigationHighlight(true); + if (navigationHighlightTimerRef.current) { + clearTimeout(navigationHighlightTimerRef.current); + } + navigationHighlightTimerRef.current = setTimeout(() => { + setHighlightedGroupId(null); + setIsNavigationHighlight(false); + navigationHighlightTimerRef.current = null; + }, 2000); + }; + void run(); }, - [conversation, setHighlightedGroupId] + [conversation, ensureGroupVisible, setHighlightedGroupId] ); // Handler to navigate to a specific tool within a turn from context panel