chocolatito-code 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +99 -15
- package/dist/agent/salidaDelAgente.js +10 -24
- package/dist/agent/toolGate.js +6 -1
- package/dist/config/engine.d.ts +21 -1
- package/dist/config/engine.js +22 -1
- package/dist/tools/browserExtension.js +118 -3
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- package/dist/ui/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +50 -2
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.js +9 -1
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/spinner.d.ts +15 -8
- package/dist/ui/spinner.js +80 -42
- package/extension/background.js +9 -3
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/ORIGEN.md +48 -0
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -68,6 +68,155 @@ function ambiguityNote(p, choice) {
|
|
|
68
68
|
function fail(action, msg) {
|
|
69
69
|
return `Error en computer_use (${action}): ${msg}`;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* El estado de la ventana DESPUES de actuar, en el mismo resultado.
|
|
73
|
+
*
|
|
74
|
+
* POR QUE
|
|
75
|
+
*
|
|
76
|
+
* El ciclo de trabajo del agente es mirar, actuar, mirar. Hasta ahora eso
|
|
77
|
+
* costaba TRES viajes al modelo:
|
|
78
|
+
*
|
|
79
|
+
* ui_snapshot -> llamada al modelo
|
|
80
|
+
* ui_click -> llamada al modelo (devolvia solo "Activado [3]")
|
|
81
|
+
* ui_snapshot -> llamada al modelo (para ver que habia pasado)
|
|
82
|
+
*
|
|
83
|
+
* Y cada viaje son entre tres y diez segundos. Medido en esta maquina, un
|
|
84
|
+
* ui_snapshot cuesta 43 ms: es GRATIS al lado de una llamada al modelo. Asi que
|
|
85
|
+
* el tercer viaje se hacia solo para pedir algo que el ordenador podia haber
|
|
86
|
+
* adjuntado desde el principio.
|
|
87
|
+
*
|
|
88
|
+
* Devolviendo el estado nuevo con la accion, el ciclo pasa de tres viajes a
|
|
89
|
+
* dos. En una tarea de diez pasos son diez viajes menos: medio minuto largo.
|
|
90
|
+
*
|
|
91
|
+
* CUANDO NO SE HACE
|
|
92
|
+
*
|
|
93
|
+
* Si la accion fallo no se adjunta nada: lo que hace falta entonces es el
|
|
94
|
+
* motivo del fallo, y una lista de controles solo lo entierra.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Mirar una ventana muchas veces sin pagarlo muchas veces.
|
|
98
|
+
*
|
|
99
|
+
* EL PROBLEMA
|
|
100
|
+
*
|
|
101
|
+
* Leer el arbol de accesibilidad cuesta 43 ms: es gratis. Lo que no es gratis
|
|
102
|
+
* es MANDARSELO al modelo. Un ui_snapshot de una ventana normal son cien lineas
|
|
103
|
+
* que entran en el historial y se vuelven a pagar en cada turno siguiente. Asi
|
|
104
|
+
* que mirar seguido -que es lo que hace falta para trabajar en tiempo real- sale
|
|
105
|
+
* caro por el lado del contexto, no por el de la maquina.
|
|
106
|
+
*
|
|
107
|
+
* LA IDEA
|
|
108
|
+
*
|
|
109
|
+
* Casi nunca cambia la ventana entera. Se pulsa un boton y se habilita otro; se
|
|
110
|
+
* escribe en un campo y cambia su valor. De cien controles cambian dos.
|
|
111
|
+
*
|
|
112
|
+
* Guardando la ultima foto de cada ventana se puede devolver solo la diferencia:
|
|
113
|
+
*
|
|
114
|
+
* CAMBIOS EN "Calculadora" (3 de 47 controles):
|
|
115
|
+
* ~ [12] Text "0" -> "42"
|
|
116
|
+
* + [31] Button "Igual" (nuevo)
|
|
117
|
+
* - Button "Limpiar" (ya no esta)
|
|
118
|
+
*
|
|
119
|
+
* Tres lineas en vez de cuarenta y siete. Eso es lo que hace viable mirar
|
|
120
|
+
* despues de cada accion sin inflar el contexto.
|
|
121
|
+
*
|
|
122
|
+
* CUANDO SE DEVUELVE LA FOTO ENTERA
|
|
123
|
+
*
|
|
124
|
+
* La primera vez, cuando no hay con que comparar. Y cuando cambia tanto que el
|
|
125
|
+
* resumen ya no es mas corto que la lista: si cambio media ventana, es que se
|
|
126
|
+
* abrio otra pantalla, y ahi lo util es verla entera.
|
|
127
|
+
*/
|
|
128
|
+
/** La ultima foto de cada ventana, para poder contar solo lo que cambio. */
|
|
129
|
+
const ultimaFoto = new Map();
|
|
130
|
+
/** Identidad de un control que NO depende de su numero de ref, que se renumera. */
|
|
131
|
+
function claveDe(e) {
|
|
132
|
+
return `${e.role}::${e.name}`;
|
|
133
|
+
}
|
|
134
|
+
function compararCon(clave, els) {
|
|
135
|
+
const ahora = new Map();
|
|
136
|
+
for (const e of els)
|
|
137
|
+
ahora.set(claveDe(e), String(e.value ?? ""));
|
|
138
|
+
const antes = ultimaFoto.get(clave);
|
|
139
|
+
ultimaFoto.set(clave, ahora);
|
|
140
|
+
if (!antes)
|
|
141
|
+
return { hayFoto: false, texto: "", cuantos: 0 };
|
|
142
|
+
const lineas = [];
|
|
143
|
+
const porClave = new Map(els.map((e) => [claveDe(e), e]));
|
|
144
|
+
for (const [k, valor] of ahora) {
|
|
145
|
+
const e = porClave.get(k);
|
|
146
|
+
if (!antes.has(k)) {
|
|
147
|
+
lineas.push(` + [${e.ref}] ${e.role} "${e.name}" (nuevo)`);
|
|
148
|
+
}
|
|
149
|
+
else if (antes.get(k) !== valor) {
|
|
150
|
+
lineas.push(` ~ [${e.ref}] ${e.role} "${e.name}": "${antes.get(k)}" -> "${valor}"`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const k of antes.keys()) {
|
|
154
|
+
if (!ahora.has(k)) {
|
|
155
|
+
const [role, name] = k.split("::");
|
|
156
|
+
lineas.push(` - ${role} "${name}" (ya no esta)`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { hayFoto: true, texto: lineas.join("\n"), cuantos: lineas.length };
|
|
160
|
+
}
|
|
161
|
+
/** Cuantos controles se adjuntan. Bastantes para decidir, pocos para no inundar. */
|
|
162
|
+
const CONTROLES_TRAS_ACTUAR = 40;
|
|
163
|
+
/**
|
|
164
|
+
* Espera a que la ventana se quede quieta, y no un tiempo fijo.
|
|
165
|
+
*
|
|
166
|
+
* Un clic puede abrir un menu, cargar una lista o no hacer nada visible. Dormir
|
|
167
|
+
* 700 ms "por si acaso" es lento cuando no pasa nada y corto cuando si pasa.
|
|
168
|
+
* Se mira dos veces seguidas: si el arbol de controles sale igual, la ventana
|
|
169
|
+
* ya termino.
|
|
170
|
+
*/
|
|
171
|
+
async function esperarQuieto(args) {
|
|
172
|
+
let anterior = "";
|
|
173
|
+
for (let intento = 0; intento < 4; intento++) {
|
|
174
|
+
const res = await winHost.send("ui_snapshot", { ...args, max: CONTROLES_TRAS_ACTUAR, interactiveOnly: true }, 20_000);
|
|
175
|
+
if (!res.ok)
|
|
176
|
+
return null;
|
|
177
|
+
const huella = JSON.stringify((res.elements || []).map((e) => [e.role, e.name, e.value]));
|
|
178
|
+
if (huella === anterior)
|
|
179
|
+
return res;
|
|
180
|
+
anterior = huella;
|
|
181
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
182
|
+
}
|
|
183
|
+
// Cuatro vueltas sin quedarse quieta: la ventana esta animando algo. Se
|
|
184
|
+
// devuelve lo ultimo visto, que es mas util que nada.
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/** Lo que se pega al resultado de una accion. Cadena vacia si no hay nada que contar. */
|
|
188
|
+
async function estadoTrasActuar(args) {
|
|
189
|
+
let res;
|
|
190
|
+
try {
|
|
191
|
+
res = await esperarQuieto(args);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
196
|
+
const els = res?.elements || [];
|
|
197
|
+
if (els.length === 0)
|
|
198
|
+
return "";
|
|
199
|
+
// Si hay foto anterior de esta ventana, con decir lo que cambio basta: tres
|
|
200
|
+
// lineas en vez de cuarenta, y el contexto no se infla en cada clic.
|
|
201
|
+
const cambios = compararCon(String(res.title || ""), els);
|
|
202
|
+
if (cambios.hayFoto && cambios.cuantos > 0 && cambios.cuantos <= els.length / 2) {
|
|
203
|
+
return `\n\nCAMBIOS EN "${res.title}" (${cambios.cuantos} de ${els.length} controles):\n${cambios.texto}`;
|
|
204
|
+
}
|
|
205
|
+
if (cambios.hayFoto && cambios.cuantos === 0) {
|
|
206
|
+
return `\n\nLa ventana "${res.title}" no cambio en nada visible.`;
|
|
207
|
+
}
|
|
208
|
+
// Primera vez, o cambio tanto que el resumen ya no seria mas corto: entonces
|
|
209
|
+
// lo util es la lista entera, porque eso significa que se abrio otra pantalla.
|
|
210
|
+
const filas = els
|
|
211
|
+
.map((e) => {
|
|
212
|
+
const val = e.value ? ` = "${e.value}"` : "";
|
|
213
|
+
const off = e.enabled ? "" : " [deshabilitado]";
|
|
214
|
+
return ` [${e.ref}] ${e.role} "${e.name}"${val}${off} @(${e.x}, ${e.y})`;
|
|
215
|
+
})
|
|
216
|
+
.join("\n");
|
|
217
|
+
return (`\n\nASI HA QUEDADO "${res.title}" (los numeros son NUEVOS, usa estos):\n${filas}` +
|
|
218
|
+
(res.truncated ? `\n … hay mas controles; pide ui_snapshot con 'max' mas alto si te falta alguno.` : ""));
|
|
219
|
+
}
|
|
71
220
|
export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
72
221
|
const { action } = params;
|
|
73
222
|
const mode = params.mode || "background";
|
|
@@ -151,6 +300,10 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
151
300
|
`Si es un lienzo grafico, usa screenshot con analyze.` +
|
|
152
301
|
ambiguityNote(params, choice));
|
|
153
302
|
}
|
|
303
|
+
// Se apunta esta foto como referencia: si no, el resumen de cambios de la
|
|
304
|
+
// siguiente accion comparara contra una foto vieja y contara cambios que
|
|
305
|
+
// el modelo ya ha visto.
|
|
306
|
+
compararCon(String(res.title || ""), els);
|
|
154
307
|
const rows = els
|
|
155
308
|
.map((e) => {
|
|
156
309
|
const val = e.value ? ` = "${e.value}"` : "";
|
|
@@ -163,13 +316,38 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
163
316
|
`Actua con ui_click / ui_type usando el numero entre corchetes. No hace falta mover el raton.` +
|
|
164
317
|
ambiguityNote(params, choice));
|
|
165
318
|
}
|
|
319
|
+
// Esperar a que la ventana haga algo, en vez de dormir un rato y mirar.
|
|
320
|
+
//
|
|
321
|
+
// Antes solo existia "wait", que duerme un numero fijo de milisegundos:
|
|
322
|
+
// corto cuando la aplicacion tarda y desperdiciado cuando no tarda nada.
|
|
323
|
+
// Esto se suscribe a los avisos de la propia ventana y vuelve en cuanto
|
|
324
|
+
// pasa algo. Medido en vivo: 275 ms contra los 2 s que se habrian dormido.
|
|
325
|
+
case "wait_change": {
|
|
326
|
+
const elegida = await resolveWindow(params);
|
|
327
|
+
if (!elegida.ok)
|
|
328
|
+
return fail(action, elegida.error);
|
|
329
|
+
const ms = Math.min(Math.max(params.ms ?? 3000, 100), 30_000);
|
|
330
|
+
const res = await winHost.send("wait_change", { ...pinned(elegida), ...(elegida.hwnd ? {} : windowArgs(params)), timeoutMs: ms }, ms + 10_000);
|
|
331
|
+
if (!res.ok)
|
|
332
|
+
return fail(action, res.error || "fallo desconocido");
|
|
333
|
+
if (!res.changed) {
|
|
334
|
+
return (`La ventana "${elegida.title || params.window}" no cambio en ${ms} ms. ` +
|
|
335
|
+
`Si esperabas una reaccion, puede que la accion anterior no surtiera efecto.`);
|
|
336
|
+
}
|
|
337
|
+
// Y ya que se vuelve, se dice COMO quedo: si no, hace falta otro viaje.
|
|
338
|
+
const despues = await estadoTrasActuar(windowArgs(params));
|
|
339
|
+
return `Cambio: ${res.what}.${despues}`;
|
|
340
|
+
}
|
|
166
341
|
case "ui_click": {
|
|
167
342
|
if (!params.ref)
|
|
168
343
|
return fail(action, 'indica el "ref" que devolvio ui_snapshot.');
|
|
169
344
|
const res = await winHost.send("ui_click", { ref: params.ref }, 20_000);
|
|
170
345
|
if (!res.ok)
|
|
171
346
|
return fail(action, res.error || "fallo desconocido");
|
|
172
|
-
|
|
347
|
+
// El estado nuevo va aqui y no en otro viaje al modelo: cuesta 43 ms en
|
|
348
|
+
// la maquina contra varios segundos de ida y vuelta. Ver estadoTrasActuar.
|
|
349
|
+
const despues = await estadoTrasActuar(windowArgs(params));
|
|
350
|
+
return `Activado [${params.ref}] ${res.label || ""} (via ${res.via}, sin mover el raton).${despues}`;
|
|
173
351
|
}
|
|
174
352
|
case "ui_type": {
|
|
175
353
|
if (!params.ref)
|
|
@@ -221,8 +399,11 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
|
|
|
221
399
|
interactiveOnly: false,
|
|
222
400
|
});
|
|
223
401
|
const landed = (check.elements || []).length > 0;
|
|
402
|
+
// Igual que en ui_click: el estado nuevo viaja con la accion en vez de
|
|
403
|
+
// costar otra ida y vuelta al modelo.
|
|
404
|
+
const tras = landed ? await estadoTrasActuar(windowArgs(params)) : "";
|
|
224
405
|
return landed
|
|
225
|
-
? `Texto escrito en [${params.ref}] con clic y teclado reales, y verificado en el control: "${preview(params.text)}"`
|
|
406
|
+
? `Texto escrito en [${params.ref}] con clic y teclado reales, y verificado en el control: "${preview(params.text)}"${tras}`
|
|
226
407
|
: `Aviso: se escribio en [${params.ref}] pero el control no muestra el texto. ` +
|
|
227
408
|
`Puede que el foco se perdiera. Haz ui_snapshot y reintenta antes de dar por hecho que funciono.`;
|
|
228
409
|
}
|
|
@@ -82,7 +82,10 @@ export const COMPUTER_USE_TOOL = {
|
|
|
82
82
|
"REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta. " +
|
|
83
83
|
"FLUJO CORRECTO: 'list_windows' para saber que hay abierto -> 'ui_snapshot' con la ventana objetivo para listar sus controles reales -> " +
|
|
84
84
|
"'ui_click' / 'ui_type' con el numero de ref. Eso actua en segundo plano, sin mover el raton ni robar el foco al usuario. " +
|
|
85
|
-
"
|
|
85
|
+
"CUANTO CUESTA CADA COSA, que decide cual usar: ui_snapshot tarda unos 40 ms y te devuelve texto que entiendes directo. "
|
|
86
|
+
+ "screenshot cuesta una llamada entera a otro modelo -varios segundos- mas los tokens de la imagen. "
|
|
87
|
+
+ "O sea que screenshot es unas cien veces mas caro. Usalo SOLO cuando el arbol de accesibilidad no vea el control: lienzos, juegos, imagenes, interfaces dibujadas a mano. "
|
|
88
|
+
+ "Para todo lo demas, ui_snapshot y luego find_element. " +
|
|
86
89
|
"Las coordenadas siempre son pixeles fisicos de pantalla.",
|
|
87
90
|
parameters: {
|
|
88
91
|
type: "object",
|
|
@@ -109,6 +112,7 @@ export const COMPUTER_USE_TOOL = {
|
|
|
109
112
|
"type",
|
|
110
113
|
"key",
|
|
111
114
|
"wait",
|
|
115
|
+
"wait_change",
|
|
112
116
|
"cursor_position",
|
|
113
117
|
"focus_window",
|
|
114
118
|
"get_active_window",
|
|
@@ -116,7 +120,7 @@ export const COMPUTER_USE_TOOL = {
|
|
|
116
120
|
],
|
|
117
121
|
description: "list_windows: inventario de ventanas. ui_snapshot: lista los controles reales de una ventana con su ref. " +
|
|
118
122
|
"ui_click/ui_type/ui_focus: actuan sobre un ref sin tocar el raton. find_element: busca un control por texto y, si hace falta, lo localiza con vision. " +
|
|
119
|
-
"screenshot: captura una ventana concreta (aunque este tapada) y la describe. click/type/key: entrada real, solo como ultimo recurso.",
|
|
123
|
+
"wait_change: se suscribe a la ventana y vuelve EN CUANTO cambia algo, con el estado nuevo. Usalo en vez de wait siempre que esperes una reaccion: wait duerme un tiempo fijo (corto si la app tarda, desperdiciado si no). " + "screenshot: captura una ventana concreta (aunque este tapada) y la describe con otro modelo; lento y caro, ultimo recurso para lo que el arbol no ve. click/type/key: entrada real, solo como ultimo recurso.",
|
|
120
124
|
},
|
|
121
125
|
window: {
|
|
122
126
|
type: "string",
|
|
@@ -5,15 +5,72 @@ import { apuntarUso } from "../agent/usageMeter.js";
|
|
|
5
5
|
function client(apiKey) {
|
|
6
6
|
return createEngineClient(apiKey, "vision");
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Cuanto puede pesar una imagen antes de que la peticion se muera por el camino.
|
|
10
|
+
*
|
|
11
|
+
* EL FALLO QUE CIERRA ESTO
|
|
12
|
+
*
|
|
13
|
+
* `toDataUrl` leia el archivo entero y lo metia en base64 sin mirar el tamano.
|
|
14
|
+
* Una captura de pantalla en PNG de un monitor normal son 1,3 MB, que en base64
|
|
15
|
+
* se convierten en 1,75 MB dentro del JSON de la peticion. Eso no llega: el
|
|
16
|
+
* Worker del proxy se queda sin margen a mitad de camino, la peticion muere sin
|
|
17
|
+
* respuesta, y ENCIMA se lleva por delante uno de los cuatro huecos de la
|
|
18
|
+
* licencia.
|
|
19
|
+
*
|
|
20
|
+
* Desde fuera se veia asi, y el usuario lo sufrio una tarde entera:
|
|
21
|
+
*
|
|
22
|
+
* chrome(screenshot) → chrome_tab.png (1314.8 KB)
|
|
23
|
+
* 💭 penso 4s
|
|
24
|
+
* ⚠ El motor no devolvio nada.
|
|
25
|
+
* ...
|
|
26
|
+
* ⏸ Tienes 4 peticiones en curso. Se libera en 169s.
|
|
27
|
+
*
|
|
28
|
+
* Cuatro capturas grandes y la licencia queda bloqueada tres minutos, sin que
|
|
29
|
+
* nada explique por que. Ahora se para ANTES de mandarla y se dice como
|
|
30
|
+
* arreglarlo, que cuesta lo mismo y no gasta un hueco.
|
|
31
|
+
*
|
|
32
|
+
* El limite no es caprichoso: 900 KB de base64 son ~675 KB de imagen, muy por
|
|
33
|
+
* encima de lo que ocupa una captura en JPEG (unos 120 KB) y muy por debajo de
|
|
34
|
+
* donde empiezan los problemas.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_BASE64 = 900_000;
|
|
37
|
+
/** El tipo real de la imagen, mirando sus primeros bytes y no su extension. */
|
|
38
|
+
function tipoDeImagen(buf) {
|
|
39
|
+
if (buf.length > 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff)
|
|
40
|
+
return "image/jpeg";
|
|
41
|
+
if (buf.length > 8 && buf[0] === 0x89 && buf[1] === 0x50)
|
|
42
|
+
return "image/png";
|
|
43
|
+
if (buf.length > 12 && buf.toString("ascii", 8, 12) === "WEBP")
|
|
44
|
+
return "image/webp";
|
|
45
|
+
// Se deja pasar como PNG: equivocarse aqui es un error del servidor, no un
|
|
46
|
+
// cuelgue silencioso.
|
|
47
|
+
return "image/png";
|
|
48
|
+
}
|
|
49
|
+
function prepararImagen(imagePath) {
|
|
9
50
|
const buf = fs.readFileSync(imagePath);
|
|
10
|
-
|
|
51
|
+
const b64 = buf.toString("base64");
|
|
52
|
+
if (b64.length > MAX_BASE64) {
|
|
53
|
+
const kb = Math.round(buf.length / 1024);
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: `La imagen pesa ${kb} KB y no cabe en una peticion (el tope son ${Math.round(MAX_BASE64 / 1024)} KB de datos). ` +
|
|
57
|
+
`Mandarla igualmente no falla con un error: la peticion se muere por el camino, no vuelve nada, y se pierde ` +
|
|
58
|
+
`uno de los cuatro huecos de peticion durante minutos.\n\n` +
|
|
59
|
+
`Vuelve a capturar en JPEG, o recorta la zona que te interesa en vez de la pantalla entera.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, url: `data:${tipoDeImagen(buf)};base64,${b64}` };
|
|
11
63
|
}
|
|
12
64
|
async function ask(apiKey, prompt, imagePath, maxTokens) {
|
|
13
65
|
if (!apiKey)
|
|
14
66
|
return { ok: false, error: "No hay API key disponible para el modelo de vision." };
|
|
15
67
|
if (!fs.existsSync(imagePath))
|
|
16
68
|
return { ok: false, error: `No existe la imagen "${imagePath}".` };
|
|
69
|
+
// Se comprueba ANTES de gastar un hueco de peticion: una imagen demasiado
|
|
70
|
+
// grande no da error, mata la peticion en silencio. Ver prepararImagen().
|
|
71
|
+
const imagen = prepararImagen(imagePath);
|
|
72
|
+
if (!imagen.ok)
|
|
73
|
+
return { ok: false, error: imagen.error };
|
|
17
74
|
try {
|
|
18
75
|
const res = await client(apiKey).chat.completions.create({
|
|
19
76
|
model: VISION_MODEL,
|
|
@@ -24,7 +81,7 @@ async function ask(apiKey, prompt, imagePath, maxTokens) {
|
|
|
24
81
|
role: "user",
|
|
25
82
|
content: [
|
|
26
83
|
{ type: "text", text: prompt },
|
|
27
|
-
{ type: "image_url", image_url: { url:
|
|
84
|
+
{ type: "image_url", image_url: { url: imagen.url } },
|
|
28
85
|
],
|
|
29
86
|
},
|
|
30
87
|
],
|
|
@@ -518,6 +518,155 @@ public static class Choco
|
|
|
518
518
|
return null;
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
// Lo que el elemento dice SER AHORA MISMO, con el mismo formato que se
|
|
522
|
+
// guardo al hacer la foto. Si un elemento murio, aqui salta y se trata como
|
|
523
|
+
// cambio, que es lo correcto.
|
|
524
|
+
static string EtiquetaViva(AutomationElement e)
|
|
525
|
+
{
|
|
526
|
+
try { return RoleOf(e) + " " + (e.Current.Name == null ? "" : e.Current.Name); }
|
|
527
|
+
catch { return "(elemento muerto)"; }
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Un ref solo vale si sigue siendo lo que era.
|
|
531
|
+
//
|
|
532
|
+
// POR QUE
|
|
533
|
+
//
|
|
534
|
+
// ui_snapshot guarda los elementos en _snap y su etiqueta en _snapLabel, y
|
|
535
|
+
// hasta ahora _snapLabel solo servia para INFORMAR: nunca se comparaba. Pero
|
|
536
|
+
// entre mirar y actuar pasa tiempo, y las interfaces cambian solas -una
|
|
537
|
+
// lista que se refresca, un menu que se cierra, un dialogo que aparece-. El
|
|
538
|
+
// elemento guardado en el ref 3 sigue ahi, pero ya no es el boton que el
|
|
539
|
+
// modelo vio: es otro. Y se pulsa ese otro, sin que nada avise.
|
|
540
|
+
//
|
|
541
|
+
// Es la misma familia del fallo que ya mordio dos veces en el navegador
|
|
542
|
+
// (hallazgos #19 y #20): actuar sobre lo que habia antes creyendo que es lo
|
|
543
|
+
// de ahora. Alli se arreglo; aqui seguia abierto.
|
|
544
|
+
//
|
|
545
|
+
// Comparar la etiqueta cuesta una lectura y convierte un clic en el sitio
|
|
546
|
+
// equivocado -que el usuario descubre cuando ya se ha borrado algo- en un
|
|
547
|
+
// mensaje que el modelo puede resolver volviendo a mirar.
|
|
548
|
+
static string RefCambiado(int id)
|
|
549
|
+
{
|
|
550
|
+
AutomationElement e = Ref(id);
|
|
551
|
+
if (e == null) return "ref inexistente; repite ui_snapshot";
|
|
552
|
+
|
|
553
|
+
string antes;
|
|
554
|
+
if (!_snapLabel.TryGetValue(id, out antes)) return null;
|
|
555
|
+
|
|
556
|
+
string ahora = EtiquetaViva(e);
|
|
557
|
+
if (ahora == antes) return null;
|
|
558
|
+
|
|
559
|
+
if (ahora == "(elemento muerto)")
|
|
560
|
+
{
|
|
561
|
+
return "el elemento del ref " + N(id) + " (" + antes + ") ya no existe: la ventana cambio. Repite ui_snapshot.";
|
|
562
|
+
}
|
|
563
|
+
return "el ref " + N(id) + " ya no es lo que viste: era \"" + antes + "\" y ahora es \"" + ahora +
|
|
564
|
+
"\". Actuar sobre el seria pulsar otra cosa. Repite ui_snapshot y usa el numero nuevo.";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ------------------------------------------------------------- esperar cambios
|
|
568
|
+
//
|
|
569
|
+
// Esperar a que la ventana haga algo, en vez de preguntarle cada tanto.
|
|
570
|
+
//
|
|
571
|
+
// POR QUE
|
|
572
|
+
//
|
|
573
|
+
// Hasta ahora, para saber si una accion habia surtido efecto se leia el arbol
|
|
574
|
+
// varias veces seguidas hasta que dos salieran iguales. Funciona, pero tiene
|
|
575
|
+
// dos costes: se lee el arbol entero N veces, y la respuesta llega en el
|
|
576
|
+
// siguiente sondeo, no cuando pasa la cosa. Con una ventana que tarda 40 ms en
|
|
577
|
+
// reaccionar y un sondeo cada 120, se pierden 80 ms de nada; con una que tarda
|
|
578
|
+
// 2 s, se leyo el arbol veinte veces para nada.
|
|
579
|
+
//
|
|
580
|
+
// UI Automation puede avisar. Aqui se suscribe a los cambios de la ventana, se
|
|
581
|
+
// espera bloqueado, y se vuelve en cuanto ocurre el primero.
|
|
582
|
+
//
|
|
583
|
+
// LO QUE HAY QUE HACER BIEN O NO HACERLO
|
|
584
|
+
//
|
|
585
|
+
// Los manejadores de UIA viven dentro del proceso de la aplicacion observada.
|
|
586
|
+
// Si se registran y no se quitan, esa aplicacion se queda mas lenta PARA
|
|
587
|
+
// SIEMPRE, hasta que el usuario la cierre. Por eso todo va dentro de un
|
|
588
|
+
// try/finally y se quitan pase lo que pase, incluido si salta una excepcion o
|
|
589
|
+
// se agota el tiempo.
|
|
590
|
+
//
|
|
591
|
+
// El evento llega en un hilo interno de UIA, no en este: por eso lo que se
|
|
592
|
+
// comparte es un ManualResetEventSlim y una cadena, y nada mas.
|
|
593
|
+
static string _cambioQue = "";
|
|
594
|
+
static readonly object _cambioLock = new object();
|
|
595
|
+
|
|
596
|
+
static void ApuntarCambio(string texto)
|
|
597
|
+
{
|
|
598
|
+
lock (_cambioLock) { if (_cambioQue.Length == 0) _cambioQue = texto; }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
public static string WaitChange(IntPtr hwnd, int timeoutMs)
|
|
602
|
+
{
|
|
603
|
+
if (hwnd == IntPtr.Zero) return "{\"ok\":false,\"error\":\"ventana no encontrada\"}";
|
|
604
|
+
if (timeoutMs < 100) timeoutMs = 100;
|
|
605
|
+
if (timeoutMs > 30000) timeoutMs = 30000;
|
|
606
|
+
|
|
607
|
+
AutomationElement raiz;
|
|
608
|
+
try { raiz = AutomationElement.FromHandle(hwnd); }
|
|
609
|
+
catch (Exception ex) { return "{\"ok\":false,\"error\":" + Q(ex.Message) + "}"; }
|
|
610
|
+
if (raiz == null) return "{\"ok\":false,\"error\":\"la ventana no expone arbol de accesibilidad\"}";
|
|
611
|
+
|
|
612
|
+
lock (_cambioLock) { _cambioQue = ""; }
|
|
613
|
+
ManualResetEventSlim listo = new ManualResetEventSlim(false);
|
|
614
|
+
|
|
615
|
+
AutomationPropertyChangedEventHandler onProp = delegate(object src, AutomationPropertyChangedEventArgs e)
|
|
616
|
+
{
|
|
617
|
+
string prop = e.Property == ValuePattern.ValueProperty ? "valor"
|
|
618
|
+
: e.Property == AutomationElement.NameProperty ? "nombre"
|
|
619
|
+
: e.Property == AutomationElement.IsEnabledProperty ? "habilitado"
|
|
620
|
+
: "propiedad";
|
|
621
|
+
string quien = "";
|
|
622
|
+
try { AutomationElement el = src as AutomationElement; if (el != null) quien = RoleOf(el) + " " + el.Current.Name; }
|
|
623
|
+
catch { quien = "un control"; }
|
|
624
|
+
ApuntarCambio("cambio el " + prop + " de " + quien);
|
|
625
|
+
listo.Set();
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
StructureChangedEventHandler onStruct = delegate(object src, StructureChangedEventArgs e)
|
|
629
|
+
{
|
|
630
|
+
ApuntarCambio("la ventana anadio o quito controles");
|
|
631
|
+
listo.Set();
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
bool propOk = false, structOk = false;
|
|
635
|
+
try
|
|
636
|
+
{
|
|
637
|
+
AutomationProperty[] props = new AutomationProperty[] {
|
|
638
|
+
ValuePattern.ValueProperty,
|
|
639
|
+
AutomationElement.NameProperty,
|
|
640
|
+
AutomationElement.IsEnabledProperty
|
|
641
|
+
};
|
|
642
|
+
try { Automation.AddAutomationPropertyChangedEventHandler(raiz, TreeScope.Subtree, onProp, props); propOk = true; } catch { }
|
|
643
|
+
try { Automation.AddStructureChangedEventHandler(raiz, TreeScope.Subtree, onStruct); structOk = true; } catch { }
|
|
644
|
+
|
|
645
|
+
if (!propOk && !structOk)
|
|
646
|
+
{
|
|
647
|
+
return "{\"ok\":false,\"error\":\"esta ventana no admite avisos de cambio; usa ui_snapshot para mirar\"}";
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
bool paso = listo.Wait(timeoutMs);
|
|
651
|
+
string que;
|
|
652
|
+
lock (_cambioLock) { que = _cambioQue; }
|
|
653
|
+
|
|
654
|
+
if (!paso)
|
|
655
|
+
{
|
|
656
|
+
return "{\"ok\":true,\"changed\":false,\"waited\":" + N(timeoutMs) + "}";
|
|
657
|
+
}
|
|
658
|
+
return "{\"ok\":true,\"changed\":true,\"what\":" + Q(que.Length > 0 ? que : "algo cambio") + "}";
|
|
659
|
+
}
|
|
660
|
+
finally
|
|
661
|
+
{
|
|
662
|
+
// Sin esto, la aplicacion observada se queda mas lenta hasta que el
|
|
663
|
+
// usuario la cierre. Se quitan siempre, aunque haya saltado todo.
|
|
664
|
+
if (propOk) { try { Automation.RemoveAutomationPropertyChangedEventHandler(raiz, onProp); } catch { } }
|
|
665
|
+
if (structOk) { try { Automation.RemoveStructureChangedEventHandler(raiz, onStruct); } catch { } }
|
|
666
|
+
listo.Dispose();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
521
670
|
public static string RefRect(int id)
|
|
522
671
|
{
|
|
523
672
|
AutomationElement e = Ref(id);
|
|
@@ -537,6 +686,8 @@ public static class Choco
|
|
|
537
686
|
{
|
|
538
687
|
AutomationElement e = Ref(id);
|
|
539
688
|
if (e == null) return "{\"ok\":false,\"error\":\"ref inexistente; repite ui_snapshot\"}";
|
|
689
|
+
string cambio = RefCambiado(id);
|
|
690
|
+
if (cambio != null) return "{\"ok\":false,\"error\":" + Q(cambio) + "}";
|
|
540
691
|
string label = _snapLabel.ContainsKey(id) ? _snapLabel[id] : ("ref " + N(id));
|
|
541
692
|
try
|
|
542
693
|
{
|
|
@@ -555,6 +706,8 @@ public static class Choco
|
|
|
555
706
|
{
|
|
556
707
|
AutomationElement e = Ref(id);
|
|
557
708
|
if (e == null) return "{\"ok\":false,\"error\":\"ref inexistente; repite ui_snapshot\"}";
|
|
709
|
+
string cambio = RefCambiado(id);
|
|
710
|
+
if (cambio != null) return "{\"ok\":false,\"error\":" + Q(cambio) + "}";
|
|
558
711
|
try
|
|
559
712
|
{
|
|
560
713
|
// ValuePattern.SetValue SUSTITUYE todo el contenido del control. En un
|
|
@@ -902,6 +1055,7 @@ while ($true) {
|
|
|
902
1055
|
'ui_snapshot' {
|
|
903
1056
|
$res = [Choco]::Snapshot((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'max' 250), [string](Get-Prop $cmd 'filter' ''), [bool](Get-Prop $cmd 'interactiveOnly' $true))
|
|
904
1057
|
}
|
|
1058
|
+
'wait_change' { $res = [Choco]::WaitChange((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'timeoutMs' 3000)) }
|
|
905
1059
|
'ui_click' { $res = [Choco]::RefInvoke([int](Get-Prop $cmd 'ref' 0)) }
|
|
906
1060
|
'ui_set_value' { $res = [Choco]::RefSetValue([int](Get-Prop $cmd 'ref' 0), [string](Get-Prop $cmd 'text' ''), [bool](Get-Prop $cmd 'replace' $false)) }
|
|
907
1061
|
'ui_focus' { $res = [Choco]::RefFocus([int](Get-Prop $cmd 'ref' 0)) }
|
package/dist/ui/interrupt.d.ts
CHANGED
|
@@ -3,6 +3,14 @@ export declare function tomarTecladoParaPrompt(): void;
|
|
|
3
3
|
/** Lo llama el prompt al cerrarse, para que el siguiente turno pueda restaurar. */
|
|
4
4
|
export declare function devolverTecladoDelPrompt(): void;
|
|
5
5
|
export declare function releaseKeyboard(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Devuelve el teclado a quien lo tenia antes del dialogo.
|
|
8
|
+
*
|
|
9
|
+
* NO se llama a start(): eso crearia un AbortController nuevo, y la peticion en
|
|
10
|
+
* curso se quedaria escuchando al viejo. Esc dejaria de cancelar justo lo que el
|
|
11
|
+
* usuario quiere cancelar.
|
|
12
|
+
*/
|
|
13
|
+
export declare function retomarTeclado(): void;
|
|
6
14
|
export declare class InterruptWatcher {
|
|
7
15
|
private controller;
|
|
8
16
|
private wasRaw;
|
|
@@ -13,6 +21,8 @@ export declare class InterruptWatcher {
|
|
|
13
21
|
get interrupted(): boolean;
|
|
14
22
|
get signal(): AbortSignal | undefined;
|
|
15
23
|
start(): AbortSignal | undefined;
|
|
24
|
+
/** Vuelve a escuchar despues de un dialogo, sin tocar la senal de aborto. */
|
|
25
|
+
reanudar(): void;
|
|
16
26
|
private abort;
|
|
17
27
|
stop(): void;
|
|
18
28
|
}
|
package/dist/ui/interrupt.js
CHANGED
|
@@ -3,6 +3,7 @@ import process from "node:process";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { cycleMode, modeAnnouncement } from "./modes.js";
|
|
5
5
|
import { pushTyped, backspaceTyped, commitTyped } from "./typeAhead.js";
|
|
6
|
+
import { reafirmarModoCrudo } from "./keyboardGuard.js";
|
|
6
7
|
import { dibujar } from "./salida.js";
|
|
7
8
|
/**
|
|
8
9
|
* Interrupcion con Esc mientras el agente trabaja.
|
|
@@ -69,11 +70,39 @@ export function tomarTecladoParaPrompt() {
|
|
|
69
70
|
export function devolverTecladoDelPrompt() {
|
|
70
71
|
promptConElTeclado = false;
|
|
71
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Vigilantes que se soltaron para ensenar un dialogo, y que hay que devolver.
|
|
75
|
+
*
|
|
76
|
+
* El dialogo de permisos suelta el teclado antes de dibujarse -si no, el
|
|
77
|
+
* vigilante se come las teclas y la caja no responde-, pero hasta ahora nadie lo
|
|
78
|
+
* devolvia despues. El resto del turno se quedaba SIN vigilante: en modo cocido
|
|
79
|
+
* el terminal hacia el eco por su cuenta, Esc dejaba de cancelar, y lo que el
|
|
80
|
+
* usuario escribiera no se encolaba.
|
|
81
|
+
*
|
|
82
|
+
* Se veia asi, y el usuario dio con la pista sin saberlo: "a veces se queda
|
|
83
|
+
* trabado y no se puede escribir, pero con darle Enter vuelve a la normalidad".
|
|
84
|
+
* Claro: en modo cocido el terminal guarda la linea y Enter la suelta de golpe.
|
|
85
|
+
*/
|
|
86
|
+
let soltados = [];
|
|
72
87
|
export function releaseKeyboard() {
|
|
73
|
-
|
|
88
|
+
soltados = Array.from(activos);
|
|
89
|
+
for (const w of soltados) {
|
|
74
90
|
w.stop();
|
|
75
91
|
}
|
|
76
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Devuelve el teclado a quien lo tenia antes del dialogo.
|
|
95
|
+
*
|
|
96
|
+
* NO se llama a start(): eso crearia un AbortController nuevo, y la peticion en
|
|
97
|
+
* curso se quedaria escuchando al viejo. Esc dejaria de cancelar justo lo que el
|
|
98
|
+
* usuario quiere cancelar.
|
|
99
|
+
*/
|
|
100
|
+
export function retomarTeclado() {
|
|
101
|
+
const devolver = soltados;
|
|
102
|
+
soltados = [];
|
|
103
|
+
for (const w of devolver)
|
|
104
|
+
w.reanudar();
|
|
105
|
+
}
|
|
77
106
|
export class InterruptWatcher {
|
|
78
107
|
controller = null;
|
|
79
108
|
wasRaw = false;
|
|
@@ -164,6 +193,23 @@ export class InterruptWatcher {
|
|
|
164
193
|
activos.add(this);
|
|
165
194
|
return this.controller.signal;
|
|
166
195
|
}
|
|
196
|
+
/** Vuelve a escuchar despues de un dialogo, sin tocar la senal de aborto. */
|
|
197
|
+
reanudar() {
|
|
198
|
+
if (this.listening || !this.onKey)
|
|
199
|
+
return;
|
|
200
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode)
|
|
201
|
+
return;
|
|
202
|
+
readline.emitKeypressEvents(process.stdin);
|
|
203
|
+
if (activos.size === 0)
|
|
204
|
+
modoCrudoOriginal = process.stdin.isRaw || false;
|
|
205
|
+
// Reafirmar y no solo poner: el readline del dialogo deja la consola en modo
|
|
206
|
+
// cocido, y un setRawMode(true) a secas puede ser un no-op.
|
|
207
|
+
reafirmarModoCrudo(process.stdin);
|
|
208
|
+
process.stdin.resume();
|
|
209
|
+
process.stdin.on("keypress", this.onKey);
|
|
210
|
+
this.listening = true;
|
|
211
|
+
activos.add(this);
|
|
212
|
+
}
|
|
167
213
|
abort() {
|
|
168
214
|
if (this.aborted)
|
|
169
215
|
return;
|
|
@@ -176,7 +222,9 @@ export class InterruptWatcher {
|
|
|
176
222
|
stop() {
|
|
177
223
|
if (this.onKey) {
|
|
178
224
|
process.stdin.removeListener("keypress", this.onKey);
|
|
179
|
-
|
|
225
|
+
// El manejador NO se tira: reanudar() lo vuelve a enganchar tal cual, con
|
|
226
|
+
// su AbortController intacto. Rehacerlo desde start() romperia el Esc de
|
|
227
|
+
// la peticion que ya esta en vuelo.
|
|
180
228
|
}
|
|
181
229
|
const escuchaba = this.listening;
|
|
182
230
|
this.listening = false;
|