chocolatito-code 1.6.12 → 1.6.14

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.
Files changed (42) hide show
  1. package/COMPUTER-USE.md +37 -0
  2. package/README.md +9 -2
  3. package/dist/agent/cortes.d.ts +41 -0
  4. package/dist/agent/cortes.js +52 -0
  5. package/dist/agent/loop.js +35 -0
  6. package/dist/config/engine.js +7 -0
  7. package/dist/config/permissions.js +4 -0
  8. package/dist/index.js +147 -24
  9. package/dist/prompts/systemPrompt.js +15 -11
  10. package/dist/servidor/archivos.d.ts +69 -0
  11. package/dist/servidor/archivos.js +156 -0
  12. package/dist/servidor/fondo.d.ts +85 -0
  13. package/dist/servidor/fondo.js +134 -0
  14. package/dist/servidor/pagina.js +266 -2
  15. package/dist/servidor/puente.d.ts +13 -0
  16. package/dist/servidor/puente.js +11 -0
  17. package/dist/servidor/servidor.d.ts +6 -0
  18. package/dist/servidor/servidor.js +129 -6
  19. package/dist/tools/computerUse.d.ts +11 -1
  20. package/dist/tools/computerUse.js +376 -34
  21. package/dist/tools/toolDefsComputer.js +39 -14
  22. package/dist/tools/visionBridge.d.ts +4 -2
  23. package/dist/tools/visionBridge.js +40 -15
  24. package/dist/tools/win/hostScript.js +300 -29
  25. package/dist/ui/comandos.js +1 -0
  26. package/dist/ui/ink/App.js +23 -3
  27. package/dist/ui/ink/Prompt.d.ts +5 -1
  28. package/dist/ui/ink/Prompt.js +92 -4
  29. package/dist/ui/ink/control.d.ts +2 -0
  30. package/dist/ui/ink/control.js +15 -0
  31. package/dist/ui/ink/montarApp.d.ts +0 -19
  32. package/dist/ui/ink/montarApp.js +3 -0
  33. package/dist/ui/permissionPrompt.js +44 -9
  34. package/dist/ui/pieFijo.js +5 -0
  35. package/dist/ui/prompt.js +6 -0
  36. package/dist/ui/renderer.d.ts +1 -0
  37. package/dist/ui/renderer.js +27 -2
  38. package/dist/ui/selector.js +5 -0
  39. package/dist/ui/spinner.d.ts +1 -0
  40. package/dist/ui/spinner.js +6 -0
  41. package/extension/manifest.json +1 -1
  42. package/package.json +1 -1
