"""V7: PRECISE patch - exact string matching with source code. Adds auto-continue pause button for free models. All patches use exact string content from the original source files for reliable replacement. """ import os, sys # ========== PATCH 1: events.ts - add 'task_incomplete' to EventType ========== events_ts = "/source/frontend/src/types/events.ts" def patch_events(): with open(events_ts) as f: c = f.read() if "'task_incomplete'" not in c: c = c.replace( " | 'interrupted'", " | 'interrupted'\n | 'task_incomplete'" ) with open(events_ts, 'w') as f: f.write(c) print("OK: events.ts") # ========== PATCH 2: SSE transport - add onTaskIncomplete to interface + handler ========== sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts" def patch_sse(): with open(sse_ts) as f: c = f.read() # Add 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 before default if "case 'task_incomplete'" not in c: case_handler = """ case 'task_incomplete': sideChannel.onTaskIncomplete( (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [], ); break; default:""" c = c.replace("\n default:", "\n" + case_handler) with open(sse_ts, 'w') as f: f.write(c) print("OK: sse-chat-transport.ts") # ========== PATCH 3: useAgentChat.ts - add state + handler + return ========== hook_ts = "/source/frontend/src/hooks/useAgentChat.ts" def patch_hook(): with open(hook_ts) as f: c = f.read() # 1. Add useState to imports if "useState" not in c: c = c.replace( "import { useCallback, useEffect, useMemo, useRef } from 'react';", "import { useCallback, useEffect, useMemo, useRef, useState } from 'react';" ) # 2. Add state variables after callbacksRef state_block = """ // Auto-continue state for free models const _acTimerRef = useRef | null>(null); const [_showAc, _setShowAc] = useState(false);""" if "_acTimerRef" not in c: c = c.replace( "callbacksRef.current = { onReady, onError, onSessionDead };", "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block ) # 3. 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;" ) # 4. Add onTaskIncomplete handler in sideChannel object # This inserts BEFORE the existing onInterrupted handler handler = """ onTaskIncomplete: (incompletePlan: Array<{ id: string; content: string; status: string }>) => { _setShowAc(true); if (_acTimerRef.current) clearTimeout(_acTimerRef.current); _acTimerRef.current = setTimeout(() => { _setShowAc(false); const planStr = incompletePlan.map(i => `- ${i.content}`).join('\\\\n'); const setMsgs = chatActionsRef.current.setMessages; if (setMsgs) { const msgs = chatActionsRef.current.messages; setMsgs([...msgs, { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}` }], content: '' }]); } }, 10000); }, onInterrupted: () => { /* no-op */ },""" if "onTaskIncomplete: (incompletePlan" not in c: old_interrupted = ' onInterrupted: () => { /* no-op \u2014 handled by stop() caller */ },' if old_interrupted in c: c = c.replace(old_interrupted, handler) else: # Try simpler version old2 = ' onInterrupted: () => { /* no-op */ },' c = c.replace(old2, handler) # 5. Add _acCancel function + return values cancel_func = """ const _acCancel = useCallback(() => { _setShowAc(false); if (_acTimerRef.current) { clearTimeout(_acTimerRef.current); _acTimerRef.current = null; } }, []);""" if "_acCancel" not in c: c = c.replace("\n return {\n messages: chat.messages,", cancel_func + "\n return {\n messages: chat.messages,") # 6. Return values if "_showAc," not in c: c = c.replace( "refreshMessages,\n };", "refreshMessages,\n _showAc,\n _acCancel,\n };" ) with open(hook_ts, 'w') as f: f.write(c) print("OK: useAgentChat.ts") # ========== PATCH 4: ChatInput.tsx - add pause button ========== input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx" def patch_chat_input(): with open(input_tsx) as f: c = f.read() # 1. Add Button import (it's missing from the original imports) if "Button" not in c: c = c.replace( " Tooltip,", " Tooltip,\n Button," ) # 2. Add props to interface if "_showAc" not in c: old_interface_end = """interface ChatInputProps { sessionId?: string; initialModelPath?: string | null; onSend: (text: string) => void; onStop?: () => void; onDatasetUploaded?: () => Promise | boolean; isProcessing?: boolean; disabled?: boolean; placeholder?: string; }""" new_interface = """interface ChatInputProps { sessionId?: string; initialModelPath?: string | null; onSend: (text: string) => void; onStop?: () => void; onDatasetUploaded?: () => Promise | boolean; isProcessing?: boolean; disabled?: boolean; placeholder?: string; _showAc?: boolean; _acCancel?: () => void; }""" c = c.replace(old_interface_end, new_interface) # 3. Add destructured props in function signature if "_showAc" not in c: old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {" new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {" if old_props not in c: old_props2 = "placeholder = 'Ask anything...' }: ChatInputProps) {" new_props2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {" c = c.replace(old_props2, new_props2) else: c = c.replace(old_props, new_props) # 4. Add pause button after the model badge area, before if "T\u1ea1m d\u1eebng" not in c: btn = """ {/* Auto-continue pause for free models */} {_showAc && _acCancel && ( )} """ if " 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")] """ c = c.replace("class Handlers:", fn + "\n\nclass Handlers:") # Add detection if "task_incomplete" not in c: check = """ # Auto-continue detection 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 c: c = c.replace(marker, check + "\n" + marker) with open(agent_py, 'w') as f: f.write(c) print("OK: agent_loop.py") if __name__ == "__main__": patch_events() patch_sse() patch_hook() patch_chat_input() patch_session() patch_agent() print("\nDONE: V7 - precise patches applied")