"""V6: FIXED - Auto-continue with pause button for free models.""" import os import re EVENTS_FILE = "/source/frontend/src/types/events.ts" SSE_TRANSPORT = "/source/frontend/src/lib/sse-chat-transport.ts" HOOK_FILE = "/source/frontend/src/hooks/useAgentChat.ts" CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx" SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx" AGENT_LOOP = "/app/agent/core/agent_loop.py" def fix_events(): 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: events.ts") def fix_sse(): with open(SSE_TRANSPORT, "r") as f: content = f.read() # Add onTaskIncomplete to interface if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content: content = content.replace( " onInterrupted: () => void;", " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;" ) # Add case handler if "case 'task_incomplete'" not in content: case_code = """ case 'task_incomplete' as const: sideChannel.onTaskIncomplete( (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [], ); break; """ content = content.replace("case 'turn_complete':", case_code + "\n case 'turn_complete':") with open(SSE_TRANSPORT, "w") as f: f.write(content) print("OK: sse-chat-transport.ts") def fix_hook(): with open(HOOK_FILE, "r") as f: content = f.read() # 1. Import useState 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 after callbacksRef block state_block = """ // Auto-continue for free models const _acTimer = useRef | null>(null); const [_showAc, _setShowAc] = useState(false); const _acCancel = useCallback(() => { _setShowAc(false); if (_acTimer.current) { clearTimeout(_acTimer.current); _acTimer.current = null; } }, []);""" if "_acTimer" not in content: content = content.replace( "callbacksRef.current = { onReady, onError, onSessionDead };", "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block ) # 3. Add onTaskIncomplete to interface if "onTaskIncomplete:" not in content and "onInterrupted: () => void;" in content: content = content.replace( " onInterrupted: () => void;", " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;" ) # 4. Add handler in sideChannel - uses chatActionsRef to avoid hoisting issues handler_code = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => { _setShowAc(true); if (_acTimer.current) clearTimeout(_acTimer.current); _acTimer.current = setTimeout(() => { _setShowAc(false); const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n'); const msg = { text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } }; const setMsgs = chatActionsRef.current.setMessages; if (setMsgs) { chatActionsRef.current.messages = [...chatActionsRef.current.messages, { id: 'ac-continue', role: 'user', parts: [{ type: 'text', text: msg.text }], content: msg.text }]; } }, 10000); }, onInterrupted: () => { /* no-op - handled by stop() */ },""" if "onTaskIncomplete: (incompletePlan" not in content: old_handler = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },' if old_handler in content: content = content.replace(old_handler, handler_code) else: old_handler2 = ' onInterrupted: () => { /* no-op */ },' content = content.replace(old_handler2, handler_code) # 5. Return values if "_showAc" not in content: # The _showAc and _acCancel need to be in the return return_marker = "refreshMessages,\n };" if return_marker in content: content = content.replace( return_marker, "refreshMessages,\n _showAc,\n _acCancel,\n };" ) with open(HOOK_FILE, "w") as f: f.write(content) print("OK: useAgentChat.ts") def fix_chat_input(): with open(CHAT_INPUT, "r") as f: content = f.read() # Fix DatasetUploadResponse wrong_import = "import type { DatasetUploadResponse } from '@/types/agent';" if wrong_import in content: content = content.replace(wrong_import, "") content = content.replace( "import { apiFetch } from '@/utils/api';", "import { apiFetch } from '@/utils/api';\ntype DatasetUploadResponse = { url: string; repo_id: string; filename: string; size: number; };" ) # Add Button import 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';" ) # Fix interface if "_showAc" not in content: iface_match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', content, re.DOTALL) if iface_match: new_iface = """interface ChatInputProps { sessionId: string; initialModelPath: string | null | undefined; onSend: (text: string) => Promise; onStop: () => void; onDatasetUploaded: () => Promise; isProcessing: boolean; disabled: boolean; placeholder?: string; _showAc?: boolean; _acCancel?: () => void; }""" content = content.replace(iface_match.group(0), new_iface) # Add pause button btn_code = """ {/* Auto-continue pause for free models */} {_showAc && _acCancel && ( )}""" if "T\u1ea1m d\u1eebng" not in content and "free models" not in content: content = content.replace(" list[dict[str, str]]: p = getattr(s, "current_plan", None) or [] return [it for it in p if it.get("status") in ("pending", "in_progress")] ''' content = content.replace("class Handlers:", fn_block + "\n\nclass Handlers:") if "task_incomplete" not in content: check_block = ''' # Auto-continue detect if not llm_result.tool_calls_acc and llm_result.content: unfinished = _unfinished_plan(session) if unfinished: await session.send_event(Event(event_type="task_incomplete", data={"incomplete_plan": unfinished})) ''' marker = " # -- End of turn --" if marker in content: content = content.replace(marker, check_block + "\n" + marker) with open(AGENT_LOOP, "w") as f: f.write(content) print("OK: agent_loop.py") if __name__ == "__main__": fix_events() fix_sse() fix_hook() fix_chat_input() fix_session_chat() fix_agent_loop() print("\nDONE: V6 - all fixed")