File size: 10,734 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""2.6.1: app aberto que não expõe os botões (Electron: Claude, Discord...).

Log real: "open claude and type..." -> a janela do Claude não tem árvore de
acessibilidade; a IA tentou click_element com 14 nomes diferentes (tirados
da descrição do print), todos falhando, até o limite de etapas.

O xmessage (X puro, sem acessibilidade) faz o papel do app Electron."""

import os
import shutil
import subprocess
import sys
import time

from _util import carregar_tars

tars = carregar_tars()

# ------------------------------------------------------------
# 1. Parte com a janela de verdade (precisa de X + D-Bus + xmessage)
# ------------------------------------------------------------
tem_x = (os.environ.get("DISPLAY") and os.environ.get("DBUS_SESSION_BUS_ADDRESS")
         and shutil.which("xmessage") and not tars.acess.indisponivel())
if tem_x:
    proc = subprocess.Popen(["xmessage", "-name", "clauded", "-title", "Claude", "Olá, eu sou um app Electron"],
                            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    try:
        limite = time.time() + 10
        while time.time() < limite and not tars.encontrar_janela_por_nome("Claude"):
            time.sleep(0.2)
        abriu = []
        tars.abrir_aplicativo = lambda app: abriu.append(app) or {"sucesso": True}
        r = tars.executar_ferramenta("click_element", {"name": "New chat", "window": "Claude"})
        assert not r["sucesso"] and "does not expose" in r["mensagem"] and "type_in_element" in r["mensagem"], r
        assert not abriu, "a janela já estava aberta: não abre o app de novo"
        r2 = tars.executar_ferramenta("list_elements", {"window": "Claude"})
        if getattr(tars, "ocr", None) and tars.ocr.disponivel():     # o OCR saiu do app; o ramo fica para se voltar
            # 2.9: sem árvore, a janela é lida pela tela (OCR)
            assert r2["sucesso"] and "READ the window" in r2["mensagem"] and r2["textos"], r2
        else:
            assert not r2["sucesso"] and "Do NOT use click_element" in r2["mensagem"]
        print("OK: janela aberta sem botões expostos -> diz na hora pra usar type_in_element/click_mouse (e não reabre o app)")

        # type_in_element numa janela dessas: foca a janela, digita no campo com foco e aperta Enter
        digitado, teclas = [], []
        tars.digitar_texto = lambda txt: digitado.append(txt) or {"sucesso": True}
        tars.pressionar_tecla = lambda k: teclas.append(k) or {"sucesso": True}
        r3 = tars.executar_ferramenta("type_in_element", {"text": "Qual a capital da França?", "window": "Claude",
                                                          "submit": "true"})
        assert r3["sucesso"] and digitado == ["Qual a capital da França?"] and teclas == ["enter"], (r3, digitado, teclas)
        ativa = tars.janela_ativa_x()
        # sem gerenciador de janelas (o xvfb da suíte) nenhuma janela fica "ativa": só confere quando há um
        tem_gerenciador = subprocess.run(["xprop", "-root", "_NET_SUPPORTING_WM_CHECK"], capture_output=True,
                                         text=True).stdout.count("window id") > 0 if shutil.which("xprop") else False
        if tem_gerenciador:
            assert ativa == tars.encontrar_janela_por_nome("Claude")["id"], "a janela do app veio pra frente antes de digitar"
        print("OK: type_in_element(window, submit=true) num app sem botões: foca a janela, digita e envia")
    finally:
        proc.kill()
else:
    print("PULADO (parte 1): precisa de DISPLAY, D-Bus, xmessage e acessibilidade")

# ------------------------------------------------------------
# 2. O loop do log: a IA insiste no click_element com nomes diferentes
# ------------------------------------------------------------
tars.carregar_modelo = lambda m, silencioso=False: True
tars.contexto_ia = lambda: 8192
tars.modo_modelo = "MANUAL"
tars.catalogar_modelos = lambda forcar=False: {}
nomes = iter(f"Botão {i}" for i in range(100))
turnos = []


def turno(modelo, msgs, tools, **kw):
    turnos.append(bool(tools))
    if not tools:
        return {"content": "Não consegui clicar: o app não mostra os botões. Clique você no campo.", "tool_calls": []}, None
    return {"content": "", "tool_calls": [{"function": {"name": "click_element",
                                                         "arguments": {"name": next(nomes), "window": "Claude"}}}]}, None


tars._executar_turno_streaming = turno
tars._DISPATCH_FERRAMENTAS["click_element"] = lambda a: {"sucesso": False, "mensagem": f"No element named '{a['name']}'."}
tars.conversa = []
resposta = tars.perguntar_modelo("qwen3:8b", "open claude and type hello", tarefa="acao")
assert "Não consegui" in resposta, resposta
assert turnos.count(True) == tars.LIMITE_FALHAS_SEGUIDAS and turnos[-1] is False, turnos
resultados = [m["content"] for m in tars.obter_sessao("qwen3:8b") if m.get("role") == "tool"]
assert "STOP" not in resultados[1] and "STOP: click_element failed 3 times" in resultados[2], resultados[:3]
print(f"OK: 3 falhas seguidas -> 'mude de estratégia'; {tars.LIMITE_FALHAS_SEGUIDAS} -> para e explica ao usuário "
      f"(antes: 20 etapas)")

# 3. A mesma chamada que já falhou nem roda de novo
rodou = []
tars._DISPATCH_FERRAMENTAS["click_element"] = lambda a: rodou.append(a) or {"sucesso": False, "mensagem": "nope."}
chamadas = iter([("click_element", {"name": "Send", "window": "Claude"})] * 2 + [None])


def turno2(modelo, msgs, tools, **kw):
    proxima = next(chamadas)
    if proxima is None or not tools:
        return {"content": "ok", "tool_calls": []}, None
    return {"content": "", "tool_calls": [{"function": {"name": proxima[0], "arguments": proxima[1]}}]}, None


tars._executar_turno_streaming = turno2
tars.perguntar_modelo("qwen3:8b", "type hello in claude", tarefa="acao")
ultimo = [m["content"] for m in tars.obter_sessao("qwen3:8b") if m.get("role") == "tool"][-1]
assert len(rodou) == 1 and "already tried exactly this" in ultimo, (rodou, ultimo)
print("OK: repetir exatamente a mesma chamada que falhou não roda de novo (a IA é avisada)")

# 4. Descrição da tela de um app sem botões expostos: manda clicar por coordenada
tars._descrever_print = lambda img, pedido, modelo: ("Message box at (400, 700). Send button at (760, 700).", "gemma4:e4b")
tars._marcar_sem_arvore("Claude")
msg = tars._print_em_texto("x", "type hello", "qwen3:8b", "Claude")
assert "click_mouse(x, y, target=" in msg and "click_element(name)" not in msg, msg
msg_outro = tars._print_em_texto("x", "type hello", "qwen3:8b", "calculator")
assert "click_element(name)" in msg_outro
print("OK: com um app sem botões expostos, a descrição da tela manda usar click_mouse(x, y, target=...) e type_text")

# 5. Abrir app pede a árvore de acessibilidade ao Chromium/Electron
envs = []


class Popen:
    def __init__(self, *a, **kw):
        envs.append(kw.get("env") or {})
        self.returncode = 0

    def poll(self):
        return 0


fonte = carregar_tars()  # abrir_aplicativo original (a parte 1 trocou o do outro)
fonte.subprocess.Popen = Popen
fonte.encontrar_app = lambda app: {"comando": ["claude-desktop"], "nome": "Claude", "tipo": "desktop"}
fonte.listar_janelas_x = lambda: None
fonte._processo_ativo = lambda termos: True
fonte.abrir_aplicativo("claude")
assert envs and envs[-1].get("ACCESSIBILITY_ENABLED") == "1", envs
print("OK: apps são abertos com ACCESSIBILITY_ENABLED=1 (Electron/Chromium passam a expor os botões)")

# 6. Log real da 2.6.1: uma falha e a IA manda o usuário fazer na mão
chamadas_ia = iter([
    ("tool", "click_element", {"name": "Ask a question", "window": "Claude"}),
    ("texto", "I cannot directly interact with the Claude app's buttons. To ask a question, focus on the text box and type your question, then press Enter."),
    ("tool", "type_in_element", {"text": "What is the capital of France?", "window": "Claude", "submit": True}),
    ("texto", "Done: I asked Claude 'What is the capital of France?'."),
])
recebeu = []


def turno3(modelo, msgs, tools, **kw):
    recebeu.append(msgs[-1]["content"])
    tipo, *resto = next(chamadas_ia)
    if tipo == "texto":
        return {"content": resto[0], "tool_calls": []}, None
    return {"content": "", "tool_calls": [{"function": {"name": resto[0], "arguments": resto[1]}}]}, None


tars._executar_turno_streaming = turno3
tars._DISPATCH_FERRAMENTAS["click_element"] = lambda a: {"sucesso": False, "mensagem": tars._mensagem_sem_arvore("Claude")}
escrito = []
tars._DISPATCH_FERRAMENTAS["type_in_element"] = lambda a: escrito.append(a) or {"sucesso": True, "mensagem": "SUCCESS: typed and sent."}
resposta = tars.perguntar_modelo("qwen3:8b", "abra o claude e faça uma pergunta simples", tarefa="acao")
assert resposta.startswith("Done") and escrito and escrito[0]["submit"] is True, (resposta, escrito)
assert any("Don't hand the task back" in r for r in recebeu)
assert not any("I cannot directly interact" in (m.get("content") or "") for m in tars.obter_sessao("qwen3:8b"))
print("OK: 'foque a caixa e digite você' depois de uma falha -> a IA é mandada fazer ela mesma, e faz")

# 7. Electron/Chromium detectado -> abre com --force-renderer-accessibility
import tempfile  # noqa: E402

pasta = tempfile.mkdtemp(prefix="opentars-electron-")
for arquivo, conteudo in (("claude-desktop", b"\x7fELF..."), ("resources.pak", b"")):
    with open(os.path.join(pasta, arquivo), "wb") as f:
        f.write(conteudo)
script = os.path.join(pasta, "discord")
with open(script, "wb") as f:
    f.write(b"#!/bin/sh\nexec /usr/lib/electron/electron /usr/lib/discord/app.asar \"$@\"\n")
comum = os.path.join(tempfile.mkdtemp(), "gnome-calculator")
with open(comum, "wb") as f:
    f.write(b"\x7fELF-normal")
assert fonte._eh_electron([os.path.join(pasta, "claude-desktop")]), "resources.pak ao lado: Electron"
assert fonte._eh_electron([script]), "script lançador que chama o electron"
assert not fonte._eh_electron([comum]) and not fonte._eh_electron(["flatpak", "run", "x"])
envs.clear()
chamados = []


class PopenComando(Popen):
    def __init__(self, comando, *a, **kw):
        chamados.append(list(comando))
        super().__init__(comando, *a, **kw)


fonte.subprocess.Popen = PopenComando
fonte.encontrar_app = lambda app: {"comando": [os.path.join(pasta, "claude-desktop")], "nome": "Claude", "tipo": "desktop"}
fonte.abrir_aplicativo("claude")
fonte.encontrar_app = lambda app: {"comando": [comum], "nome": "Calculadora", "tipo": "desktop"}
fonte.abrir_aplicativo("calculadora")
assert chamados[0][-1] == "--force-renderer-accessibility" and chamados[1] == [comum], chamados
print("OK: app Electron é aberto com --force-renderer-accessibility (os outros, do jeito normal)")

print("\nTUDO OK.")