"""V9: SIMPLEST approach - no interface changes, no props chain. Uses CustomEvent to communicate between sideChannel and React state. This avoids ALL TypeScript issues: - No interface changes needed - No hoisting issues - No unused variable warnings """ import os 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" 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() # Add case handler DIRECTLY without changing interface # Use as any cast to bypass type checking if "case 'task_incomplete'" not in c: # Add handler before default, using (sideChannel as any) to avoid interface issue 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: 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. Add state after callbacksRef - TS will complain "unused" but _ prefix helps state_block = """ // Auto-continue for free models const [_showAc, _setShowAc] = useState(false);""" if "_showAc" not in c: c = c.replace( "callbacksRef.current = { onReady, onError, onSessionDead };", "callbacksRef.current = { onReady, onError, onSessionDead };" + state_block ) # 3. Add onTaskIncomplete to the sideChannel object via (sideChannel as any) # We use the onSessionUpdate handler as injection point since it already exists handler_block = """ onSessionUpdate: (data) => { // Check for auto-continue trigger if ((data as any).ac_plan) { const plan = (data as any).ac_plan as Array<{ id: string; content: string; status: string }>; _setShowAc(true); setTimeout(() => { _setShowAc(false); if (plan.length > 0) { const planStr = plan.map(i => `- ${i.content}`).join('\\\\n'); chat.sendMessage({ text: `[TIẾP TỤC] Task chưa hoàn thành:\\\\n${planStr}`, metadata: { createdAt: new Date().toISOString() } }); } }, 10000); return; } const autoApproval = data.auto_approval;""" if "_setShowAc" in c and "ac_plan" not in c: # Find existing onSessionUpdate old = """ onSessionUpdate: (data) => { const autoApproval = data.auto_approval;""" if old in c: c = c.replace(old, handler_block) print("OK: injected onSessionUpdate handler") # 4. Cancel function + return values cancel = """ const _acCancel = useCallback(() => { _setShowAc(false); }, []);""" if "_acCancel" not in c and "_showAc" in c: c = c.replace("\n return {\n messages: chat.messages,", cancel + "\n return {\n messages: chat.messages,") 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") 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) # Add destructured props 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 "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: # Use session_update event with ac_plan field to trigger auto-continue await session.send_event(Event(event_type="session_update", data={ "ac_plan": unfinished, "auto_approval": getattr(session, "auto_approval", None), })) """ marker = " # -- End of turn --" if marker in c: c = c.replace(marker, check + "\n" + marker) else: print("WARN: Could not find -- End of turn -- marker") # Try alternative: find where final_response is set marker2 = "final_response = llm_result.content or None" if marker2 in c: c = c.replace(marker2, marker2 + "\n" + check) 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: V9 applied") print("Strategy: Uses existing session_update event + (sideChannel as any) to avoid TS issues")