@@ -127,6 +127,8 @@ function fail(action, msg) {
127
127
  */
128
128
  /** La ultima foto de cada ventana, para poder contar solo lo que cambio. */
129
129
  const ultimaFoto = new Map();
130
+ /** Coordenadas de los controles vistos en el ultimo snapshot para reintentos por coordenadas. */
131
+ const ultimoSnapshotCoords = new Map();
130
132
  /** Identidad de un control que NO depende de su numero de ref, que se renumera. */
131
133
  function claveDe(e) {
132
134
  return `${e.role}::${e.name}`;
@@ -217,6 +219,60 @@ async function estadoTrasActuar(args) {
217
219
  return (`\n\nASI HA QUEDADO "${res.title}" (los numeros son NUEVOS, usa estos):\n${filas}` +
218
220
  (res.truncated ? `\n … hay mas controles; pide ui_snapshot con 'max' mas alto si te falta alguno.` : ""));
219
221
  }
222
+ /**
223
+ * POR QUE: "Recuperarse solo, una vez" (patron Astra / OpenAI Operator).
224
+ * Si un clic por ref UIA falla (elemento descolgado del arbol, ventana recargada,
225
+ * o control que no expone Invoke/Toggle/SelectionItem), reintenta AUTOMATICAMENTE
226
+ * una vez mediante clic por coordenadas usando el rectangulo que UIA reporto antes
227
+ * de romperse (guardado en cache o via ui_rect). Solo si este segundo intento tambien
228
+ * falla, devuelve un error claro avisando al modelo de que hace falta usar vision (screenshot).
229
+ */
230
+ async function uiClickFallback(params, res, mode) {
231
+ let fallbackCoord = res.x && res.y && res.x > 0 && res.y > 0 ? { x: res.x, y: res.y } : null;
232
+ if (!fallbackCoord && params.ref) {
233
+ const cached = ultimoSnapshotCoords.get(params.ref);
234
+ if (cached && cached.x > 0 && cached.y > 0) {
235
+ fallbackCoord = { x: cached.x, y: cached.y };
236
+ }
237
+ else {
238
+ const rect = await winHost.send("ui_rect", { ref: params.ref });
239
+ if (rect.ok && rect.x > 0 && rect.y > 0)
240
+ fallbackCoord = { x: rect.x, y: rect.y };
241
+ }
242
+ }
243
+ if (fallbackCoord) {
244
+ const elegida = await resolveWindow(params);
245
+ if (elegida.ok && elegida.hwnd && mode === "background") {
246
+ const bgRes = await winHost.send("bg_click", {
247
+ ...pinned(elegida),
248
+ x: fallbackCoord.x,
249
+ y: fallbackCoord.y,
250
+ button: "left",
251
+ count: 1,
252
+ });
253
+ if (bgRes.ok)
254
+ return { ok: true, label: res.label || `[${params.ref}]`, via: "reintento coordenadas bg_click" };
255
+ }
256
+ else {
257
+ const fgRes = await winHost.send("click", {
258
+ x: fallbackCoord.x,
259
+ y: fallbackCoord.y,
260
+ button: "left",
261
+ count: 1,
262
+ });
263
+ if (fgRes.ok)
264
+ return { ok: true, label: res.label || `[${params.ref}]`, via: "reintento coordenadas click" };
265
+ }
266
+ return {
267
+ ok: false,
268
+ error: `fallo la activacion UIA (${res.error || "sin patron"}) y tambien el reintento por coordenadas en (${fallbackCoord.x}, ${fallbackCoord.y}). Hace falta vision: usa screenshot con analyze/grid.`,
269
+ };
270
+ }
271
+ return {
272
+ ok: false,
273
+ error: `fallo la activacion UIA (${res.error || "sin patron"}) y no habia coordenadas conocidas para reintentar. Hace falta vision: usa screenshot con analyze/grid.`,
274
+ };
275
+ }
220
276
  export async function computerUse(params, cwd = process.cwd(), apiKey) {
221
277
  const { action } = params;
222
278
  const mode = params.mode || "background";
@@ -224,26 +280,53 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
224
280
  let targetY = params.coordinate ? params.coordinate[1] : params.y;
225
281
  let startX = params.start_coordinate ? params.start_coordinate[0] : params.x;
226
282
  let startY = params.start_coordinate ? params.start_coordinate[1] : params.y;
227
- // Soporte de coordenadas normalizadas (0.0 a 1.0) estilo Astra / OpenAI Operator
228
- if (targetX !== undefined && targetY !== undefined && targetX > 0 && targetX <= 1.0 && targetY > 0 && targetY <= 1.0) {
229
- try {
230
- const sInfo = await winHost.send("screen_info");
231
- const sw = sInfo.virtualWidth || sInfo.primaryWidth || 1920;
232
- const sh = sInfo.virtualHeight || sInfo.primaryHeight || 1080;
233
- targetX = Math.round(targetX * sw);
234
- targetY = Math.round(targetY * sh);
283
+ // Soporte de coordenadas normalizadas (0.0 a 1.0 o escala 0..1000 estilo Astra / OpenAI Operator)
284
+ if (targetX !== undefined && targetY !== undefined) {
285
+ const isFloat = targetX > 0 && targetX <= 1.0 && targetY > 0 && targetY <= 1.0;
286
+ const isMille = params.normalized === true || (targetX > 1.0 && targetX <= 1000 && targetY > 1.0 && targetY <= 1000 && params.normalized);
287
+ if (isFloat || isMille) {
288
+ try {
289
+ let sw = 1920, sh = 1080, offX = 0, offY = 0;
290
+ if (params.window) {
291
+ const wChoice = await resolveWindow(params);
292
+ if (wChoice.ok && wChoice.hwnd) {
293
+ const wInfo = await winHost.send("find_window", { hwnd: wChoice.hwnd });
294
+ if (wInfo && wInfo.width > 0 && wInfo.height > 0) {
295
+ sw = wInfo.width;
296
+ sh = wInfo.height;
297
+ offX = wInfo.x || 0;
298
+ offY = wInfo.y || 0;
299
+ }
300
+ }
301
+ }
302
+ if (sw === 1920 && sh === 1080 && offX === 0 && offY === 0) {
303
+ const sInfo = await winHost.send("screen_info");
304
+ sw = sInfo.virtualWidth || sInfo.primaryWidth || 1920;
305
+ sh = sInfo.virtualHeight || sInfo.primaryHeight || 1080;
306
+ offX = sInfo.virtualX || 0;
307
+ offY = sInfo.virtualY || 0;
308
+ }
309
+ const factor = isFloat ? 1.0 : 1000.0;
310
+ targetX = Math.round(offX + (targetX / factor) * sw);
311
+ targetY = Math.round(offY + (targetY / factor) * sh);
312
+ }
313
+ catch { }
235
314
  }
236
- catch { }
237
315
  }
238
- if (startX !== undefined && startY !== undefined && startX > 0 && startX <= 1.0 && startY > 0 && startY <= 1.0) {
239
- try {
240
- const sInfo = await winHost.send("screen_info");
241
- const sw = sInfo.virtualWidth || sInfo.primaryWidth || 1920;
242
- const sh = sInfo.virtualHeight || sInfo.primaryHeight || 1080;
243
- startX = Math.round(startX * sw);
244
- startY = Math.round(startY * sh);
316
+ if (startX !== undefined && startY !== undefined) {
317
+ const isFloat = startX > 0 && startX <= 1.0 && startY > 0 && startY <= 1.0;
318
+ const isMille = params.normalized === true || (startX > 1.0 && startX <= 1000 && startY > 1.0 && startY <= 1000 && params.normalized);
319
+ if (isFloat || isMille) {
320
+ try {
321
+ const sInfo = await winHost.send("screen_info");
322
+ const sw = sInfo.virtualWidth || sInfo.primaryWidth || 1920;
323
+ const sh = sInfo.virtualHeight || sInfo.primaryHeight || 1080;
324
+ const factor = isFloat ? 1.0 : 1000.0;
325
+ startX = Math.round((sInfo.virtualX || 0) + (startX / factor) * sw);
326
+ startY = Math.round((sInfo.virtualY || 0) + (startY / factor) * sh);
327
+ }
328
+ catch { }
245
329
  }
246
- catch { }
247
330
  }
248
331
  try {
249
332
  switch (action) {
@@ -315,10 +398,33 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
315
398
  if (!res.ok)
316
399
  return fail(action, res.error || "fallo desconocido");
317
400
  const els = res.elements || [];
318
- if (els.length === 0) {
319
- return (`Ventana "${res.title}": el arbol de accesibilidad no devolvio controles.\n` +
320
- `Si es una pagina web, usa la herramienta chrome (o browser) en vez de computer_use. ` +
321
- `Si es un lienzo grafico, usa screenshot con analyze.` +
401
+ // Guardamos las coordenadas de los controles para el reintento resiliente estilo Astra
402
+ for (const e of els) {
403
+ if (e.ref && e.x && e.y) {
404
+ ultimoSnapshotCoords.set(e.ref, { x: e.x, y: e.y, label: `${e.role} "${e.name}"` });
405
+ }
406
+ }
407
+ // POR QUE: Leer texto que el arbol no expone (Astra / Operator).
408
+ // Cuando UIA devuelve 0 o casi 0 controles (canvas, app vieja sin UIA, Electron con
409
+ // accesibilidad desactivada), no dejamos al agente ciego pensando que la ventana esta vacia.
410
+ // Se intenta extraer texto nativo Win32 y si no hay nada se instruye explicitamente que DEBE
411
+ // usarse screenshot con vision.
412
+ const isCanvasOrEmpty = els.length === 0 || (els.length <= 2 && els.every((e) => !e.name && (e.role === "Pane" || e.role === "Window" || e.role === "Custom")));
413
+ if (isCanvasOrEmpty) {
414
+ const textRes = await winHost.send("extract_text", {
415
+ ...pinned(choice),
416
+ ...(choice.hwnd ? {} : windowArgs(params)),
417
+ });
418
+ const lineas = (textRes.ok && Array.isArray(textRes.lines)) ? textRes.lines : [];
419
+ if (lineas.length > 0) {
420
+ return (`Ventana "${res.title}": el arbol UIA devolvio ${els.length} controles interactivos, pero se extrajo texto de controles nativos Win32:\n\n` +
421
+ lineas.map((l) => ` ${l}`).join("\n") +
422
+ `\n\nSi necesitas interactuar con elementos que no estan en el arbol, usa screenshot con analyze/grid o clic por coordenadas.` +
423
+ ambiguityNote(params, choice));
424
+ }
425
+ return (`Ventana "${res.title}": el arbol de accesibilidad devolvio ${els.length} controles (vacio o casi vacio: es un lienzo grafico, canvas, Electron con accesibilidad rota o app sin UIA).\n` +
426
+ `La ventana es SOLO VISUAL: no intentes adivinar ni asumas que la ventana esta vacia.\n` +
427
+ `DEBES usar la accion 'screenshot' con 'analyze: true' (o con 'region' / 'grid: true') para leer su contenido con vision.` +
322
428
  ambiguityNote(params, choice));
323
429
  }
324
430
  // Se apunta esta foto como referencia: si no, el resumen de cambios de la
@@ -359,16 +465,91 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
359
465
  const despues = await estadoTrasActuar(windowArgs(params));
360
466
  return `Cambio: ${res.what}.${despues}`;
361
467
  }
468
+ // Espera un cambio visual en la ventana o región (ideal para lienzos, juegos, transiciones o cuando UIA no emite eventos)
469
+ case "wait_visual_change": {
470
+ const elegida = await resolveWindow(params);
471
+ if (!elegida.ok)
472
+ return fail(action, elegida.error);
473
+ const ms = Math.min(Math.max(params.ms ?? 3000, 100), 30_000);
474
+ let rx1 = 0, ry1 = 0, rx2 = 0, ry2 = 0;
475
+ if (params.region && params.region.length === 4) {
476
+ [rx1, ry1, rx2, ry2] = params.region;
477
+ }
478
+ const res = await winHost.send("wait_visual_change", {
479
+ ...pinned(elegida),
480
+ ...(elegida.hwnd ? {} : windowArgs(params)),
481
+ timeoutMs: ms,
482
+ rx1,
483
+ ry1,
484
+ rx2,
485
+ ry2,
486
+ }, ms + 10_000);
487
+ if (!res.ok)
488
+ return fail(action, res.error || "fallo desconocido");
489
+ if (!res.changed) {
490
+ return `No se detectaron cambios visuales en ${ms}ms en "${elegida.title || params.window || "la pantalla"}".`;
491
+ }
492
+ const despues = await estadoTrasActuar(windowArgs(params));
493
+ return `Cambio visual detectado (~${res.diffPercent}% de pixeles en ${res.elapsed}ms).${despues}`;
494
+ }
495
+ // Encadenamiento de acciones rápidas estilo Astra y OpenAI Operator
496
+ case "sequence": {
497
+ const steps = params.steps;
498
+ if (!Array.isArray(steps) || steps.length === 0) {
499
+ return fail(action, 'falta el array "steps" con las acciones a encadenar.');
500
+ }
501
+ // POR QUE: Secuencias anidadas complican el flujo de errores y pueden saturar
502
+ // el host o eludir la aprobacion del usuario. Rechazar de inmediato cualquier
503
+ // paso con action="sequence".
504
+ for (let i = 0; i < steps.length; i++) {
505
+ if (steps[i]?.action === "sequence") {
506
+ return fail(action, `el paso ${i + 1} no puede ser otra 'sequence'. Las secuencias no se anidan; pon los pasos en una sola lista plana.`);
507
+ }
508
+ }
509
+ const logs = [];
510
+ let stepIdx = 1;
511
+ for (const step of steps) {
512
+ const merged = {
513
+ ...step,
514
+ window: step.window || params.window,
515
+ mode: step.mode || params.mode || mode,
516
+ normalized: step.normalized !== undefined ? step.normalized : params.normalized,
517
+ };
518
+ const stepResult = await computerUse(merged, cwd, apiKey);
519
+ const firstLine = stepResult.split("\n")[0];
520
+ logs.push(`[Paso ${stepIdx}/${steps.length} ${merged.action}] ${firstLine}`);
521
+ if (stepResult.startsWith("Error en computer_use")) {
522
+ return `Secuencia interrumpida en el paso ${stepIdx}:\n${logs.join("\n")}`;
523
+ }
524
+ stepIdx++;
525
+ if (merged.ms) {
526
+ await new Promise((r) => setTimeout(r, merged.ms));
527
+ }
528
+ else if (stepIdx <= steps.length) {
529
+ await new Promise((r) => setTimeout(r, 80));
530
+ }
531
+ }
532
+ const despues = params.window ? await estadoTrasActuar(windowArgs(params)) : "";
533
+ return `Secuencia de ${steps.length} pasos completada con exito:\n${logs.join("\n")}${despues}`;
534
+ }
362
535
  case "ui_click": {
363
536
  if (!params.ref)
364
537
  return fail(action, 'indica el "ref" que devolvio ui_snapshot.');
365
- const res = await winHost.send("ui_click", { ref: params.ref }, 20_000);
538
+ let res = await winHost.send("ui_click", { ref: params.ref }, 20_000);
539
+ if (!res.ok)
540
+ res = await uiClickFallback(params, res, mode);
366
541
  if (!res.ok)
367
542
  return fail(action, res.error || "fallo desconocido");
368
543
  // El estado nuevo va aqui y no en otro viaje al modelo: cuesta 43 ms en
369
544
  // la maquina contra varios segundos de ida y vuelta. Ver estadoTrasActuar.
370
545
  const despues = await estadoTrasActuar(windowArgs(params));
371
- return `Activado [${params.ref}] ${res.label || ""} (via ${res.via}, sin mover el raton).${despues}`;
546
+ // POR QUE: Astra y Operator no dan por hecho que el clic funciono solo por
547
+ // recibir ok de la API de ventanas. Si la ventana no mostro ningun cambio visible,
548
+ // se avisa al modelo para que no suponga un exito ciego y verifique.
549
+ if (despues.includes("no cambio en nada visible")) {
550
+ return `Hice clic pero la ventana NO reacciono (no cambio en nada visible). No asumas que funciono; comprueba con ui_snapshot o usa clic por coordenadas.${despues}`;
551
+ }
552
+ return `Pulsado [${params.ref}] ${res.label || ""} (via ${res.via}, sin mover el raton).${despues}`;
372
553
  }
373
554
  case "ui_type": {
374
555
  if (!params.ref)
@@ -437,8 +618,23 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
437
618
  // ValuePattern borraria todo. En ambos se enfoca y se teclea, que inserta
438
619
  // en el cursor en vez de arrasar el contenido.
439
620
  const focus = await winHost.send("ui_focus", { ref: params.ref }, 15_000);
440
- if (!focus.ok)
441
- return fail(action, `${res.error}; ademas fallo el enfoque: ${focus.error}`);
621
+ if (!focus.ok) {
622
+ // Recuperación Astra: si UIA focus falla, clic en sus coordenadas para colocar el cursor
623
+ const rect = await winHost.send("ui_rect", { ref: params.ref });
624
+ if (rect.ok && rect.x > 0 && rect.y > 0) {
625
+ const elegidaWin = await resolveWindow(params);
626
+ if (elegidaWin.ok && elegidaWin.hwnd && mode === "background") {
627
+ await winHost.send("bg_click", { ...pinned(elegidaWin), x: rect.x, y: rect.y, button: "left", count: 1 });
628
+ }
629
+ else {
630
+ await winHost.send("click", { x: rect.x, y: rect.y, button: "left", count: 1 });
631
+ }
632
+ await new Promise((r) => setTimeout(r, 200));
633
+ }
634
+ else {
635
+ return fail(action, `${res.error}; ademas fallo el enfoque: ${focus.error}`);
636
+ }
637
+ }
442
638
  const typed = await winHost.send("type", { text: params.text, delay: 6 }, 120_000);
443
639
  if (!typed.ok)
444
640
  return fail(action, typed.error || "fallo al teclear");
@@ -481,7 +677,8 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
481
677
  return `Sin coincidencias en el arbol de accesibilidad para "${query}", y no hay API key para recurrir a la vision.`;
482
678
  }
483
679
  const shotPath = path.join(ensureCache(), "find_target.jpg");
484
- const shot = await captureFor(params, shotPath);
680
+ // Activamos la cuadrícula de coordenadas (grid) para que la visión tenga máxima precisión
681
+ const shot = await captureFor({ ...params, grid: true }, shotPath);
485
682
  if (!shot.ok)
486
683
  return fail(action, shot.error || "no se pudo capturar");
487
684
  const loc = await locateElement(apiKey, shotPath, query, shot.imageWidth, shot.imageHeight);
@@ -491,8 +688,9 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
491
688
  return `No se encontro "${query}" ni en el arbol ni en la imagen. ${loc.reason || ""}`;
492
689
  // De pixeles de imagen a pixeles de pantalla.
493
690
  const screen = imageToScreen(loc.x, loc.y, shot);
494
- return (`"${query}" localizado por vision en pantalla (${screen.x}, ${screen.y}). ${loc.reason || ""}\n` +
495
- `Ojo: la vision es aproximada. Haz clic ahi y despues verifica con otra screenshot.`);
691
+ const bboxNote = loc.bbox ? ` (caja [${loc.bbox.join(",")}])` : "";
692
+ return (`"${query}" localizado por vision multimodal en pantalla (${screen.x}, ${screen.y})${bboxNote}. ${loc.reason || ""}\n` +
693
+ `Haz clic con computer_use(action: "click", coordinate: [${screen.x}, ${screen.y}]${params.window ? `, window: "${params.window}"` : ""}).`);
496
694
  }
497
695
  // ---------------------------------------------------------------- captura
498
696
  case "screenshot": {
@@ -505,9 +703,11 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
505
703
  return fail(action, shot.error || "no se pudo capturar");
506
704
  const aviso = ambiguityNote(params, choice);
507
705
  const sizeKb = (fs.statSync(out).size / 1024).toFixed(1);
706
+ const regStr = (params.region && params.region.length === 4) ? ` [recorte (${params.region.join(",")})]` : "";
707
+ const gridStr = params.grid ? " [cuadricula visual activa]" : "";
508
708
  const head = shot.kind === "window"
509
- ? `Captura de la ventana "${shot.title}" (${shot.sourceWidth}x${shot.sourceHeight}, capturada en segundo plano)`
510
- : `Captura de pantalla completa (${shot.sourceWidth}x${shot.sourceHeight})`;
709
+ ? `Captura de la ventana "${shot.title}" (${shot.sourceWidth}x${shot.sourceHeight}${regStr}${gridStr}, capturada en segundo plano)`
710
+ : `Captura de pantalla completa (${shot.sourceWidth}x${shot.sourceHeight}${regStr}${gridStr})`;
511
711
  const meta = `${head}\nArchivo: ${out} (${sizeKb} KB, imagen ${shot.imageWidth}x${shot.imageHeight}, escala ${shot.scale})`;
512
712
  if (params.analyze === false)
513
713
  return meta;
@@ -553,12 +753,22 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
553
753
  });
554
754
  if (!res.ok)
555
755
  return fail(action, res.error || "fallo desconocido");
556
- return `Clic ${button}${count > 1 ? ` x${count}` : ""} enviado en segundo plano a "${res.window}" en (${targetX}, ${targetY}). El raton del usuario no se ha movido.`;
756
+ const despues = await estadoTrasActuar(windowArgs(params));
757
+ // POR QUE: Astra y Operator verifican que la accion visualmente causo un cambio.
758
+ // Si Windows devuelve ok pero la ventana sigue identica, hay que avisar para no asumir exito.
759
+ if (despues.includes("no cambio en nada visible")) {
760
+ return `Hice clic pero la ventana NO reacciono (no cambio en nada visible). No asumas que funciono; comprueba con ui_snapshot o usa clic por coordenadas.${despues}`;
761
+ }
762
+ return `Clic ${button}${count > 1 ? ` x${count}` : ""} enviado en segundo plano a "${res.window}" en (${targetX}, ${targetY}). El raton del usuario no se ha movido.${despues}`;
557
763
  }
558
764
  const res = await winHost.send("click", { x: targetX, y: targetY, button, count });
559
765
  if (!res.ok)
560
766
  return fail(action, res.error || "fallo desconocido");
561
- return `Clic ${button}${count > 1 ? ` x${count}` : ""} en (${targetX}, ${targetY}) sobre [${res.window}].`;
767
+ const despues = params.window ? await estadoTrasActuar(windowArgs(params)) : "";
768
+ if (despues.includes("no cambio en nada visible")) {
769
+ return `Hice clic pero la ventana NO reacciono (no cambio en nada visible). No asumas que funciono; comprueba con ui_snapshot o usa clic por coordenadas.${despues}`;
770
+ }
771
+ return `Clic ${button}${count > 1 ? ` x${count}` : ""} en (${targetX}, ${targetY}) sobre [${res.window}].${despues}`;
562
772
  }
563
773
  case "mouse_move":
564
774
  case "move": {
@@ -590,6 +800,115 @@ export async function computerUse(params, cwd = process.cwd(), apiKey) {
590
800
  return fail(action, res.error || "fallo desconocido");
591
801
  return `Scroll ${res.direction} x${res.amount}.`;
592
802
  }
803
+ // Scroll hacia un elemento o texto hasta que entre en el área visible (estilo Astra / OpenAI Operator)
804
+ case "scroll_into_view":
805
+ case "scroll_to": {
806
+ const rawTarget = params.target ?? params.query ?? params.text ?? params.ref;
807
+ if (rawTarget === undefined || rawTarget === "") {
808
+ return fail(action, 'indica el elemento o texto objetivo en "target", "query" o "ref".');
809
+ }
810
+ const targetStr = String(rawTarget).trim();
811
+ const targetRef = typeof rawTarget === "number" ? rawTarget : (typeof params.ref === "number" ? params.ref : undefined);
812
+ const maxAttempts = Math.min(Math.max(params.maxAttempts ?? params.repeat ?? 5, 1), 10);
813
+ const direction = params.direction || "down";
814
+ // POR QUE: Formularios y paneles web/escritorio no caben en una sola pantalla.
815
+ // Operator y Astra usan scroll iterativo comprobando tras cada paso si el elemento
816
+ // ya entro en el area visible, en vez de adivinar o asumir que el formulario cabe entero.
817
+ const choice = await resolveWindow(params);
818
+ if (params.window && !choice.ok)
819
+ return fail(action, choice.error);
820
+ // Si tenemos ref y soporta ScrollItemPattern nativo, intentarlo primero
821
+ if (targetRef) {
822
+ const direct = await winHost.send("ui_scroll_into_view", { ref: targetRef });
823
+ if (direct.ok) {
824
+ await new Promise((r) => setTimeout(r, 150));
825
+ return `Elemento [${targetRef}] desplazado a la vista mediante patron de scroll.`;
826
+ }
827
+ }
828
+ // Obtener limites de la ventana para verificar visibilidad real
829
+ let winX = 0, winY = 0, winW = 1920, winH = 1080;
830
+ if (choice.ok && choice.hwnd) {
831
+ const wInfo = await winHost.send("find_window", { ...pinned(choice) });
832
+ if (wInfo.ok && wInfo.width > 0) {
833
+ winX = wInfo.x;
834
+ winY = wInfo.y;
835
+ winW = wInfo.width;
836
+ winH = wInfo.height;
837
+ }
838
+ }
839
+ const scrollCenterX = Math.round(winX + winW / 2);
840
+ const scrollCenterY = Math.round(winY + winH / 2);
841
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
842
+ const snap = await winHost.send("ui_snapshot", {
843
+ ...pinned(choice),
844
+ ...(choice.hwnd ? {} : windowArgs(params)),
845
+ max: 200,
846
+ filter: "",
847
+ interactiveOnly: false,
848
+ }, 25_000);
849
+ if (snap.ok && Array.isArray(snap.elements)) {
850
+ let found = null;
851
+ for (const el of snap.elements) {
852
+ if (targetRef !== undefined && el.ref === targetRef) {
853
+ found = el;
854
+ break;
855
+ }
856
+ const textoControl = `${el.name || ""} ${el.role || ""} ${el.value || ""}`.toLowerCase();
857
+ if (textoControl.includes(targetStr.toLowerCase())) {
858
+ found = el;
859
+ break;
860
+ }
861
+ }
862
+ if (found) {
863
+ const inViewX = found.x >= winX && found.x <= winX + winW;
864
+ const inViewY = found.y >= winY && found.y <= winY + winH;
865
+ if (inViewX && inViewY) {
866
+ return `Elemento "${targetStr}" visible en la ventana tras ${attempt} intento(s) (ref [${found.ref}], ${found.role} "${found.name}" en ${found.x}, ${found.y}).`;
867
+ }
868
+ }
869
+ }
870
+ if (attempt < maxAttempts) {
871
+ await winHost.send("scroll", {
872
+ x: scrollCenterX,
873
+ y: scrollCenterY,
874
+ direction,
875
+ amount: params.amount ?? 4,
876
+ });
877
+ await new Promise((r) => setTimeout(r, 250));
878
+ }
879
+ }
880
+ return fail(action, `no se encontro el elemento "${targetStr}" visible en "${choice.title || params.window || "la ventana"}" tras ${maxAttempts} intentos de scroll. Comprueba con ui_snapshot o usa screenshot con analyze.`);
881
+ }
882
+ // Extrae texto de la ventana mediante Win32 nativo o visión (Astra / Operator)
883
+ case "read_text":
884
+ case "get_text": {
885
+ const choice = await resolveWindow(params);
886
+ if (params.window && !choice.ok)
887
+ return fail(action, choice.error);
888
+ // POR QUE: Si la aplicacion es Win32 clasica (Notepad, dialogos, controles antiguos)
889
+ // pero UIA no devuelve arbol, extraemos el texto directamente por GetWindowText/WM_GETTEXT
890
+ // sin coste de tokens ni latencia de red. Si es un canvas o lienzo grafico, usamos vision.
891
+ const nativo = await winHost.send("extract_text", {
892
+ ...pinned(choice),
893
+ ...(choice.hwnd ? {} : windowArgs(params)),
894
+ });
895
+ if (nativo.ok && Array.isArray(nativo.lines) && nativo.lines.length > 1) {
896
+ return `Texto extraído de "${nativo.title || params.window || "la ventana"}":\n` +
897
+ nativo.lines.map((l) => ` ${l}`).join("\n");
898
+ }
899
+ const out = resolveOut(params.outputPath, "read_text_latest.jpg", cwd);
900
+ const shot = await captureFor(params, out, choice);
901
+ if (!shot.ok)
902
+ return fail(action, shot.error || "no se pudo capturar la ventana");
903
+ if (apiKey) {
904
+ const desc = await describeScreen(apiKey, out, params.question || "Transcribe fielmente todo el texto visible en esta ventana, linea por linea, respetando la distribucion.");
905
+ if (desc.ok) {
906
+ return `Texto extraído visualmente de "${shot.title || params.window || "la pantalla"}":\n${desc.text}`;
907
+ }
908
+ }
909
+ return (`La ventana "${shot.title || params.window || "objetivo"}" no expone texto nativo accesible.\n` +
910
+ `Es un lienzo gráfico (canvas, juego o app acelerada). Usa 'screenshot' con 'analyze: true' o proporciona una API key para visión automática.`);
911
+ }
593
912
  case "type": {
594
913
  if (params.text === undefined)
595
914
  return fail(action, 'falta "text".');
@@ -705,8 +1024,22 @@ async function captureFor(params, outPath, choice) {
705
1024
  originX: 0,
706
1025
  originY: 0,
707
1026
  };
1027
+ let rx1 = 0, ry1 = 0, rx2 = 0, ry2 = 0;
1028
+ if (params.region && params.region.length === 4) {
1029
+ [rx1, ry1, rx2, ry2] = params.region;
1030
+ }
1031
+ const drawGrid = params.grid === true;
708
1032
  if (params.window) {
709
- const res = await winHost.send("capture_window", { ...(choice?.hwnd ? { hwnd: choice.hwnd } : { window: params.window }), path: outPath, maxLongEdge: 1568 }, 45_000);
1033
+ const res = await winHost.send("capture_window", {
1034
+ ...(choice?.hwnd ? { hwnd: choice.hwnd } : { window: params.window }),
1035
+ path: outPath,
1036
+ maxLongEdge: 1568,
1037
+ grid: drawGrid,
1038
+ rx1,
1039
+ ry1,
1040
+ rx2,
1041
+ ry2,
1042
+ }, 45_000);
710
1043
  if (!res.ok)
711
1044
  return { ...bad, error: res.error };
712
1045
  return {
@@ -722,7 +1055,16 @@ async function captureFor(params, outPath, choice) {
722
1055
  originY: res.windowY,
723
1056
  };
724
1057
  }
725
- const res = await winHost.send("capture_screen", { path: outPath, maxLongEdge: 1568, cursor: true }, 45_000);
1058
+ const res = await winHost.send("capture_screen", {
1059
+ path: outPath,
1060
+ maxLongEdge: 1568,
1061
+ cursor: true,
1062
+ grid: drawGrid,
1063
+ rx1,
1064
+ ry1,
1065
+ rx2,
1066
+ ry2,
1067
+ }, 45_000);
726
1068
  if (!res.ok)
727
1069
  return { ...bad, error: res.error };
728
1070
  return {
@@ -78,15 +78,18 @@ export const COMPUTER_USE_TOOL = {
78
78
  type: "function",
79
79
  function: {
80
80
  name: "computer_use",
81
- description: "Controla aplicaciones de ESCRITORIO de Windows. Para paginas web usa siempre 'browser', que es mas fiable. " +
82
- "REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta. " +
81
+ description: "Controla aplicaciones de ESCRITORIO de Windows con capacidades multimodales avanzadas (nivel Astra y OpenAI Operator). Para paginas web usa siempre 'browser', que es mas fiable. " +
82
+ "REGLA DE ORO: nunca hagas clic en una coordenada que no te haya dado antes una herramienta o una cuadrícula visual. " +
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
- "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. " +
89
- "Las coordenadas siempre son pixeles fisicos de pantalla.",
85
+ "FLUJO COMPUESTO (RAPIDO): usa 'sequence' con 'steps' para encadenar clic -> escritura -> Enter en un solo turno. " +
86
+ "CUANTO CUESTA CADA COSA, que decide cual usar: ui_snapshot tarda unos 40 ms y te devuelve texto que entiendes directo. " +
87
+ "screenshot cuesta una llamada entera a otro modelo -varios segundos- mas los tokens de la imagen. " +
88
+ "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. " +
89
+ "Para todo lo demas, ui_snapshot y luego find_element. " +
90
+ "CAMBIOS VISUALES: usa 'wait_visual_change' cuando interactúes con lienzos gráficos, juegos, videos o transiciones visuales donde el árbol no cambia. " +
91
+ "CUADRÍCULA VISUAL: usa 'grid: true' en screenshot o find_element para ver marcas de coordenadas exactas. " +
92
+ "Las coordenadas siempre son pixeles fisicos de pantalla o normalizadas 0..1000.",
90
93
  parameters: {
91
94
  type: "object",
92
95
  properties: {
@@ -100,29 +103,45 @@ export const COMPUTER_USE_TOOL = {
100
103
  "ui_focus",
101
104
  "find_element",
102
105
  "screenshot",
106
+ "sequence",
107
+ "scroll_into_view",
108
+ "scroll_to",
109
+ "read_text",
110
+ "get_text",
103
111
  "click",
104
112
  "left_click",
113
+ "mouse_click",
105
114
  "double_click",
106
115
  "triple_click",
107
116
  "right_click",
108
117
  "middle_click",
109
118
  "mouse_move",
119
+ "move",
110
120
  "left_click_drag",
121
+ "drag",
111
122
  "scroll",
112
123
  "type",
113
124
  "key",
114
125
  "hotkey",
115
126
  "press",
127
+ "key_press",
116
128
  "wait",
117
129
  "wait_change",
130
+ "wait_visual_change",
118
131
  "cursor_position",
119
132
  "focus_window",
120
133
  "get_active_window",
121
134
  "open_app",
122
135
  ],
123
136
  description: "list_windows: inventario de ventanas. ui_snapshot: lista los controles reales de una ventana con su ref. " +
124
- "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. " +
125
- "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.",
137
+ "ui_click/ui_type/ui_focus: actúan sobre un ref sin tocar el ratón (con auto-recuperación por coordenadas). " +
138
+ "sequence: ejecuta una lista de acciones encadenadas ('steps') en una sola llamada, sin esperar turnos adicionales. " +
139
+ "find_element: busca un control por texto en el árbol o visualmente con cuadrícula. " +
140
+ "scroll_into_view: hace scroll iterativo hasta que el control/texto indicado ('target'/'ref') entra en el área visible. " +
141
+ "read_text: extrae texto de la ventana (vía controles nativos Win32 o visión multimodal) cuando el árbol UIA no lo expone. " +
142
+ "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). " +
143
+ "wait_visual_change: detecta cambios de píxeles en pantalla/ventana/región (para lienzos, juegos y transiciones). " +
144
+ "screenshot: captura ventana o pantalla completa (soporta grid:true y region:[x1,y1,x2,y2]).",
126
145
  },
127
146
  window: {
128
147
  type: "string",
@@ -137,15 +156,17 @@ export const COMPUTER_USE_TOOL = {
137
156
  ref: { type: "integer", description: "Numero de control devuelto por ui_snapshot." },
138
157
  query: { type: "string", description: "Texto del control a buscar en find_element." },
139
158
  filter: { type: "string", description: "Filtra ui_snapshot o list_windows por texto." },
159
+ target: { type: "string", description: "Texto o ref objetivo para scroll_into_view." },
160
+ maxAttempts: { type: "integer", description: "Máximo de intentos de scroll para scroll_into_view (por defecto 5, tope 10)." },
140
161
  max: { type: "integer", description: "Maximo de controles en ui_snapshot (por defecto 120)." },
141
162
  coordinate: {
142
163
  type: "array",
143
- items: { type: "integer" },
144
- description: "[x, y] en pixeles fisicos. Solo con coordenadas que te haya dado ui_snapshot o find_element.",
164
+ items: { type: "number" },
165
+ description: "[x, y] en píxeles físicos o coordenadas normalizadas 0..1000.",
145
166
  },
146
- x: { type: "integer", description: "Coordenada X en pixeles fisicos." },
147
- y: { type: "integer", description: "Coordenada Y en pixeles fisicos." },
148
- start_coordinate: { type: "array", items: { type: "integer" }, description: "[x, y] de inicio del arrastre." },
167
+ x: { type: "number", description: "Coordenada X en píxeles o normalizada." },
168
+ y: { type: "number", description: "Coordenada Y en píxeles o normalizada." },
169
+ start_coordinate: { type: "array", items: { type: "number" }, description: "[x, y] de inicio del arrastre." },
149
170
  text: { type: "string", description: "Texto a escribir." },
150
171
  keys: {
151
172
  type: "string",
@@ -160,6 +181,10 @@ export const COMPUTER_USE_TOOL = {
160
181
  outputPath: { type: "string", description: "Ruta donde guardar la captura." },
161
182
  question: { type: "string", description: "Pregunta concreta para el analisis visual de la captura." },
162
183
  analyze: { type: "boolean", description: "false para guardar la captura sin gastar tokens de vision." },
184
+ grid: { type: "boolean", description: "true dibuja una cuadrícula de coordenadas sobre la captura para facilitar la localización visual exacta." },
185
+ region: { type: "array", items: { type: "integer" }, description: "[x1, y1, x2, y2] área de interés para captura o espera visual a máxima resolución." },
186
+ steps: { type: "array", items: { type: "object" }, description: "Lista de acciones encadenadas para action='sequence'." },
187
+ normalized: { type: "boolean", description: "true si las coordenadas están en escala 0..1000 estilo Operator/Astra." },
163
188
  appName: { type: "string", description: "Aplicacion o URL a lanzar con open_app." },
164
189
  },
165
190
  required: ["action"],