/** * LinkedToolItem * * Main component for rendering linked tool calls in the chat view. * Uses specialized viewers for different tool types and shared utilities * for summary generation and token calculation. */ import React, { useRef } from 'react'; import { CARD_ICON_MUTED } from '@renderer/constants/cssVariables'; import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors'; import { useTheme } from '@renderer/hooks/useTheme'; import { getToolContextTokens, getToolStatus, getToolSummary, hasEditContent, hasReadContent, hasSkillInstructions, hasWriteContent, } from '@renderer/utils/toolRendering'; import { getToolHighlightProps, getTriggerColorDef, isPresetColorKey, TOOL_HIGHLIGHT_CLASSES, type TriggerColor, } from '@shared/constants/triggerColors'; import { Wrench } from 'lucide-react'; import { highlightQueryInText } from '../searchHighlightUtils'; import { BaseItem, StatusDot } from './BaseItem'; import { formatDuration } from './baseItemHelpers'; import { DefaultToolViewer, EditToolViewer, ReadToolViewer, SkillToolViewer, ToolErrorDisplay, WriteToolViewer, } from './linkedTool'; import type { LinkedToolItem as LinkedToolItemType } from '@renderer/types/groups'; interface LinkedToolItemProps { linkedTool: LinkedToolItemType; onClick: () => void; isExpanded: boolean; /** Optional local search query override for inline highlighting */ searchQueryOverride?: string; /** Whether this item should be highlighted for error deep linking */ isHighlighted?: boolean; /** Custom highlight color from trigger */ highlightColor?: TriggerColor; /** Notification dot color for this tool item */ notificationDotColor?: TriggerColor; /** Optional ref registration callback for external scroll control */ registerRef?: (el: HTMLDivElement | null) => void; } export const LinkedToolItem: React.FC = ({ linkedTool, onClick, isExpanded, searchQueryOverride, isHighlighted, highlightColor, notificationDotColor, registerRef, }) => { const status = getToolStatus(linkedTool); const { isLight } = useTheme(); const summary = getToolSummary(linkedTool.name, linkedTool.input); const summaryNode = searchQueryOverride && searchQueryOverride.trim().length > 0 ? highlightQueryInText( summary, searchQueryOverride, `${linkedTool.id ?? linkedTool.name}:summary`, { forceAllActive: true, } ) : summary; const elementRef = useRef(null); // Combined ref callback - handles both internal ref and external registration const handleRef = (el: HTMLDivElement | null): void => { // Update internal ref (elementRef as React.MutableRefObject).current = el; // Call external registration if provided registerRef?.(el); }; // Render teammate_spawned results as a minimal inline row const isTeammateSpawned = linkedTool.result?.toolUseResult?.status === 'teammate_spawned'; if (isTeammateSpawned) { const teamResult = linkedTool.result!.toolUseResult!; const name = (teamResult.name as string) || 'teammate'; const color = (teamResult.color as string) || ''; const colors = getTeamColorSet(color); return (
{name} Teammate spawned
); } // Render SendMessage shutdown_request as a minimal inline row const isShutdownRequest = linkedTool.name === 'SendMessage' && linkedTool.input?.type === 'shutdown_request'; if (isShutdownRequest) { const target = (linkedTool.input?.recipient as string) || 'teammate'; return (
Shutdown requested →{' '} {target}
); } // Note: We no longer scroll locally - the navigation coordinator handles this // via the registered ref. This prevents double-scroll issues. // Highlight animation for error deep linking (supports custom hex) const effectiveColor = highlightColor ?? 'red'; let highlightClasses = ''; let highlightStyle: React.CSSProperties | undefined; if (isHighlighted) { if (isPresetColorKey(effectiveColor)) { highlightClasses = TOOL_HIGHLIGHT_CLASSES[effectiveColor]; } else { const hp = getToolHighlightProps(effectiveColor); highlightClasses = hp.className; highlightStyle = hp.style; } } // Determine which specialized viewer to use const useReadViewer = linkedTool.name === 'Read' && hasReadContent(linkedTool) && !linkedTool.result?.isError; const useEditViewer = linkedTool.name === 'Edit' && hasEditContent(linkedTool); const useWriteViewer = linkedTool.name === 'Write' && hasWriteContent(linkedTool) && !linkedTool.result?.isError; const useSkillViewer = linkedTool.name === 'Skill' && hasSkillInstructions(linkedTool); const useDefaultViewer = !useReadViewer && !useEditViewer && !useWriteViewer && !useSkillViewer; // Check if we should show error display for Read/Write tools const showReadError = linkedTool.name === 'Read' && linkedTool.result?.isError; const showWriteError = linkedTool.name === 'Write' && linkedTool.result?.isError; return (
} label={linkedTool.name} summary={summaryNode} tokenCount={getToolContextTokens(linkedTool)} status={status} durationMs={linkedTool.durationMs} onClick={onClick} isExpanded={isExpanded} highlightClasses={highlightClasses} highlightStyle={highlightStyle} notificationDotColor={notificationDotColor} > {/* Read tool with CodeBlockViewer */} {useReadViewer && } {/* Edit tool with DiffViewer */} {useEditViewer && } {/* Write tool */} {useWriteViewer && } {/* Skill tool with instructions */} {useSkillViewer && } {/* Default rendering for other tools */} {useDefaultViewer && } {/* Error output for Read tool */} {showReadError && } {/* Error output for Write tool */} {showWriteError && } {/* Orphaned indicator */} {linkedTool.isOrphaned && (
No result received
)} {/* Timing */}
Duration: {formatDuration(linkedTool.durationMs)}
); };