import React from 'react'; import ReactMarkdown from 'react-markdown'; import { useStore } from '@renderer/store'; import { REHYPE_PLUGINS } from '@renderer/utils/markdownPlugins'; import { AlertTriangle, CheckCircle, FileCheck, XCircle } from 'lucide-react'; import remarkGfm from 'remark-gfm'; import { useShallow } from 'zustand/react/shallow'; import { CopyButton } from '../common/CopyButton'; import { OngoingBanner } from '../common/OngoingIndicator'; import { createMarkdownComponents, markdownComponents } from './markdownComponents'; import { createSearchContext } from './searchHighlightUtils'; import type { AIGroupLastOutput } from '@renderer/types/groups'; interface LastOutputDisplayProps { lastOutput: AIGroupLastOutput | null; aiGroupId: string; /** Whether this is the last AI group in the conversation */ isLastGroup?: boolean; /** Whether the session is ongoing (from sessions array, same source as sidebar) */ isSessionOngoing?: boolean; } /** * LastOutputDisplay shows the always-visible last text output OR last tool result. * This is what the user sees as "the answer" from the AI. * * Features: * - Shows text output with elegant prose styling * - Shows tool result with tool name and icon * - Handles error states for tool results * - Shows timestamp * - Expandable for long content */ export const LastOutputDisplay = ({ lastOutput, aiGroupId, isLastGroup = false, isSessionOngoing = false, }: Readonly): React.JSX.Element | null => { const { searchQuery, searchMatches, currentSearchIndex } = useStore( useShallow((s) => ({ searchQuery: s.searchQuery, searchMatches: s.searchMatches, currentSearchIndex: s.currentSearchIndex, })) ); const isTextOutput = lastOutput?.type === 'text' && Boolean(lastOutput.text); // Create search context (fresh each render so counter starts at 0) const searchCtx = searchQuery && isTextOutput ? createSearchContext(searchQuery, aiGroupId, searchMatches, currentSearchIndex) : null; // Create markdown components with optional search highlighting // When search is active, create fresh each render (match counter is stateful and must start at 0) // useMemo would cache stale closures when parent re-renders without search deps changing const mdComponents = searchCtx ? createMarkdownComponents(searchCtx) : markdownComponents; // Show ongoing banner if this is the last AI group and session is ongoing // This uses the same source (sessions array) as the sidebar green dot for consistency if (isLastGroup && isSessionOngoing) { return ; } if (!lastOutput) { return null; } const { type } = lastOutput; // Render text output if (type === 'text' && lastOutput.text) { const textContent = lastOutput.text || ''; return (
{/* Content - scrollable */}
{textContent}
); } // Render tool result if (type === 'tool_result' && lastOutput.toolResult) { const isError = lastOutput.isError ?? false; const Icon = isError ? XCircle : CheckCircle; return (
{/* Header */}
{lastOutput.toolName && ( {lastOutput.toolName} )} {isError && ( Error )}
{/* Content */}
            {lastOutput.toolResult}
          
); } // Render interruption as a simple horizontal banner if (type === 'interruption') { return (
Request interrupted by user
); } // Render plan_exit (ExitPlanMode) with plan content in markdown if (type === 'plan_exit' && lastOutput.planContent) { const planContent = lastOutput.planContent || ''; const planPreamble = lastOutput.planPreamble; return (
{/* Preamble text (e.g., "The plan is complete. Let me exit plan mode...") */} {planPreamble && (
{planPreamble}
)} {/* Plan content block */}
{/* Header */}
Plan Ready for Approval
{/* Plan content - scrollable */}
{planContent}
); } return null; };