"""V2: Fix all TypeScript errors for auto-continue feature. Errors fixed: 1. 'startAutoContinue' used before declaration → use refs 2. 'showAutoContinue'/'taskIncompleteInfo' never read → they're in return 3. 'incompletePlan' implicit any → add type 4. EventType missing 'task_incomplete' → add to events.ts 5. DatasetUploadResponse not found → fix import path 6. onTaskIncomplete not on SideChannelCallbacks → fix in SSE transport """ import os import re # ===================================================================== # PATCH 1: events.ts - Add 'task_incomplete' to EventType # ===================================================================== EVENTS_FILE = "/source/frontend/src/types/events.ts" def patch_events(): if not os.path.exists(EVENTS_FILE): print(f"SKIP: {EVENTS_FILE} not found") return with open(EVENTS_FILE, "r") as f: content = f.read() if "'task_incomplete'" not in content: content = content.replace( " | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';" ) with open(EVENTS_FILE, "w") as f: f.write(content) print("OK: Added 'task_incomplete' to EventType") else: print("OK: EventType already has 'task_incomplete'") # ===================================================================== # PATCH 2: useAgentChat.ts - Fix hoisting + type issues # ===================================================================== USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts" def patch_use_agent_chat(): if not os.path.exists(USE_AGENT_CHAT): print(f"SKIP: {USE_AGENT_CHAT} not found") return with open(USE_AGENT_CHAT, "r") as f: content = f.read() # 1. Add useState to imports if "useState" not in content: content = content.replace( "import { useCallback, useEffect, useMemo, useRef } from 'react';", "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';" ) # 2. Add state + refs after callbacksRef state_code = ''' // Auto-continue state for free models when task incomplete const autoContinueTimerRef = useRef | null>(null); const [showAutoContinue, setShowAutoContinue] = useState(false); const [taskIncompleteInfo, setTaskIncompleteInfo] = useState<{ incompletePlan: Array<{ id: string; content: string; status: string }>; } | null>(null); const startAutoContinueRef = useRef<() => void>(() => {}); const cancelAutoContinueRef = useRef<() => void>(() => {}); ''' if "autoContinueTimerRef" not in content: content = content.replace( "callbacksRef.current = { onReady, onError, onSessionDead };", "callbacksRef.current = { onReady, onError, onSessionDead };" + state_code ) # 3. Add onTaskIncomplete to SideChannelCallbacks interface if "onTaskIncomplete:" not in content and "onInterrupted:" in content: content = content.replace( " onInterrupted: () => void;", " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;" ) # 4. Replace onTaskIncomplete handler to use refs old_handler = "onTaskIncomplete: (incompletePlan) => {" new_handler = "onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => {" if old_handler in content: content = content.replace(old_handler, new_handler) # Fix the timer callback to use ref instead of direct call old_timer = "autoContinueTimerRef.current = setTimeout(() => {\n startAutoContinue();\n }, 10000);" new_timer = "autoContinueTimerRef.current = setTimeout(() => {\n startAutoContinueRef.current();\n }, 10000);" if old_timer in content: content = content.replace(old_timer, new_timer) # 5. Add auto-continue functions BEFORE return (but using refs pattern) # Remove old startAutoContinue/cancelAutoContinue if they exist old_funcs_start = "const startAutoContinue = useCallback" if old_funcs_start in content: # Find and remove the old function blocks start_idx = content.find("const startAutoContinue = useCallback") cancel_idx = content.find("const cancelAutoContinue = useCallback") cleanup_idx = content.find("// Cleanup timer on unmount") # Remove from startAutoContinue to end of cleanup if start_idx > 0 and cleanup_idx > start_idx: # Find the end of cleanup effect end_cleanup = content.find("\n", cleanup_idx) # Find the closing of the useEffect close_idx = content.find("}, []);", end_cleanup) if close_idx > 0: close_idx += len("}, []);") content = content[:start_idx] + content[close_idx:] # 5b. Add new functions using ref pattern, before return new_funcs = ''' // -- Auto-continue for free models when task incomplete ----------------- const startAutoContinue = useCallback(() => { setShowAutoContinue(false); setTaskIncompleteInfo(null); if (autoContinueTimerRef.current) { clearTimeout(autoContinueTimerRef.current); autoContinueTimerRef.current = null; } const incompleteItems = taskIncompleteInfo?.incompletePlan || []; const continuationText = incompleteItems.length > 0 ? `Tiếp tục từ các task chưa hoàn thành:\\n${incompleteItems.map(i => `- ${i.content}`).join('\\n')}` : 'Tiếp tục nhiệm vụ.'; chat.sendMessage({ text: `[TỰ ĐỘNG TIẾP TỤC] ${continuationText}`, metadata: { createdAt: new Date().toISOString() }, }); }, [taskIncompleteInfo, chat]); const cancelAutoContinue = useCallback(() => { setShowAutoContinue(false); setTaskIncompleteInfo(null); if (autoContinueTimerRef.current) { clearTimeout(autoContinueTimerRef.current); autoContinueTimerRef.current = null; } }, []); // Keep refs in sync startAutoContinueRef.current = startAutoContinue; cancelAutoContinueRef.current = cancelAutoContinue; // Cleanup timer on unmount useEffect(() => { return () => { if (autoContinueTimerRef.current) { clearTimeout(autoContinueTimerRef.current); } }; }, []); ''' if "startAutoContinueRef.current" not in content: content = content.replace( '\n return {\n messages: chat.messages,', new_funcs + '\n return {\n messages: chat.messages,' ) # 6. Update return to include new values if "showAutoContinue," not in content: content = content.replace( "refreshMessages,\n };", "refreshMessages,\n showAutoContinue,\n taskIncompleteInfo,\n startAutoContinue,\n cancelAutoContinue,\n };" ) with open(USE_AGENT_CHAT, "w") as f: f.write(content) print(f"OK: Patched {USE_AGENT_CHAT}") # ===================================================================== # PATCH 3: ChatInput.tsx - Fix DatasetUploadResponse import # ===================================================================== CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx" def patch_chat_input(): if not os.path.exists(CHAT_INPUT): print(f"SKIP: {CHAT_INPUT} not found") return with open(CHAT_INPUT, "r") as f: content = f.read() # Fix DatasetUploadResponse import - try correct path if "DatasetUploadResponse" in content: # Check if it's imported from agent.ts wrong_import = "import type { DatasetUploadResponse } from '@/types/agent';" if wrong_import in content: # Try to find the correct import - it's probably in the same file or from backend # Replace with inline type or remove content = content.replace(wrong_import, "") # Add inline type content = content.replace( "import { apiFetch } from '@/utils/api';", "import { apiFetch } from '@/utils/api';\n\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };" ) print("OK: Fixed DatasetUploadResponse import") # Add Button to MUI imports if "Button" not in content: content = content.replace( "import { Box, IconButton, Stack, Tooltip } from '@mui/material';", "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';" ) # Check if interface already has our props (from previous patch) if "showAutoContinue" not in content: # Find interface and replace with complete version interface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL) if interface_match: props_code = ''' interface ChatInputProps { sessionId: string; initialModelPath: string | null | undefined; onSend: (text: string) => Promise; onStop: () => void; onDatasetUploaded: () => Promise; isProcessing: boolean; disabled: boolean; placeholder?: string; showAutoContinue?: boolean; taskIncompleteInfo?: { incompletePlan: Array<{ id: string; content: string; status: string }> } | null; startAutoContinue?: () => void; cancelAutoContinue?: () => void; } ''' content = content.replace(interface_match.group(0), props_code) print("OK: Updated ChatInputProps interface") # Add auto-continue buttons buttons_code = ''' {/* Auto-continue controls for free models */} {showAutoContinue && taskIncompleteInfo && ( )} ''' if "Tự động tiếp tục" not in content: content = content.replace( " void;", " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;" ) # Use @ts-ignore for the case since EventType union doesn't include 'task_incomplete' # Or better: add the case with a type assertion if "case 'task_incomplete'" in content: # Already added, make sure it compiles old_case = "case 'task_incomplete':\n sideChannel.onTaskIncomplete(" new_case = "case 'task_incomplete' as const:\n sideChannel.onTaskIncomplete(" content = content.replace(old_case, new_case) with open(SSE_TRANSPORT, "w") as f: f.write(content) print(f"OK: Patched {SSE_TRANSPORT}") if __name__ == "__main__": patch_events() patch_use_agent_chat() patch_chat_input() patch_sse_transport() print("\nDONE: All V2 patches applied")