ml-intern / patch_auto_continue_v7.py
bep40's picture
V7: Precise patches with exact source matching
68e54e0 verified
Raw
History Blame Contribute Delete
9.5 kB
"""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<ReturnType<typeof setTimeout> | 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> | 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> | 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 <JobsUpgradeDialog>
if "T\u1ea1m d\u1eebng" not in c:
btn = """
{/* Auto-continue pause for free models */}
{_showAc && _acCancel && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
<Button
variant="outlined"
size="small"
onClick={_acCancel}
sx={{ textTransform: 'none', fontSize: '0.7rem', opacity: 0.7, '&:hover': { opacity: 1 } }}
>
T\u1ea1m d\u1eebng t\u1ef1 \u0111\u1ed9ng ti\u1ebfp t\u1ee5c (10s)
</Button>
</Box>
)}
"""
if "<JobsUpgradeDialog" in c:
c = c.replace("<JobsUpgradeDialog", btn + "\n <JobsUpgradeDialog")
with open(input_tsx, 'w') as f: f.write(c)
print("OK: ChatInput.tsx")
# ========== PATCH 5: SessionChat.tsx - pass props down ==========
session_tsx = "/source/frontend/src/components/SessionChat.tsx"
def patch_session():
if not os.path.exists(session_tsx):
print(f"SKIP: {session_tsx}")
return
with open(session_tsx) as f:
c = f.read()
if "_showAc" not in c:
old = "<ChatInput "
new = "<ChatInput _showAc={_showAc} _acCancel={_acCancel} "
c = c.replace(old, new)
with open(session_tsx, 'w') as f: f.write(c)
print("OK: SessionChat.tsx")
# ========== PATCH 6: agent_loop.py - detection ==========
agent_py = "/app/agent/core/agent_loop.py"
def patch_agent():
if not os.path.exists(agent_py):
print(f"SKIP: {agent_py}")
return
with open(agent_py) as f:
c = f.read()
# Add helper function
if "_unfinished_plan" not in c:
fn = """
def _unfinished_plan(s: Session) -> 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")