'use client'; import { useState, useEffect, useRef } from 'react'; import { useTranslation } from '@/hooks/useTranslation'; import { Message as AIMessage, MessageContent, MessageResponse, } from '@/components/ai-elements/message'; import { ToolActionsGroup } from '@/components/ai-elements/tool-actions-group'; import { Confirmation, ConfirmationTitle, ConfirmationRequest, ConfirmationAccepted, ConfirmationRejected, ConfirmationActions, ConfirmationAction, } from '@/components/ai-elements/confirmation'; import { Shimmer } from '@/components/ai-elements/shimmer'; import type { ToolUIPart } from 'ai'; import type { PermissionRequestEvent } from '@/types'; interface ToolUseInfo { id: string; name: string; input: unknown; } interface ToolResultInfo { tool_use_id: string; content: string; is_error?: boolean; } interface StreamingMessageProps { content: string; isStreaming: boolean; toolUses?: ToolUseInfo[]; toolResults?: ToolResultInfo[]; streamingToolOutput?: string; statusText?: string; pendingPermission?: PermissionRequestEvent | null; onPermissionResponse?: (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record) => void; permissionResolved?: 'allow' | 'deny' | null; onForceStop?: () => void; } function ElapsedTimer() { const [elapsed, setElapsed] = useState(0); const startRef = useRef(0); useEffect(() => { startRef.current = Date.now(); const interval = setInterval(() => { setElapsed(Math.floor((Date.now() - startRef.current) / 1000)); }, 1000); return () => clearInterval(interval); }, []); const mins = Math.floor(elapsed / 60); const secs = elapsed % 60; return ( {mins > 0 ? `${mins}m ${secs}s` : `${secs}s`} ); } function AskUserQuestionUI({ toolInput, onSubmit, }: { toolInput: Record; onSubmit: (decision: 'allow', updatedInput: Record) => void; }) { const questions = (toolInput.questions || []) as Array<{ question: string; options: Array<{ label: string; description?: string }>; multiSelect: boolean; header?: string; }>; const [selections, setSelections] = useState>>({}); const [otherTexts, setOtherTexts] = useState>({}); const [useOther, setUseOther] = useState>({}); const toggleOption = (qIdx: string, label: string, multi: boolean) => { setSelections((prev) => { const current = new Set(prev[qIdx] || []); if (multi) { current.has(label) ? current.delete(label) : current.add(label); } else { current.clear(); current.add(label); } return { ...prev, [qIdx]: current }; }); // Deselect "Other" when picking a regular option setUseOther((prev) => ({ ...prev, [qIdx]: false })); }; const toggleOther = (qIdx: string, multi: boolean) => { if (!multi) { setSelections((prev) => ({ ...prev, [qIdx]: new Set() })); } setUseOther((prev) => ({ ...prev, [qIdx]: !prev[qIdx] })); }; const handleSubmit = () => { const answers: Record = {}; questions.forEach((q, i) => { const qIdx = String(i); const selected = Array.from(selections[qIdx] || []); if (useOther[qIdx] && otherTexts[qIdx]?.trim()) { selected.push(otherTexts[qIdx].trim()); } answers[q.question] = selected.join(', '); }); onSubmit('allow', { questions: toolInput.questions, answers }); }; const hasAnswer = questions.some((_, i) => { const qIdx = String(i); return (selections[qIdx]?.size || 0) > 0 || (useOther[qIdx] && otherTexts[qIdx]?.trim()); }); return (
{questions.map((q, i) => { const qIdx = String(i); const selected = selections[qIdx] || new Set(); return (
{q.header && ( {q.header} )}

{q.question}

{q.options.map((opt) => { const isSelected = selected.has(opt.label); return ( ); })} {/* Other option */}
{useOther[qIdx] && ( setOtherTexts((prev) => ({ ...prev, [qIdx]: e.target.value }))} className="w-full rounded-md border border-border bg-background px-3 py-1.5 text-xs focus:border-primary focus:outline-none" autoFocus /> )}
); })}
); } function ExitPlanModeUI({ toolInput, onApprove, onDeny, }: { toolInput: Record; onApprove: () => void; onDeny: () => void; }) { const allowedPrompts = (toolInput.allowedPrompts || []) as Array<{ tool: string; prompt: string; }>; return (
Plan complete — ready to execute
{allowedPrompts.length > 0 && (

Requested permissions:

    {allowedPrompts.map((p, i) => (
  • {p.tool} {p.prompt}
  • ))}
)}
); } function StreamingStatusBar({ statusText, onForceStop }: { statusText?: string; onForceStop?: () => void }) { const displayText = statusText || 'Thinking'; // Parse elapsed seconds from statusText like "Running bash... (45s)" const elapsedMatch = statusText?.match(/\((\d+)s\)/); const toolElapsed = elapsedMatch ? parseInt(elapsedMatch[1], 10) : 0; const isWarning = toolElapsed >= 60; const isCritical = toolElapsed >= 90; return (
{displayText} {isWarning && !isCritical && ( Running longer than usual )} {isCritical && ( Tool may be stuck )}
| {isCritical && onForceStop && ( )}
); } export function StreamingMessage({ content, isStreaming, toolUses = [], toolResults = [], streamingToolOutput, statusText, pendingPermission, onPermissionResponse, permissionResolved, onForceStop, }: StreamingMessageProps) { const { t } = useTranslation(); const runningTools = toolUses.filter( (tool) => !toolResults.some((r) => r.tool_use_id === tool.id) ); // Determine confirmation state for the AI Elements component const getConfirmationState = (): ToolUIPart['state'] => { if (permissionResolved) return 'approval-responded'; if (pendingPermission) return 'approval-requested'; return 'input-available'; }; const getApproval = () => { if (!pendingPermission && !permissionResolved) return undefined; if (permissionResolved === 'allow') { return { id: pendingPermission?.permissionRequestId || '', approved: true as const }; } if (permissionResolved === 'deny') { return { id: pendingPermission?.permissionRequestId || '', approved: false as const }; } // Pending - no decision yet return { id: pendingPermission?.permissionRequestId || '' }; }; const formatToolInput = (input: Record): string => { if (input.command) return String(input.command); if (input.file_path) return String(input.file_path); if (input.path) return String(input.path); return JSON.stringify(input, null, 2); }; // Extract a human-readable summary of the running command const getRunningCommandSummary = (): string | undefined => { if (runningTools.length === 0) { // All tools completed but still streaming — AI is generating text if (toolUses.length > 0) return 'Generating response...'; return undefined; } const tool = runningTools[runningTools.length - 1]; const input = tool.input as Record; if (tool.name === 'Bash' && input.command) { const cmd = String(input.command); return cmd.length > 80 ? cmd.slice(0, 80) + '...' : cmd; } if (input.file_path) return `${tool.name}: ${String(input.file_path)}`; if (input.path) return `${tool.name}: ${String(input.path)}`; return `Running ${tool.name}...`; }; return ( {/* Tool calls — compact collapsible group */} {toolUses.length > 0 && ( { const result = toolResults.find((r) => r.tool_use_id === tool.id); return { id: tool.id, name: tool.name, input: tool.input, result: result?.content, isError: result?.is_error, }; })} isStreaming={isStreaming} streamingToolOutput={streamingToolOutput} /> )} {/* Permission approval — AskUserQuestion gets a dedicated UI */} {pendingPermission?.toolName === 'AskUserQuestion' && !permissionResolved && ( } onSubmit={(decision, updatedInput) => onPermissionResponse?.(decision, updatedInput)} /> )} {pendingPermission?.toolName === 'AskUserQuestion' && permissionResolved && (

Answer submitted

)} {/* Permission approval — ExitPlanMode gets a dedicated UI */} {pendingPermission?.toolName === 'ExitPlanMode' && !permissionResolved && ( } onApprove={() => onPermissionResponse?.('allow')} onDeny={() => onPermissionResponse?.('deny')} /> )} {pendingPermission?.toolName === 'ExitPlanMode' && permissionResolved === 'allow' && (

Plan approved — executing

)} {pendingPermission?.toolName === 'ExitPlanMode' && permissionResolved === 'deny' && (

Plan rejected

)} {/* Permission approval — generic confirmation for other tools */} {(pendingPermission || permissionResolved) && pendingPermission?.toolName !== 'AskUserQuestion' && pendingPermission?.toolName !== 'ExitPlanMode' && ( {pendingPermission?.toolName} {pendingPermission?.decisionReason && ( — {pendingPermission.decisionReason} )} {pendingPermission && (
{formatToolInput(pendingPermission.toolInput)}
)} onPermissionResponse?.('deny')} > Deny onPermissionResponse?.('allow')} > Allow Once {pendingPermission?.suggestions && pendingPermission.suggestions.length > 0 && ( onPermissionResponse?.('allow_session')} > {t('streaming.allowForSession')} )}

{t('streaming.allowed')}

{t('streaming.denied')}

)} {/* Streaming text content rendered via Streamdown */} {content && ( {content} )} {/* Loading indicator when no content yet */} {isStreaming && !content && toolUses.length === 0 && !pendingPermission && (
{t('streaming.thinking')}
)} {/* Status bar during streaming — show permission wait status when awaiting authorization */} {isStreaming && }
); }