"""V10: Fixes scope issue with chat.sendMessage. Uses: ref to store plan, useEffect + chatActionsRef to send.""" import os, re events_ts = "/source/frontend/src/types/events.ts" sse_ts = "/source/frontend/src/lib/sse-chat-transport.ts" hook_ts = "/source/frontend/src/hooks/useAgentChat.ts" input_tsx = "/source/frontend/src/components/Chat/ChatInput.tsx" session_tsx = "/source/frontend/src/components/SessionChat.tsx" agent_py = "/app/agent/core/agent_loop.py" 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") def patch_sse(): with open(sse_ts) as f: c = f.read() if "case 'task_incomplete'" not in c: case_block = """ case 'task_incomplete': (sideChannel as any).onTaskIncomplete( (event.data?.incomplete_plan as Array<{ id: string; content: string; status: string }>) || [], ); break; default:""" c = c.replace("\n default:", "\n" + case_block) with open(sse_ts, 'w') as f: f.write(c) print("OK: sse.ts") def patch_hook(): with open(hook_ts) as f: lines = f.readlines() # Find key insertion points by line number result = [] state_added = False handler_added = False effect_added = False cancel_added = False return_added = False for i, line in enumerate(lines): # 1. Add useState import if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line and "useState" not in line: line = line.replace("useRef }", "useRef, useState }") # 2. Add state after callbacksRef if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_added: result.append(line) result.append(' // Auto-continue for free models\n') result.append(' const [_showAc, _setShowAc] = useState(false);\n') result.append(' const _acRef = useRef>([]);\n') state_added = True continue # 3. Add handler in sideChannel's onSessionUpdate if "onSessionUpdate: (data) => {" in line and not handler_added: result.append(line) result.append(' // Auto-continue: check for ac_plan\n') result.append(' if ((data as any).ac_plan) {\n') result.append(' _acRef.current = (data as any).ac_plan;\n') result.append(' _setShowAc(true);\n') result.append(' return;\n') result.append(' }\n') handler_added = True continue # 4. Add cancel function + effect before return stripped = line.strip() if stripped == "return {" and not cancel_added: result.append(' // Auto-continue cancel\n') result.append(' const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n') cancel_added = True # Add effect that uses chatActionsRef to send message result.append('\n') result.append(' // Auto-continue effect: 10s timer, sends via chatActionsRef\n') result.append(' useEffect(() => {\n') result.append(' if (!_showAc) return;\n') result.append(' const plan = _acRef.current;\n') result.append(' const timer = setTimeout(() => {\n') result.append(' const s = chatActionsRef.current.setMessages;\n') result.append(' const m = chatActionsRef.current.messages;\n') result.append(' if (s && plan.length > 0) {\n') result.append(" const t = plan.map(i => `- ${i.content}`).join('\\\\n');\n") result.append(" s([...m, { 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${t}` }], content: '' }]);\n") result.append(' }\n') result.append(' _setShowAc(false);\n') result.append(' }, 10000);\n') result.append(' return () => clearTimeout(timer);\n') result.append(' }, [_showAc]);\n') effect_added = True # 5. Return values if "refreshMessages," in line and not return_added: result.append(line) result.append(' _showAc,\n') result.append(' _acCancel,\n') return_added = True continue result.append(line) with open(hook_ts, 'w') as f: f.writelines(result) print(f"OK: useAgentChat.ts (added={state_added}/{handler_added}/{cancel_added}/{effect_added}/{return_added})") def patch_chat_input(): with open(input_tsx) as f: c = f.read() if "Button" not in c: c = c.replace(" Tooltip,", " Tooltip,\n Button,") if "_showAc" not in c: old_iface = """interface ChatInputProps { sessionId?: string; initialModelPath?: string | null; onSend: (text: string) => void; onStop?: () => void; onDatasetUploaded?: () => Promise | boolean; isProcessing?: boolean; disabled?: boolean; placeholder?: string; }""" new_iface = """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_iface, new_iface) old_props = "placeholder = 'Ask anything...' }: ChatInputProps) {" new_props = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {" c = c.replace(old_props, new_props) if "T\u1ea1m d\u1eebng" not in c: btn = """ {_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:") if "ac_plan" not in c: check = """ if not llm_result.tool_calls_acc and llm_result.content: unfinished = _unfinished_plan(session) if unfinished: current_ac = getattr(session, "auto_approval", None) await session.send_event(Event(event_type="session_update", data={ "ac_plan": unfinished, "auto_approval": current_ac, })) """ 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: V10")