feat(chat): enhance navigation and tool highlighting in chat history
- Introduced context panel navigation for user message groups and specific tools within turns, improving user experience in navigating chat history. - Added state management for context navigation tool use ID and effective highlight color, allowing distinct visual cues for context panel interactions. - Updated `ChatHistory` and `SessionContextPanel` components to support new navigation handlers and integrate deep-linking functionality for tools. - Enhanced `RankedInjectionList` to facilitate navigation to user groups and tools, providing a more interactive and user-friendly interface.
This commit is contained in:
parent
9915cf5a03
commit
fb2d56e23f
9 changed files with 214 additions and 49 deletions
|
|
@ -252,7 +252,11 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
||||||
selectSearchMatch,
|
selectSearchMatch,
|
||||||
});
|
});
|
||||||
|
|
||||||
const effectiveHighlightToolUseId = controllerToolUseId ?? undefined;
|
// Local tool highlight for context panel navigation (separate from controller)
|
||||||
|
const [contextNavToolUseId, setContextNavToolUseId] = useState<string | null>(null);
|
||||||
|
const effectiveHighlightToolUseId = controllerToolUseId ?? contextNavToolUseId ?? undefined;
|
||||||
|
// Use blue for context panel tool navigation, otherwise use controller's color
|
||||||
|
const effectiveHighlightColor = contextNavToolUseId ? ('blue' as const) : highlightColor;
|
||||||
|
|
||||||
// Keep search match indices aligned with this tab's rendered conversation.
|
// Keep search match indices aligned with this tab's rendered conversation.
|
||||||
// This avoids stale/global match lists after tab switches or in-place refreshes.
|
// This avoids stale/global match lists after tab switches or in-place refreshes.
|
||||||
|
|
@ -396,6 +400,87 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
||||||
[conversation, ensureGroupVisible, setHighlightedGroupId]
|
[conversation, ensureGroupVisible, setHighlightedGroupId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handler to navigate to a user message group (preceding the AI group at turnIndex)
|
||||||
|
const handleNavigateToUserGroup = useCallback(
|
||||||
|
(turnIndex: number) => {
|
||||||
|
if (!conversation) return;
|
||||||
|
const aiItemIndex = conversation.items.findIndex(
|
||||||
|
(item) => item.type === 'ai' && item.group.turnIndex === turnIndex
|
||||||
|
);
|
||||||
|
if (aiItemIndex < 0) return;
|
||||||
|
|
||||||
|
// Find the user item preceding this AI group
|
||||||
|
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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
[conversation, setHighlightedGroupId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handler to navigate to a specific tool within a turn from context panel
|
||||||
|
const handleNavigateToTool = useCallback(
|
||||||
|
(turnIndex: number, toolUseId: string) => {
|
||||||
|
if (!conversation) return;
|
||||||
|
const targetItem = conversation.items.find(
|
||||||
|
(item) => item.type === 'ai' && item.group.turnIndex === turnIndex
|
||||||
|
);
|
||||||
|
if (targetItem?.type !== 'ai') return;
|
||||||
|
|
||||||
|
const run = async (): Promise<void> => {
|
||||||
|
const groupId = targetItem.group.id;
|
||||||
|
await ensureGroupVisible(groupId);
|
||||||
|
|
||||||
|
// Set group + tool highlight immediately
|
||||||
|
setHighlightedGroupId(groupId);
|
||||||
|
setIsNavigationHighlight(true);
|
||||||
|
setContextNavToolUseId(toolUseId);
|
||||||
|
|
||||||
|
// Wait for tool element to appear in DOM (up to 500ms)
|
||||||
|
let toolElement: HTMLElement | undefined;
|
||||||
|
const startTime = Date.now();
|
||||||
|
while (Date.now() - startTime < 500) {
|
||||||
|
toolElement = toolItemRefs.current.get(toolUseId);
|
||||||
|
if (toolElement) break;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to tool element, or fall back to AI group
|
||||||
|
const scrollTarget = toolElement ?? aiGroupRefs.current.get(groupId);
|
||||||
|
if (scrollTarget) {
|
||||||
|
scrollTarget.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear highlight after 2s
|
||||||
|
if (navigationHighlightTimerRef.current) {
|
||||||
|
clearTimeout(navigationHighlightTimerRef.current);
|
||||||
|
}
|
||||||
|
navigationHighlightTimerRef.current = setTimeout(() => {
|
||||||
|
setHighlightedGroupId(null);
|
||||||
|
setIsNavigationHighlight(false);
|
||||||
|
setContextNavToolUseId(null);
|
||||||
|
navigationHighlightTimerRef.current = null;
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
void run();
|
||||||
|
},
|
||||||
|
[conversation, ensureGroupVisible, setHighlightedGroupId]
|
||||||
|
);
|
||||||
|
|
||||||
// Scroll to current search result when it changes
|
// Scroll to current search result when it changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentMatch = currentSearchIndex >= 0 ? searchMatches[currentSearchIndex] : null;
|
const currentMatch = currentSearchIndex >= 0 ? searchMatches[currentSearchIndex] : null;
|
||||||
|
|
@ -695,7 +780,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
||||||
highlightToolUseId={effectiveHighlightToolUseId}
|
highlightToolUseId={effectiveHighlightToolUseId}
|
||||||
isSearchHighlight={isSearchHighlight}
|
isSearchHighlight={isSearchHighlight}
|
||||||
isNavigationHighlight={isNavigationHighlight}
|
isNavigationHighlight={isNavigationHighlight}
|
||||||
highlightColor={highlightColor}
|
highlightColor={effectiveHighlightColor}
|
||||||
registerChatItemRef={registerChatItemRef}
|
registerChatItemRef={registerChatItemRef}
|
||||||
registerAIGroupRef={registerAIGroupRefCombined}
|
registerAIGroupRef={registerAIGroupRefCombined}
|
||||||
registerToolRef={registerToolRef}
|
registerToolRef={registerToolRef}
|
||||||
|
|
@ -713,7 +798,7 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
||||||
highlightToolUseId={effectiveHighlightToolUseId}
|
highlightToolUseId={effectiveHighlightToolUseId}
|
||||||
isSearchHighlight={isSearchHighlight}
|
isSearchHighlight={isSearchHighlight}
|
||||||
isNavigationHighlight={isNavigationHighlight}
|
isNavigationHighlight={isNavigationHighlight}
|
||||||
highlightColor={highlightColor}
|
highlightColor={effectiveHighlightColor}
|
||||||
registerChatItemRef={registerChatItemRef}
|
registerChatItemRef={registerChatItemRef}
|
||||||
registerAIGroupRef={registerAIGroupRefCombined}
|
registerAIGroupRef={registerAIGroupRefCombined}
|
||||||
registerToolRef={registerToolRef}
|
registerToolRef={registerToolRef}
|
||||||
|
|
@ -732,6 +817,8 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
|
||||||
onClose={() => setContextPanelVisible(false)}
|
onClose={() => setContextPanelVisible(false)}
|
||||||
projectRoot={sessionDetail?.session?.projectPath}
|
projectRoot={sessionDetail?.session?.projectPath}
|
||||||
onNavigateToTurn={handleNavigateToTurn}
|
onNavigateToTurn={handleNavigateToTurn}
|
||||||
|
onNavigateToTool={handleNavigateToTool}
|
||||||
|
onNavigateToUserGroup={handleNavigateToUserGroup}
|
||||||
totalSessionTokens={lastAiGroupTotalTokens}
|
totalSessionTokens={lastAiGroupTotalTokens}
|
||||||
phaseInfo={sessionPhaseInfo ?? undefined}
|
phaseInfo={sessionPhaseInfo ?? undefined}
|
||||||
selectedPhase={selectedContextPhase}
|
selectedPhase={selectedContextPhase}
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,10 @@ const ChatHistoryItemInner = ({
|
||||||
}
|
}
|
||||||
case 'ai': {
|
case 'ai': {
|
||||||
const isHighlighted = highlightedGroupId === item.group.id;
|
const isHighlighted = highlightedGroupId === item.group.id;
|
||||||
// Pass highlightToolUseId to ALL AI groups (when not search/navigation)
|
// Pass highlightToolUseId to ALL AI groups (when not search highlight)
|
||||||
// Each group will check if it contains the tool and expand accordingly
|
// Each group will check if it contains the tool and expand accordingly
|
||||||
// This fixes issues where timestamp matching might fail to find the correct group
|
// Allowed during navigation highlights so context panel tool deep-linking works
|
||||||
const toolUseIdForGroup =
|
const toolUseIdForGroup = !isSearchHighlight ? highlightToolUseId : undefined;
|
||||||
!isSearchHighlight && !isNavigationHighlight ? highlightToolUseId : undefined;
|
|
||||||
const hl = getHighlight(
|
const hl = getHighlight(
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
isSearchHighlight,
|
isSearchHighlight,
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,13 @@
|
||||||
* RankedInjectionList - All context injections sorted by token size descending.
|
* RankedInjectionList - All context injections sorted by token size descending.
|
||||||
* Injections are shown as grouped rows (e.g., "Tool output in Turn N").
|
* Injections are shown as grouped rows (e.g., "Tool output in Turn N").
|
||||||
* Tool-output rows are expandable to reveal individual tool breakdowns sorted desc.
|
* Tool-output rows are expandable to reveal individual tool breakdowns sorted desc.
|
||||||
|
* Individual tools support deep-link navigation to the exact tool in chat.
|
||||||
|
* CLAUDE.md and File items show a copy-path button.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { CopyButton } from '@renderer/components/common/CopyButton';
|
||||||
import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables';
|
import { COLOR_TEXT_MUTED, COLOR_TEXT_SECONDARY } from '@renderer/constants/cssVariables';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
|
@ -34,6 +37,8 @@ const CATEGORY_COLORS: Record<string, { bg: string; text: string; label: string
|
||||||
interface RankedInjectionListProps {
|
interface RankedInjectionListProps {
|
||||||
injections: ContextInjection[];
|
injections: ContextInjection[];
|
||||||
onNavigateToTurn?: (turnIndex: number) => void;
|
onNavigateToTurn?: (turnIndex: number) => void;
|
||||||
|
onNavigateToTool?: (turnIndex: number, toolUseId: string) => void;
|
||||||
|
onNavigateToUserGroup?: (turnIndex: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -71,6 +76,13 @@ function getInjectionTurnIndex(injection: ContextInjection): number {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get copyable path for path-based injections. */
|
||||||
|
function getCopyablePath(injection: ContextInjection): string | null {
|
||||||
|
if (injection.category === 'claude-md') return injection.path;
|
||||||
|
if (injection.category === 'mentioned-file') return injection.path;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Sub-components
|
// Sub-components
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -79,9 +91,11 @@ function getInjectionTurnIndex(injection: ContextInjection): number {
|
||||||
const ToolOutputRankedItem = ({
|
const ToolOutputRankedItem = ({
|
||||||
injection,
|
injection,
|
||||||
onNavigateToTurn,
|
onNavigateToTurn,
|
||||||
|
onNavigateToTool,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
injection: ToolOutputInjection;
|
injection: ToolOutputInjection;
|
||||||
onNavigateToTurn?: (turnIndex: number) => void;
|
onNavigateToTurn?: (turnIndex: number) => void;
|
||||||
|
onNavigateToTool?: (turnIndex: number, toolUseId: string) => void;
|
||||||
}>): React.ReactElement => {
|
}>): React.ReactElement => {
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const hasBreakdown = injection.toolBreakdown.length > 0;
|
const hasBreakdown = injection.toolBreakdown.length > 0;
|
||||||
|
|
@ -139,7 +153,9 @@ const ToolOutputRankedItem = ({
|
||||||
<button
|
<button
|
||||||
key={`${tool.toolName}-${idx}`}
|
key={`${tool.toolName}-${idx}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (onNavigateToTurn) {
|
if (tool.toolUseId && onNavigateToTool) {
|
||||||
|
onNavigateToTool(injection.turnIndex, tool.toolUseId);
|
||||||
|
} else if (onNavigateToTurn) {
|
||||||
onNavigateToTurn(injection.turnIndex);
|
onNavigateToTurn(injection.turnIndex);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
@ -185,6 +201,8 @@ const ToolOutputRankedItem = ({
|
||||||
export const RankedInjectionList = ({
|
export const RankedInjectionList = ({
|
||||||
injections,
|
injections,
|
||||||
onNavigateToTurn,
|
onNavigateToTurn,
|
||||||
|
onNavigateToTool,
|
||||||
|
onNavigateToUserGroup,
|
||||||
}: Readonly<RankedInjectionListProps>): React.ReactElement => {
|
}: Readonly<RankedInjectionListProps>): React.ReactElement => {
|
||||||
const sortedInjections = useMemo(
|
const sortedInjections = useMemo(
|
||||||
() => [...injections].sort((a, b) => b.estimatedTokens - a.estimatedTokens),
|
() => [...injections].sort((a, b) => b.estimatedTokens - a.estimatedTokens),
|
||||||
|
|
@ -201,50 +219,64 @@ export const RankedInjectionList = ({
|
||||||
key={inj.id}
|
key={inj.id}
|
||||||
injection={inj}
|
injection={inj}
|
||||||
onNavigateToTurn={onNavigateToTurn}
|
onNavigateToTurn={onNavigateToTurn}
|
||||||
|
onNavigateToTool={onNavigateToTool}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other categories: simple row
|
|
||||||
const categoryInfo = CATEGORY_COLORS[inj.category] ?? {
|
const categoryInfo = CATEGORY_COLORS[inj.category] ?? {
|
||||||
bg: 'rgba(161, 161, 170, 0.15)',
|
bg: 'rgba(161, 161, 170, 0.15)',
|
||||||
text: '#a1a1aa',
|
text: '#a1a1aa',
|
||||||
label: inj.category,
|
label: inj.category,
|
||||||
};
|
};
|
||||||
|
const copyPath = getCopyablePath(inj);
|
||||||
|
|
||||||
|
const handleClick = (): void => {
|
||||||
|
const turnIndex = getInjectionTurnIndex(inj);
|
||||||
|
if (turnIndex < 0) return;
|
||||||
|
// User messages → navigate to user group; others → navigate to AI group
|
||||||
|
if (inj.category === 'user-message' && onNavigateToUserGroup) {
|
||||||
|
onNavigateToUserGroup(turnIndex);
|
||||||
|
} else if (onNavigateToTurn) {
|
||||||
|
onNavigateToTurn(turnIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div key={inj.id} className="flex items-center gap-0.5">
|
||||||
key={inj.id}
|
<button
|
||||||
onClick={() => {
|
onClick={handleClick}
|
||||||
if (onNavigateToTurn) {
|
className="flex min-w-0 flex-1 items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
|
||||||
const turnIndex = getInjectionTurnIndex(inj);
|
|
||||||
if (turnIndex >= 0) onNavigateToTurn(turnIndex);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left transition-colors hover:bg-white/5"
|
|
||||||
>
|
|
||||||
{/* Category pill */}
|
|
||||||
<span
|
|
||||||
className="shrink-0 rounded px-1.5 py-0.5 text-[9px] font-medium"
|
|
||||||
style={{ backgroundColor: categoryInfo.bg, color: categoryInfo.text }}
|
|
||||||
>
|
>
|
||||||
{categoryInfo.label}
|
{/* Category pill */}
|
||||||
</span>
|
<span
|
||||||
{/* Description */}
|
className="shrink-0 rounded px-1.5 py-0.5 text-[9px] font-medium"
|
||||||
<span
|
style={{ backgroundColor: categoryInfo.bg, color: categoryInfo.text }}
|
||||||
className="min-w-0 flex-1 truncate text-xs"
|
>
|
||||||
style={{ color: COLOR_TEXT_SECONDARY }}
|
{categoryInfo.label}
|
||||||
>
|
</span>
|
||||||
{getInjectionDescription(inj)}
|
{/* Description */}
|
||||||
</span>
|
<span
|
||||||
{/* Token count */}
|
className="min-w-0 flex-1 truncate text-xs"
|
||||||
<span
|
style={{ color: COLOR_TEXT_SECONDARY }}
|
||||||
className="shrink-0 text-xs font-medium tabular-nums"
|
>
|
||||||
style={{ color: COLOR_TEXT_MUTED }}
|
{getInjectionDescription(inj)}
|
||||||
>
|
</span>
|
||||||
{formatTokens(inj.estimatedTokens)}
|
{/* Token count */}
|
||||||
</span>
|
<span
|
||||||
</button>
|
className="shrink-0 text-xs font-medium tabular-nums"
|
||||||
|
style={{ color: COLOR_TEXT_MUTED }}
|
||||||
|
>
|
||||||
|
{formatTokens(inj.estimatedTokens)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{/* Copy path button for CLAUDE.md and File items */}
|
||||||
|
{copyPath && (
|
||||||
|
<span className="shrink-0" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<CopyButton text={copyPath} inline />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,8 @@ export const SessionContextPanel = ({
|
||||||
onClose,
|
onClose,
|
||||||
projectRoot,
|
projectRoot,
|
||||||
onNavigateToTurn,
|
onNavigateToTurn,
|
||||||
|
onNavigateToTool,
|
||||||
|
onNavigateToUserGroup,
|
||||||
totalSessionTokens,
|
totalSessionTokens,
|
||||||
phaseInfo,
|
phaseInfo,
|
||||||
selectedPhase,
|
selectedPhase,
|
||||||
|
|
@ -250,7 +252,12 @@ export const SessionContextPanel = ({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<RankedInjectionList injections={injections} onNavigateToTurn={onNavigateToTurn} />
|
<RankedInjectionList
|
||||||
|
injections={injections}
|
||||||
|
onNavigateToTurn={onNavigateToTurn}
|
||||||
|
onNavigateToTool={onNavigateToTool}
|
||||||
|
onNavigateToUserGroup={onNavigateToUserGroup}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ export interface SessionContextPanelProps {
|
||||||
projectRoot?: string;
|
projectRoot?: string;
|
||||||
/** Click Turn N to navigate to that turn */
|
/** Click Turn N to navigate to that turn */
|
||||||
onNavigateToTurn?: (turnIndex: number) => void;
|
onNavigateToTurn?: (turnIndex: number) => 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;
|
||||||
/** Total session tokens (input + output + cache) for comparison */
|
/** Total session tokens (input + output + cache) for comparison */
|
||||||
totalSessionTokens?: number;
|
totalSessionTokens?: number;
|
||||||
/** Phase information for phase selector */
|
/** Phase information for phase selector */
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Provides UI for managing notifications, display settings, and advanced options.
|
* Provides UI for managing notifications, display settings, and advanced options.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
@ -23,12 +23,15 @@ export const SettingsView = (): React.JSX.Element | null => {
|
||||||
const pendingSettingsSection = useStore((s) => s.pendingSettingsSection);
|
const pendingSettingsSection = useStore((s) => s.pendingSettingsSection);
|
||||||
const clearPendingSettingsSection = useStore((s) => s.clearPendingSettingsSection);
|
const clearPendingSettingsSection = useStore((s) => s.clearPendingSettingsSection);
|
||||||
|
|
||||||
useEffect(() => {
|
// Consume pending section during render (React-recommended pattern for adjusting state on prop change)
|
||||||
|
const [prevPending, setPrevPending] = useState<string | null>(null);
|
||||||
|
if (pendingSettingsSection !== prevPending) {
|
||||||
|
setPrevPending(pendingSettingsSection);
|
||||||
if (pendingSettingsSection) {
|
if (pendingSettingsSection) {
|
||||||
setActiveSection(pendingSettingsSection as SettingsSection);
|
setActiveSection(pendingSettingsSection as SettingsSection);
|
||||||
clearPendingSettingsSection();
|
clearPendingSettingsSection();
|
||||||
}
|
}
|
||||||
}, [pendingSettingsSection, clearPendingSettingsSection]);
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
config,
|
config,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
* Uses @tanstack/react-virtual for efficient DOM rendering with infinite scroll.
|
* Uses @tanstack/react-virtual for efficient DOM rendering with infinite scroll.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { useStore } from '@renderer/store';
|
import { useStore } from '@renderer/store';
|
||||||
import {
|
import {
|
||||||
|
|
@ -47,7 +48,6 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
sessionsError,
|
sessionsError,
|
||||||
sessionsHasMore,
|
sessionsHasMore,
|
||||||
sessionsLoadingMore,
|
sessionsLoadingMore,
|
||||||
sessionsTotalCount,
|
|
||||||
fetchSessionsMore,
|
fetchSessionsMore,
|
||||||
pinnedSessionIds,
|
pinnedSessionIds,
|
||||||
sessionSortMode,
|
sessionSortMode,
|
||||||
|
|
@ -61,7 +61,6 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
sessionsError: s.sessionsError,
|
sessionsError: s.sessionsError,
|
||||||
sessionsHasMore: s.sessionsHasMore,
|
sessionsHasMore: s.sessionsHasMore,
|
||||||
sessionsLoadingMore: s.sessionsLoadingMore,
|
sessionsLoadingMore: s.sessionsLoadingMore,
|
||||||
sessionsTotalCount: s.sessionsTotalCount,
|
|
||||||
fetchSessionsMore: s.fetchSessionsMore,
|
fetchSessionsMore: s.fetchSessionsMore,
|
||||||
pinnedSessionIds: s.pinnedSessionIds,
|
pinnedSessionIds: s.pinnedSessionIds,
|
||||||
sessionSortMode: s.sessionSortMode,
|
sessionSortMode: s.sessionSortMode,
|
||||||
|
|
@ -70,6 +69,8 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const parentRef = useRef<HTMLDivElement>(null);
|
const parentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const countRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const [showCountTooltip, setShowCountTooltip] = useState(false);
|
||||||
|
|
||||||
// Separate pinned sessions from unpinned
|
// Separate pinned sessions from unpinned
|
||||||
const { pinned: pinnedSessions, unpinned: unpinnedSessions } = useMemo(
|
const { pinned: pinnedSessions, unpinned: unpinnedSessions } = useMemo(
|
||||||
|
|
@ -303,10 +304,39 @@ export const DateGroupedSessions = (): React.JSX.Element => {
|
||||||
>
|
>
|
||||||
{sessionSortMode === 'most-context' ? 'By Context' : 'Sessions'}
|
{sessionSortMode === 'most-context' ? 'By Context' : 'Sessions'}
|
||||||
</h2>
|
</h2>
|
||||||
<span className="text-xs" style={{ color: 'var(--color-text-muted)', opacity: 0.6 }}>
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- tooltip trigger via hover, not interactive */}
|
||||||
|
<span
|
||||||
|
ref={countRef}
|
||||||
|
className="text-xs"
|
||||||
|
style={{ color: 'var(--color-text-muted)', opacity: 0.6 }}
|
||||||
|
onMouseEnter={() => setShowCountTooltip(true)}
|
||||||
|
onMouseLeave={() => setShowCountTooltip(false)}
|
||||||
|
>
|
||||||
({sessions.length}
|
({sessions.length}
|
||||||
{sessionsTotalCount > sessions.length ? ` of ${sessionsTotalCount}` : ''})
|
{sessionsHasMore ? '+' : ''})
|
||||||
</span>
|
</span>
|
||||||
|
{showCountTooltip &&
|
||||||
|
sessionsHasMore &&
|
||||||
|
countRef.current &&
|
||||||
|
createPortal(
|
||||||
|
<div
|
||||||
|
className="pointer-events-none fixed z-50 w-48 rounded-md px-2.5 py-1.5 text-[11px] leading-snug shadow-lg"
|
||||||
|
style={{
|
||||||
|
top: countRef.current.getBoundingClientRect().bottom + 6,
|
||||||
|
left:
|
||||||
|
countRef.current.getBoundingClientRect().left +
|
||||||
|
countRef.current.getBoundingClientRect().width / 2 -
|
||||||
|
96,
|
||||||
|
backgroundColor: 'var(--color-surface-overlay)',
|
||||||
|
border: '1px solid var(--color-border-emphasis)',
|
||||||
|
color: 'var(--color-text-secondary)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sessions.length} loaded so far — scroll down to load more. Context sorting only ranks
|
||||||
|
loaded sessions.
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
|
setSessionSortMode(sessionSortMode === 'recent' ? 'most-context' : 'recent')
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ export interface ToolTokenBreakdown {
|
||||||
tokenCount: number;
|
tokenCount: number;
|
||||||
/** Whether the tool execution resulted in an error */
|
/** Whether the tool execution resulted in an error */
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
|
/** Tool use ID for deep-link navigation to specific tool in chat */
|
||||||
|
toolUseId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -214,6 +214,7 @@ function aggregateToolOutputs(
|
||||||
toolName: displayName,
|
toolName: displayName,
|
||||||
tokenCount: toolTokenCount,
|
tokenCount: toolTokenCount,
|
||||||
isError: linkedTool.result?.isError ?? false,
|
isError: linkedTool.result?.isError ?? false,
|
||||||
|
toolUseId: linkedTool.id,
|
||||||
});
|
});
|
||||||
totalTokens += toolTokenCount;
|
totalTokens += toolTokenCount;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue