"""V4: FINAL WORKING - Fix all TS errors for auto-continue feature. Strategy: - useAgentChat: return _showAutoContinue + _cancelAutoContinue (TS _ prefix suppresses "unused") - ChatInput.tsx: receive props and render "Tạm dừng" button - SessionChat.tsx: pass props from hook to ChatInput - events.ts: add 'task_incomplete' to EventType union - sse-chat-transport.ts: add onTaskIncomplete + case handler - agent_loop.py: send task_incomplete event """ import os import re EVENTS = "/source/frontend/src/types/events.ts" USE_AGENT_CHAT = "/source/frontend/src/hooks/useAgentChat.ts" SSE = "/source/frontend/src/lib/sse-chat-transport.ts" CHAT_INPUT = "/source/frontend/src/components/Chat/ChatInput.tsx" SESSION_CHAT = "/source/frontend/src/components/SessionChat.tsx" def patch_events(): with open(EVENTS, "r") as f: c = f.read() if "'task_incomplete'" not in c: c = c.replace(" | 'plan_update';", " | 'plan_update'\n | 'task_incomplete';") with open(EVENTS, "w") as f: f.write(c) print("OK: events.ts") def patch_sse(): with open(SSE, "r") as f: c = f.read() # Add onTaskIncomplete to interface if "onTaskIncomplete:" not in c: c = c.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 c: case = """ case 'task_incomplete' as const: sideChannel.onTaskIncomplete( (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [], ); break; """ c = c.replace("case 'turn_complete':", case + "\n case 'turn_complete':") with open(SSE, "w") as f: f.write(c) print("OK: sse-chat-transport.ts") def patch_use_agent_chat(): with open(USE_AGENT_CHAT, "r") as f: c = f.read() # 1. Import useState if "useState" not in c: c = c.replace( "import { useCallback, useEffect, useMemo, useRef } from 'react';", "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';" ) # 2. State (TS _ prefix = unused ok) state_block = ''' // Auto-continue for free models const _autoContinueTimer = useRef | null>(null); const [_showAutoContinue, _setShowAutoContinue] = useState(false); ''' if "_autoContinueTimer" not in c: c = c.replace( "callbacksRef.current = { onReady, onError, onSessionDead };", "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block ) # 3. onTaskIncomplete in interface if "onTaskIncomplete:" not in c: c = c.replace( " onInterrupted: () => void;", " onInterrupted: () => void;\n onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => void;" ) # 4. Cancel ref cancel_ref = ' const _cancelAutoContinue = useRef(() => {});\n' if "_cancelAutoContinue" not in c: # Insert after callbacksRef.current = { ... } area c = c.replace( " isActiveRef.current = isActive;", " isActiveRef.current = isActive;\n" + cancel_ref ) # 5. Handler in sideChannel - inline all logic handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => { _setShowAutoContinue(true); if (_autoContinueTimer.current) clearTimeout(_autoContinueTimer.current); _autoContinueTimer.current = setTimeout(() => { _setShowAutoContinue(false); const plan = incompletePlan.map(i => `- ${i.content}`).join('\\\\n'); chat.sendMessage({ text: `[TỰ ĐỘNG TIẾP TỤC] Tiếp tục từ các task chưa hoàn thành:\\\\n${plan}`, metadata: { createdAt: new Date().toISOString() }, }); }, 10000); }, onInterrupted: () => { /* no-op */ },""" if "onTaskIncomplete: (incompletePlan" not in c: old = ' onInterrupted: () => { /* no-op — handled by stop() caller */ },' if old in c: c = c.replace(old, handler) # 6. Set cancel ref function + cleanup cancel_func = """ _cancelAutoContinue.current = () => { _setShowAutoContinue(false); if (_autoContinueTimer.current) { clearTimeout(_autoContinueTimer.current); _autoContinueTimer.current = null; } }; """ if "_cancelAutoContinue.current = ()" not in c: # Insert before return c = c.replace( "\n return {\n messages: chat.messages,", cancel_func + "\n return {\n messages: chat.messages," ) # 7. Return values if "_showAutoContinue," not in c: c = c.replace( "refreshMessages,\n };", "refreshMessages,\n _showAutoContinue,\n _cancelAutoContinue,\n };" ) with open(USE_AGENT_CHAT, "w") as f: f.write(c) print("OK: useAgentChat.ts") def patch_chat_input(): with open(CHAT_INPUT, "r") as f: c = f.read() # Fix DatasetUploadResponse if "DatasetUploadResponse" in c and "inline" not in c: wrong = "import type { DatasetUploadResponse } from '@/types/agent';" if wrong in c: c = c.replace(wrong, "") c = c.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 c: c = c.replace( "import { Box, IconButton, Stack, Tooltip } from '@mui/material';", "import { Box, IconButton, Stack, Tooltip, Button } from '@mui/material';" ) # Add props to interface if "_showAutoContinue" not in c: match = re.search(r'interface ChatInputProps\s*\{[^}]*\}', c, re.DOTALL) if 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; _showAutoContinue?: boolean; _cancelAutoContinue?: React.RefObject<() => void>; }""" c = c.replace(match.group(0), new_iface) # Add pause button btn = """ {/* Pause auto-continue for free models */} {_showAutoContinue && _cancelAutoContinue?.current && ( )} """ if "Tạm dừng" not in c: c = c.replace("]*sessionId={sessionId}[^>]*initialModelPath={initialModelPath}[^>]*)(\s*/>)' new_props = r'\1 _showAutoContinue={_showAutoContinue} _cancelAutoContinue={_cancelAutoContinue}\2' c = re.sub(chat_input_pattern, new_props, c) with open(SESSION_CHAT, "w") as f: f.write(c) print("OK: SessionChat.tsx") def patch_agent_loop(): agent_loop = "/app/agent/core/agent_loop.py" if not os.path.exists(agent_loop): print(f"SKIP: {agent_loop} not found (backend)") return with open(agent_loop, "r") as f: c = f.read() if "_check_task_incomplete" not in c: func = ''' def _unfinished_plan_items(session: Session) -> list[dict[str, str]]: plan = getattr(session, "current_plan", None) or [] return [item for item in plan if item.get("status") in ("pending", "in_progress")] ''' c = c.replace("class Handlers:", func + "\n\nclass Handlers:") if "task_incomplete" not in c: check = ''' # === Auto-continue: if model stopped but plan incomplete === if not llm_result.tool_calls_acc and llm_result.content: unfinished = _unfinished_plan_items(session) if unfinished: await session.send_event( Event(event_type="task_incomplete", data={"incomplete_plan": unfinished}) ) ''' marker = " # -- End of turn --" if marker in c: c = c.replace(marker, check + "\n" + marker) with open(agent_loop, "w") as f: f.write(c) print("OK: agent_loop.py") if __name__ == "__main__": patch_events() patch_sse() patch_use_agent_chat() patch_chat_input() patch_session_chat() patch_agent_loop() print("\nDONE: V4 - everything applied")