File size: 6,861 Bytes
a2bdf09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
"""V11: Auto-continue feature - clean single-file patch.

Backend: agent_loop.py detects model stopped mid-task, sends event.
Frontend: useAgentChat listens via onSessionUpdate, shows pause button.
All TS errors avoided via (as any) casts where needed.
"""
import os

ROOT = "/source/frontend/src"
EVENTS = f"{ROOT}/types/events.ts"
SSE = f"{ROOT}/lib/sse-chat-transport.ts"
HOOK = f"{ROOT}/hooks/useAgentChat.ts"
INPUT = f"{ROOT}/components/Chat/ChatInput.tsx"
SESS = f"{ROOT}/components/SessionChat.tsx"
AGENT_LOOP = "/app/agent/core/agent_loop.py"

def p(f, s):
    print(f"OK: {f}" if s else f"FAIL: {f}")

# --- PATCH 1: events.ts ---
with open(EVENTS) as f: c = f.read()
r1 = "'task_incomplete'" not in c
if r1:
    c = c.replace("  | 'interrupted'", "  | 'interrupted'\n  | 'task_incomplete'")
    with open(EVENTS, 'w') as f: f.write(c)
p("events.ts", r1)

# --- PATCH 2: sse-chat-transport.ts ---
with open(SSE) as f: c = f.read()
r2 = "case 'task_incomplete'" not in c
if r2:
    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" + block)
    with open(SSE, 'w') as f: f.write(c)
p("sse.ts", r2)

# --- PATCH 3: useAgentChat.ts ---
with open(HOOK) as f: lines = f.readlines()
r3 = True
new_lines = []
state_done = False
handler_done = False
efx_done = False
ret_done = False

for line in lines:
    if "import { useCallback, useEffect, useMemo, useRef } from 'react';" in line:
        line = line.replace("useRef }", "useRef, useState }")
    
    if "callbacksRef.current = { onReady, onError, onSessionDead };" in line and not state_done:
        new_lines.append(line)
        new_lines.append("  // Auto-continue state\n")
        new_lines.append("  const [_showAc, _setShowAc] = useState(false);\n")
        new_lines.append("  const _acRef = useRef<Array<{ id: string; content: string; status: string }>>([]);\n")
        state_done = True
        continue
    
    if "onSessionUpdate: (data) => {" in line and not handler_done:
        new_lines.append(line)
        new_lines.append("        // Auto-continue check\n")
        new_lines.append("        if ((data as any).ac_plan) {\n")
        new_lines.append("          _acRef.current = (data as any).ac_plan;\n")
        new_lines.append("          _setShowAc(true);\n")
        new_lines.append("          return;\n")
        new_lines.append("        }\n")
        handler_done = True
        continue
    
    stripped = line.strip()
    if stripped == "return {" and not efx_done:
        new_lines.append("  const _acCancel = useCallback(() => { _setShowAc(false); }, []);\n")
        new_lines.append("  useEffect(() => {\n")
        new_lines.append("    if (!_showAc) return;\n")
        new_lines.append("    const plan = _acRef.current;\n")
        new_lines.append("    const timer = setTimeout(() => {\n")
        new_lines.append("      const s = chatActionsRef.current.setMessages;\n")
        new_lines.append("      const m = chatActionsRef.current.messages;\n")
        new_lines.append("      if (s && plan.length > 0) {\n")
        new_lines.append("        const t = plan.map(i => `- ${i.content}`).join('\\n');\n")
        new_lines.append("        const nu = { id: 'ac-'+Date.now(), role: 'user' as const, parts: [{ type: 'text' as const, text: `[TIEP TUC] Task chua hoan thanh:\\n${t}` }], content: '' };\n")
        new_lines.append("        s([...m, nu]);\n")
        new_lines.append("      }\n")
        new_lines.append("      _setShowAc(false);\n")
        new_lines.append("    }, 10000);\n")
        new_lines.append("    return () => clearTimeout(timer);\n")
        new_lines.append("  }, [_showAc]);\n")
        efx_done = True
    
    if "refreshMessages," in line and not ret_done:
        new_lines.append(line)
        new_lines.append("    _showAc,\n")
        new_lines.append("    _acCancel,\n")
        ret_done = True
        continue
    
    new_lines.append(line)

if not all([state_done, handler_done, efx_done, ret_done]):
    r3 = False
    print(f"  MISSING: state={state_done} handler={handler_done} effect={efx_done} ret={ret_done}")
else:
    with open(HOOK, 'w') as f: f.writelines(new_lines)
p("useAgentChat.ts", r3)

# --- PATCH 4: ChatInput.tsx ---
with open(INPUT) as f: c = f.read()
r4 = True
if "Button" not in c:
    c = c.replace("  Tooltip,", "  Tooltip,\n  Button,")

if "_showAc" not in c:
    old = "placeholder?: string;"
    new = "placeholder?: string;\n  _showAc?: boolean;\n  _acCancel?: () => void;"
    c = c.replace(old, new)
    
    old2 = "placeholder = 'Ask anything...' }: ChatInputProps) {"
    new2 = "placeholder = 'Ask anything...', _showAc = false, _acCancel }: ChatInputProps) {"
    c = c.replace(old2, new2)
    
    btn = """
        {_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 } }}>
              Tam dung tu dong tiep tuc (10s)
            </Button>
          </Box>
        )}
"""
    c = c.replace("<JobsUpgradeDialog", btn + "\n        <JobsUpgradeDialog")
else:
    r4 = False

with open(INPUT, 'w') as f: f.write(c)
p("ChatInput.tsx", r4)

# --- PATCH 5: SessionChat.tsx ---
if os.path.exists(SESS):
    with open(SESS) as f: c = f.read()
    r5 = "_showAc" not in c
    if r5:
        c = c.replace("<ChatInput ", "<ChatInput _showAc={_showAc} _acCancel={_acCancel} ")
        with open(SESS, 'w') as f: f.write(c)
    p("SessionChat.tsx", r5)
else:
    print("SKIP: SessionChat.tsx")

# --- PATCH 6: agent_loop.py ---
if os.path.exists(AGENT_LOOP):
    with open(AGENT_LOOP) as f: c = f.read()
    r6 = True
    if "_unfinished_plan" not in c:
        fn = """
def _unfinished_plan(s):
    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:
                    await session.send_event(Event(event_type="session_update", data={
                        "ac_plan": unfinished,
                    }))
"""
        marker = "  # -- End of turn --"
        c = c.replace(marker, check + "\n" + marker)
    else:
        r6 = False
    
    with open(AGENT_LOOP, 'w') as f: f.write(c)
    p("agent_loop.py", r6)
else:
    print("SKIP: agent_loop.py")

print("\nDONE: V11")