chocolatito-code 1.6.13 → 1.6.15

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 (49) hide show
  1. package/COMPUTER-USE.md +37 -0
  2. package/README.md +29 -3
  3. package/dist/agent/historialDeCambios.d.ts +77 -0
  4. package/dist/agent/historialDeCambios.js +220 -0
  5. package/dist/agent/salidaDelAgente.d.ts +9 -0
  6. package/dist/agent/salidaDelAgente.js +19 -0
  7. package/dist/agent/undoManager.d.ts +21 -0
  8. package/dist/agent/undoManager.js +85 -5
  9. package/dist/config/permissions.js +4 -0
  10. package/dist/index.js +216 -27
  11. package/dist/prompts/systemPrompt.js +15 -11
  12. package/dist/servidor/archivos.d.ts +69 -0
  13. package/dist/servidor/archivos.js +156 -0
  14. package/dist/servidor/captura.d.ts +9 -0
  15. package/dist/servidor/captura.js +48 -0
  16. package/dist/servidor/fondo.d.ts +85 -0
  17. package/dist/servidor/fondo.js +134 -0
  18. package/dist/servidor/marca.d.ts +14 -0
  19. package/dist/servidor/marca.js +14 -0
  20. package/dist/servidor/pagina.d.ts +0 -26
  21. package/dist/servidor/pagina.js +337 -10
  22. package/dist/servidor/puente.d.ts +13 -0
  23. package/dist/servidor/puente.js +11 -0
  24. package/dist/servidor/servidor.d.ts +76 -2
  25. package/dist/servidor/servidor.js +245 -13
  26. package/dist/tools/computerUse.d.ts +11 -1
  27. package/dist/tools/computerUse.js +376 -34
  28. package/dist/tools/toolDefsComputer.js +39 -14
  29. package/dist/tools/visionBridge.d.ts +4 -2
  30. package/dist/tools/visionBridge.js +40 -15
  31. package/dist/tools/win/hostScript.js +300 -29
  32. package/dist/ui/comandos.js +1 -0
  33. package/dist/ui/ink/App.js +23 -3
  34. package/dist/ui/ink/Prompt.d.ts +5 -1
  35. package/dist/ui/ink/Prompt.js +92 -4
  36. package/dist/ui/ink/control.d.ts +2 -0
  37. package/dist/ui/ink/control.js +15 -0
  38. package/dist/ui/ink/montarApp.d.ts +0 -19
  39. package/dist/ui/ink/montarApp.js +3 -0
  40. package/dist/ui/permissionPrompt.js +44 -9
  41. package/dist/ui/pieFijo.js +5 -0
  42. package/dist/ui/prompt.js +6 -0
  43. package/dist/ui/renderer.d.ts +1 -0
  44. package/dist/ui/renderer.js +27 -2
  45. package/dist/ui/selector.js +5 -0
  46. package/dist/ui/spinner.d.ts +1 -0
  47. package/dist/ui/spinner.js +6 -0
  48. package/extension/manifest.json +1 -1
  49. package/package.json +1 -1
@@ -28,10 +28,12 @@ export interface LocateResult {
28
28
  found?: boolean;
29
29
  reason?: string;
30
30
  error?: string;
31
+ bbox?: [number, number, number, number];
31
32
  }
32
33
  /**
33
34
  * Localiza un elemento y devuelve su centro EN PIXELES DE LA IMAGEN.
34
- * La conversion a coordenadas de pantalla la hace quien llama, que es el unico
35
- * que conoce el factor de escala y el origen de la captura.
35
+ * Soporta modelos multimodales (DeepSeek Vision, GPT-4o, Claude, Gemini Astra/Operator),
36
+ * coordenadas directas en píxeles, cuadrículas visuales, bounding boxes [x1, y1, x2, y2]
37
+ * o box_2d normalizado [ymin, xmin, ymax, xmax].
36
38
  */
37
39
  export declare function locateElement(apiKey: string, imagePath: string, target: string, imageWidth: number, imageHeight: number): Promise<LocateResult>;
@@ -122,26 +122,29 @@ async function ask(apiKey, prompt, imagePath, maxTokens) {
122
122
  /** Describe lo que se ve, orientado a decidir el siguiente paso. */
123
123
  export async function describeScreen(apiKey, imagePath, question) {
124
124
  const q = question && question.trim().length > 0 ? question.trim() : "Describe el estado actual de la pantalla.";
125
- const prompt = `Eres los ojos de un agente que controla este ordenador. Observa la captura y responde en espanol, ` +
126
- `de forma concreta y sin adornos.\n\n` +
125
+ const prompt = `Eres los ojos de un agente avanzado que controla este ordenador (estilo Google Project Astra y OpenAI Operator). ` +
126
+ `Observa la captura y responde en español, de forma concreta, estructurada y sin adornos.\n\n` +
127
127
  `Pregunta: ${q}\n\n` +
128
- `Incluye siempre: que aplicacion y que pantalla concreta se ve, que elementos accionables hay ` +
129
- `(botones, campos, pestanas) con su texto exacto, y si hay dialogos, errores o cargas en curso. ` +
130
- `Si algo esta a medio cargar, dilo explicitamente. No inventes elementos que no aparezcan.\n` +
131
- `Se breve: como mucho 12 lineas. Nada de preambulos.`;
128
+ `Incluye siempre: qué aplicación y qué pantalla o diálogo concreto se ve, qué elementos accionables hay ` +
129
+ `(botones, campos, pestañas, menús) con su texto exacto, coordenadas de cuadrícula si están visibles, ` +
130
+ `y si hay diálogos modales, alertas, spinners de carga o errores. ` +
131
+ `Si algo está a medio cargar, dilo explícitamente. No inventes elementos que no aparezcan.\n` +
132
+ `Sé conciso: máximo 14 líneas, directo al grano.`;
132
133
  return ask(apiKey, prompt, imagePath, 1600);
133
134
  }
134
135
  /**
135
136
  * Localiza un elemento y devuelve su centro EN PIXELES DE LA IMAGEN.
136
- * La conversion a coordenadas de pantalla la hace quien llama, que es el unico
137
- * que conoce el factor de escala y el origen de la captura.
137
+ * Soporta modelos multimodales (DeepSeek Vision, GPT-4o, Claude, Gemini Astra/Operator),
138
+ * coordenadas directas en píxeles, cuadrículas visuales, bounding boxes [x1, y1, x2, y2]
139
+ * o box_2d normalizado [ymin, xmin, ymax, xmax].
138
140
  */
139
141
  export async function locateElement(apiKey, imagePath, target, imageWidth, imageHeight) {
140
- const prompt = `Observa la captura de pantalla, de ${imageWidth} x ${imageHeight} pixeles.\n\n` +
142
+ const prompt = `Observa la captura de pantalla de ${imageWidth} x ${imageHeight} píxeles.\n` +
143
+ `Si la imagen tiene una cuadrícula de coordenadas con números (x,y), úsalos como referencia precisa.\n\n` +
141
144
  `Localiza este elemento: "${target}"\n\n` +
142
- `Responde UNICAMENTE con un objeto JSON, sin texto alrededor y sin bloques de codigo:\n` +
143
- `{"found": true, "x": <centro horizontal en pixeles>, "y": <centro vertical en pixeles>, "reason": "<que viste>"}\n` +
144
- `Si el elemento no aparece en la captura, responde {"found": false, "x": 0, "y": 0, "reason": "<por que>"}.\n` +
145
+ `Responde ÚNICAMENTE con un objeto JSON, sin texto alrededor y sin bloques de código:\n` +
146
+ `{"found": true, "x": <centro horizontal en píxeles>, "y": <centro vertical en píxeles>, "reason": "<qué viste>", "bbox": [<x1>, <y1>, <x2>, <y2>]}\n` +
147
+ `Si el elemento no aparece en la captura, responde {"found": false, "x": 0, "y": 0, "reason": "<por qué>"}.\n` +
145
148
  `El origen (0,0) es la esquina superior izquierda de la imagen. No inventes: si dudas, found=false.`;
146
149
  const res = await ask(apiKey, prompt, imagePath, 900);
147
150
  if (!res.ok)
@@ -152,10 +155,31 @@ export async function locateElement(apiKey, imagePath, target, imageWidth, image
152
155
  return { ok: false, error: `El modelo de vision no devolvio JSON: ${raw.slice(0, 200)}` };
153
156
  try {
154
157
  const parsed = JSON.parse(match[0]);
155
- const x = Math.round(Number(parsed.x));
156
- const y = Math.round(Number(parsed.y));
158
+ let x = Number(parsed.x);
159
+ let y = Number(parsed.y);
160
+ // Soporte para box_2d estilo Gemini/Astra: [ymin, xmin, ymax, xmax] normalizado 0..1000
161
+ if ((!Number.isFinite(x) || !Number.isFinite(y) || (x === 0 && y === 0)) && Array.isArray(parsed.box_2d) && parsed.box_2d.length === 4) {
162
+ const [ymin, xmin, ymax, xmax] = parsed.box_2d.map(Number);
163
+ x = Math.round(((xmin + xmax) / 2.0 / 1000.0) * imageWidth);
164
+ y = Math.round(((ymin + ymax) / 2.0 / 1000.0) * imageHeight);
165
+ }
166
+ else if (Array.isArray(parsed.bbox) && parsed.bbox.length === 4 && (!Number.isFinite(x) || !Number.isFinite(y))) {
167
+ const [x1, y1, x2, y2] = parsed.bbox.map(Number);
168
+ x = Math.round((x1 + x2) / 2.0);
169
+ y = Math.round((y1 + y2) / 2.0);
170
+ }
171
+ // Coordenadas normalizadas 0.0 a 1.0
172
+ if (x > 0 && x <= 1.0 && y > 0 && y <= 1.0) {
173
+ x = Math.round(x * imageWidth);
174
+ y = Math.round(y * imageHeight);
175
+ }
176
+ else if (parsed.normalized === true) {
177
+ x = Math.round((x / 1000.0) * imageWidth);
178
+ y = Math.round((y / 1000.0) * imageHeight);
179
+ }
180
+ x = Math.round(x);
181
+ y = Math.round(y);
157
182
  const found = parsed.found === true && Number.isFinite(x) && Number.isFinite(y);
158
- // Un punto fuera de la imagen es una alucinacion, no un acierto.
159
183
  const inside = x >= 0 && y >= 0 && x <= imageWidth && y <= imageHeight;
160
184
  return {
161
185
  ok: true,
@@ -163,6 +187,7 @@ export async function locateElement(apiKey, imagePath, target, imageWidth, image
163
187
  x,
164
188
  y,
165
189
  reason: typeof parsed.reason === "string" ? parsed.reason : "",
190
+ bbox: Array.isArray(parsed.bbox) ? parsed.bbox : undefined,
166
191
  };
167
192
  }
168
193
  catch (err) {
@@ -52,6 +52,8 @@ public static class Choco
52
52
  [DllImport("user32.dll")] static extern bool ClientToScreen(IntPtr h, ref POINT p);
53
53
  [DllImport("user32.dll")] static extern bool ScreenToClient(IntPtr h, ref POINT p);
54
54
  [DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr l);
55
+ [DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr hWndParent, EnumProc lpEnumFunc, IntPtr lParam);
56
+ [DllImport("user32.dll", CharSet = CharSet.Auto)] static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, StringBuilder lParam);
55
57
  [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
56
58
  [DllImport("user32.dll")] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
57
59
  [DllImport("user32.dll")] static extern bool PostMessage(IntPtr h, uint msg, IntPtr w, IntPtr l);
@@ -304,49 +306,102 @@ public static class Choco
304
306
  return scale;
305
307
  }
306
308
 
307
- public static string CaptureScreen(string path, int maxLongEdge, bool drawCursor)
309
+ static void DrawCoordinateGrid(Graphics g, int width, int height, int originX, int originY)
310
+ {
311
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
312
+ using (Pen gridPen = new Pen(Color.FromArgb(100, 230, 60, 60), 1))
313
+ using (Brush textBrush = new SolidBrush(Color.FromArgb(245, 255, 255, 255)))
314
+ using (Brush bgBrush = new SolidBrush(Color.FromArgb(160, 20, 20, 20)))
315
+ using (Font font = new Font(FontFamily.GenericMonospace, 8.5f, FontStyle.Bold))
316
+ {
317
+ gridPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
318
+ int step = 100;
319
+ for (int x = 0; x < width; x += step)
320
+ {
321
+ g.DrawLine(gridPen, x, 0, x, height);
322
+ }
323
+ for (int y = 0; y < height; y += step)
324
+ {
325
+ g.DrawLine(gridPen, 0, y, width, y);
326
+ }
327
+ for (int x = 0; x < width; x += step)
328
+ {
329
+ for (int y = 0; y < height; y += step)
330
+ {
331
+ if (x % 200 == 0 && y % 200 == 0 && x + 70 < width && y + 16 < height)
332
+ {
333
+ string lbl = (originX + x) + "," + (originY + y);
334
+ SizeF sz = g.MeasureString(lbl, font);
335
+ g.FillRectangle(bgBrush, x + 2, y + 2, sz.Width + 2, sz.Height);
336
+ g.DrawString(lbl, font, textBrush, x + 3, y + 2);
337
+ }
338
+ }
339
+ }
340
+ }
341
+ }
342
+
343
+ public static string CaptureScreen(string path, int maxLongEdge, bool drawCursor, bool drawGrid, int rx1, int ry1, int rx2, int ry2)
308
344
  {
309
345
  int vx = GetSystemMetrics(76), vy = GetSystemMetrics(77);
310
346
  int vw = GetSystemMetrics(78), vh = GetSystemMetrics(79);
311
- using (Bitmap bmp = new Bitmap(vw, vh, PixelFormat.Format32bppArgb))
347
+ int sx = vx, sy = vy, sw = vw, sh = vh;
348
+ if (rx2 > rx1 && ry2 > ry1)
349
+ {
350
+ sx = vx + rx1; sy = vy + ry1;
351
+ sw = Math.Min(vw, rx2 - rx1); sh = Math.Min(vh, ry2 - ry1);
352
+ }
353
+ if (sw <= 0 || sh <= 0) return "{\"ok\":false,\"error\":\"area invalida para captura\"}";
354
+
355
+ using (Bitmap bmp = new Bitmap(sw, sh, PixelFormat.Format32bppArgb))
312
356
  {
313
357
  using (Graphics g = Graphics.FromImage(bmp))
314
358
  {
315
- g.CopyFromScreen(vx, vy, 0, 0, new Size(vw, vh), CopyPixelOperation.SourceCopy);
359
+ g.CopyFromScreen(sx, sy, 0, 0, new Size(sw, sh), CopyPixelOperation.SourceCopy);
316
360
  if (drawCursor)
317
361
  {
318
362
  POINT c; GetCursorPos(out c);
319
- g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
320
- using (Pen p = new Pen(Color.FromArgb(255, 235, 75, 75), 3))
363
+ if (c.X >= sx && c.X <= sx + sw && c.Y >= sy && c.Y <= sy + sh)
321
364
  {
322
- g.DrawEllipse(p, c.X - vx - 16, c.Y - vy - 16, 32, 32);
323
- g.DrawLine(p, c.X - vx - 24, c.Y - vy, c.X - vx + 24, c.Y - vy);
324
- g.DrawLine(p, c.X - vx, c.Y - vy - 24, c.X - vx, c.Y - vy + 24);
365
+ g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
366
+ using (Pen p = new Pen(Color.FromArgb(255, 235, 75, 75), 3))
367
+ {
368
+ int relX = c.X - sx;
369
+ int relY = c.Y - sy;
370
+ g.DrawEllipse(p, relX - 16, relY - 16, 32, 32);
371
+ g.DrawLine(p, relX - 24, relY, relX + 24, relY);
372
+ g.DrawLine(p, relX, relY - 24, relX, relY + 24);
373
+ }
325
374
  }
326
375
  }
376
+ if (drawGrid)
377
+ {
378
+ DrawCoordinateGrid(g, sw, sh, sx, sy);
379
+ }
327
380
  }
328
381
  double s = SaveScaled(bmp, path, maxLongEdge);
329
- return "{\"ok\":true,\"path\":" + Q(path) + ",\"screenWidth\":" + N(vw) + ",\"screenHeight\":" + N(vh) +
330
- ",\"originX\":" + N(vx) + ",\"originY\":" + N(vy) +
331
- ",\"imageWidth\":" + N((int)Math.Round(vw * s)) + ",\"imageHeight\":" + N((int)Math.Round(vh * s)) +
382
+ return "{\"ok\":true,\"path\":" + Q(path) + ",\"screenWidth\":" + N(sw) + ",\"screenHeight\":" + N(sh) +
383
+ ",\"originX\":" + N(sx) + ",\"originY\":" + N(sy) +
384
+ ",\"imageWidth\":" + N((int)Math.Round(sw * s)) + ",\"imageHeight\":" + N((int)Math.Round(sh * s)) +
332
385
  ",\"scale\":" + s.ToString("0.#####", CultureInfo.InvariantCulture) + "}";
333
- }
386
+ }
334
387
  }
335
388
 
336
389
  // Captura UNA ventana concreta sin traerla al frente.
337
390
  // PrintWindow(PW_RENDERFULLCONTENT=2) pide a la ventana que se redibuje en
338
391
  // nuestro DC, asi funciona aunque este tapada o en segundo plano.
339
- public static string CaptureWindow(IntPtr h, string path, int maxLongEdge, bool clientOnly)
392
+ public static string CaptureWindow(IntPtr h, string path, int maxLongEdge, bool clientOnly, bool drawGrid, int rx1, int ry1, int rx2, int ry2)
340
393
  {
341
394
  if (h == IntPtr.Zero) return "{\"ok\":false,\"error\":\"ventana no encontrada\"}";
342
395
  RECT r;
343
396
  if (clientOnly) { GetClientRect(h, out r); } else { GetWindowRect(h, out r); }
344
397
  int w = r.Right - r.Left, hh = r.Bottom - r.Top;
345
398
  if (w <= 0 || hh <= 0) return "{\"ok\":false,\"error\":\"ventana sin area visible (minimizada?)\"}";
346
- using (Bitmap bmp = new Bitmap(w, hh, PixelFormat.Format32bppArgb))
399
+ RECT wr2; GetWindowRect(h, out wr2);
400
+
401
+ using (Bitmap full = new Bitmap(w, hh, PixelFormat.Format32bppArgb))
347
402
  {
348
403
  bool printed = false;
349
- using (Graphics g = Graphics.FromImage(bmp))
404
+ using (Graphics g = Graphics.FromImage(full))
350
405
  {
351
406
  IntPtr hdc = g.GetHdc();
352
407
  try { printed = PrintWindow(h, hdc, 2); }
@@ -354,19 +409,52 @@ public static class Choco
354
409
  }
355
410
  // Chrome/Electron a veces devuelven negro con PrintWindow: si sale casi
356
411
  // todo negro y la ventana esta visible, caemos a copia de pantalla.
357
- if (!printed || IsMostlyBlank(bmp))
412
+ if (!printed || IsMostlyBlank(full))
358
413
  {
359
- RECT wr; GetWindowRect(h, out wr);
360
- using (Graphics g2 = Graphics.FromImage(bmp))
361
- g2.CopyFromScreen(wr.Left, wr.Top, 0, 0, new Size(w, hh), CopyPixelOperation.SourceCopy);
414
+ using (Graphics g2 = Graphics.FromImage(full))
415
+ g2.CopyFromScreen(wr2.Left, wr2.Top, 0, 0, new Size(w, hh), CopyPixelOperation.SourceCopy);
416
+ }
417
+
418
+ int cw = w, ch = hh, cx = 0, cy = 0;
419
+ if (rx2 > rx1 && ry2 > ry1)
420
+ {
421
+ cx = Math.Max(0, rx1); cy = Math.Max(0, ry1);
422
+ cw = Math.Min(w - cx, rx2 - rx1); ch = Math.Min(hh - cy, ry2 - ry1);
423
+ }
424
+ if (cw <= 0 || ch <= 0) return "{\"ok\":false,\"error\":\"region fuera de los limites de la ventana\"}";
425
+
426
+ Bitmap targetBmp = full;
427
+ bool isCropped = (cx > 0 || cy > 0 || cw < w || ch < hh);
428
+ if (isCropped)
429
+ {
430
+ Bitmap crop = new Bitmap(cw, ch, PixelFormat.Format32bppArgb);
431
+ using (Graphics gCrop = Graphics.FromImage(crop))
432
+ {
433
+ gCrop.DrawImage(full, new Rectangle(0, 0, cw, ch), new Rectangle(cx, cy, cw, ch), GraphicsUnit.Pixel);
434
+ }
435
+ targetBmp = crop;
436
+ }
437
+
438
+ try
439
+ {
440
+ if (drawGrid)
441
+ {
442
+ using (Graphics gGrid = Graphics.FromImage(targetBmp))
443
+ {
444
+ DrawCoordinateGrid(gGrid, cw, ch, wr2.Left + cx, wr2.Top + cy);
445
+ }
446
+ }
447
+ double s = SaveScaled(targetBmp, path, maxLongEdge);
448
+ return "{\"ok\":true,\"path\":" + Q(path) + ",\"title\":" + Q(Title(h)) +
449
+ ",\"windowX\":" + N(wr2.Left + cx) + ",\"windowY\":" + N(wr2.Top + cy) +
450
+ ",\"windowWidth\":" + N(cw) + ",\"windowHeight\":" + N(ch) +
451
+ ",\"imageWidth\":" + N((int)Math.Round(cw * s)) + ",\"imageHeight\":" + N((int)Math.Round(ch * s)) +
452
+ ",\"scale\":" + s.ToString("0.#####", CultureInfo.InvariantCulture) + "}";
453
+ }
454
+ finally
455
+ {
456
+ if (isCropped && targetBmp != null) targetBmp.Dispose();
362
457
  }
363
- double s = SaveScaled(bmp, path, maxLongEdge);
364
- RECT wr2; GetWindowRect(h, out wr2);
365
- return "{\"ok\":true,\"path\":" + Q(path) + ",\"title\":" + Q(Title(h)) +
366
- ",\"windowX\":" + N(wr2.Left) + ",\"windowY\":" + N(wr2.Top) +
367
- ",\"windowWidth\":" + N(w) + ",\"windowHeight\":" + N(hh) +
368
- ",\"imageWidth\":" + N((int)Math.Round(w * s)) + ",\"imageHeight\":" + N((int)Math.Round(hh * s)) +
369
- ",\"scale\":" + s.ToString("0.#####", CultureInfo.InvariantCulture) + "}";
370
458
  }
371
459
  }
372
460
 
@@ -672,6 +760,98 @@ public static class Choco
672
760
  }
673
761
  }
674
762
 
763
+ public static string WaitVisualChange(IntPtr hwnd, int timeoutMs, int rx1, int ry1, int rx2, int ry2)
764
+ {
765
+ if (timeoutMs < 100) timeoutMs = 100;
766
+ if (timeoutMs > 30000) timeoutMs = 30000;
767
+ int vx = 0, vy = 0, vw = 0, vh = 0;
768
+ if (hwnd != IntPtr.Zero)
769
+ {
770
+ RECT r; GetWindowRect(hwnd, out r);
771
+ vx = r.Left; vy = r.Top; vw = r.Right - r.Left; vh = r.Bottom - r.Top;
772
+ }
773
+ else
774
+ {
775
+ vx = GetSystemMetrics(76); vy = GetSystemMetrics(77);
776
+ vw = GetSystemMetrics(78); vh = GetSystemMetrics(79);
777
+ }
778
+ if (rx2 > rx1 && ry2 > ry1)
779
+ {
780
+ vx += rx1; vy += ry1;
781
+ vw = Math.Min(vw, rx2 - rx1); vh = Math.Min(vh, ry2 - ry1);
782
+ }
783
+ if (vw <= 2 || vh <= 2) return "{\"ok\":false,\"error\":\"area invalida para cambio visual\"}";
784
+
785
+ int sampleW = Math.Min(vw, 64);
786
+ int sampleH = Math.Min(vh, 64);
787
+ byte[] prev = SamplePixels(vx, vy, vw, vh, sampleW, sampleH);
788
+
789
+ int elapsed = 0;
790
+ int interval = 120;
791
+ while (elapsed < timeoutMs)
792
+ {
793
+ Thread.Sleep(interval);
794
+ elapsed += interval;
795
+ byte[] curr = SamplePixels(vx, vy, vw, vh, sampleW, sampleH);
796
+ int diff = 0;
797
+ int total = curr.Length / 3;
798
+ for (int i = 0; i < curr.Length; i += 3)
799
+ {
800
+ int dr = Math.Abs((int)curr[i] - (int)prev[i]);
801
+ int dg = Math.Abs((int)curr[i + 1] - (int)prev[i + 1]);
802
+ int db = Math.Abs((int)curr[i + 2] - (int)prev[i + 2]);
803
+ if (dr + dg + db > 30) diff++;
804
+ }
805
+ double pct = (double)diff / (double)total;
806
+ if (pct > 0.015)
807
+ {
808
+ return "{\"ok\":true,\"changed\":true,\"diffPercent\":" + (pct * 100).ToString("0.#", CultureInfo.InvariantCulture) + ",\"elapsed\":" + N(elapsed) + "}";
809
+ }
810
+ }
811
+ return "{\"ok\":true,\"changed\":false,\"waited\":" + N(timeoutMs) + "}";
812
+ }
813
+
814
+ static byte[] SamplePixels(int x, int y, int w, int h, int sw, int sh)
815
+ {
816
+ byte[] bytes = new byte[sw * sh * 3];
817
+ try
818
+ {
819
+ using (Bitmap full = new Bitmap(w, h, PixelFormat.Format24bppRgb))
820
+ {
821
+ using (Graphics g = Graphics.FromImage(full))
822
+ {
823
+ g.CopyFromScreen(x, y, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy);
824
+ }
825
+ using (Bitmap thumb = new Bitmap(sw, sh, PixelFormat.Format24bppRgb))
826
+ {
827
+ using (Graphics g2 = Graphics.FromImage(thumb))
828
+ {
829
+ g2.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
830
+ g2.DrawImage(full, 0, 0, sw, sh);
831
+ }
832
+ BitmapData bd = thumb.LockBits(new Rectangle(0, 0, sw, sh), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
833
+ try
834
+ {
835
+ int stride = bd.Stride;
836
+ IntPtr ptr = bd.Scan0;
837
+ byte[] row = new byte[Math.Abs(stride)];
838
+ for (int rowIdx = 0; rowIdx < sh; rowIdx++)
839
+ {
840
+ Marshal.Copy(new IntPtr(ptr.ToInt64() + (rowIdx * stride)), row, 0, sw * 3);
841
+ Array.Copy(row, 0, bytes, rowIdx * sw * 3, sw * 3);
842
+ }
843
+ }
844
+ finally
845
+ {
846
+ thumb.UnlockBits(bd);
847
+ }
848
+ }
849
+ }
850
+ }
851
+ catch { }
852
+ return bytes;
853
+ }
854
+
675
855
  public static string RefRect(int id)
676
856
  {
677
857
  AutomationElement e = Ref(id);
@@ -702,11 +882,71 @@ public static class Choco
702
882
  if (e.TryGetCurrentPattern(SelectionItemPattern.Pattern, out p)) { ((SelectionItemPattern)p).Select(); return "{\"ok\":true,\"via\":\"select\",\"label\":" + Q(label) + "}"; }
703
883
  if (e.TryGetCurrentPattern(TogglePattern.Pattern, out p)) { ((TogglePattern)p).Toggle(); return "{\"ok\":true,\"via\":\"toggle\",\"label\":" + Q(label) + "}"; }
704
884
  if (e.TryGetCurrentPattern(ExpandCollapsePattern.Pattern, out p)) { ((ExpandCollapsePattern)p).Expand(); return "{\"ok\":true,\"via\":\"expand\",\"label\":" + Q(label) + "}"; }
705
- return "{\"ok\":false,\"error\":" + Q("El elemento " + label + " no expone patron activable; usa click sobre sus coordenadas.") + "}";
885
+ int cx = 0, cy = 0;
886
+ try {
887
+ System.Windows.Rect r = e.Current.BoundingRectangle;
888
+ cx = (int)Math.Round(r.X + r.Width / 2.0);
889
+ cy = (int)Math.Round(r.Y + r.Height / 2.0);
890
+ } catch { }
891
+ return "{\"ok\":false,\"noPattern\":true,\"x\":" + N(cx) + ",\"y\":" + N(cy) + ",\"label\":" + Q(label) + ",\"error\":" + Q("El elemento " + label + " no expone patron activable; usa click sobre sus coordenadas.") + "}";
706
892
  }
707
893
  catch (Exception ex) { return "{\"ok\":false,\"error\":" + Q(ex.Message) + "}"; }
708
894
  }
709
895
 
896
+ // POR QUE: Formularios y listas largas no caben en una sola pantalla. Si un control
897
+ // soporta ScrollItemPattern, lo traemos al area visible directamente sin mover el raton.
898
+ public static string RefScrollIntoView(int id)
899
+ {
900
+ AutomationElement e = Ref(id);
901
+ if (e == null) return "{\"ok\":false,\"error\":\"ref inexistente; repite ui_snapshot\"}";
902
+ try
903
+ {
904
+ object sp;
905
+ if (e.TryGetCurrentPattern(ScrollItemPattern.Pattern, out sp))
906
+ {
907
+ ((ScrollItemPattern)sp).ScrollIntoView();
908
+ return "{\"ok\":true,\"via\":\"scroll_item_pattern\"}";
909
+ }
910
+ return "{\"ok\":false,\"error\":\"el elemento no admite ScrollItemPattern\"}";
911
+ }
912
+ catch (Exception ex) { return "{\"ok\":false,\"error\":" + Q(ex.Message) + "}"; }
913
+ }
914
+
915
+ // POR QUE: Ventanas antiguas (WinForms, Delphi, MFC) o Electron con accesibilidad rota
916
+ // devuelven un árbol UIA vacío, dejando al modelo a ciegas. Este método recupera
917
+ // el texto de los controles nativos Win32 (editores, etiquetas, botones) directamente.
918
+ public static string ExtractWindowText(IntPtr hwnd)
919
+ {
920
+ if (hwnd == IntPtr.Zero) return "{\"ok\":false,\"error\":\"ventana no encontrada\"}";
921
+ List<string> lines = new List<string>();
922
+ HashSet<string> seen = new HashSet<string>();
923
+ string title = Title(hwnd);
924
+ if (!string.IsNullOrEmpty(title)) { lines.Add(title); seen.Add(title); }
925
+
926
+ EnumChildWindows(hwnd, (child, lParam) =>
927
+ {
928
+ if (!IsWindowVisible(child)) return true;
929
+ StringBuilder sb = new StringBuilder(1024);
930
+ GetWindowText(child, sb, 1024);
931
+ string txt = sb.ToString().Trim();
932
+ if (txt.Length == 0)
933
+ {
934
+ SendMessage(child, 0x000D, (IntPtr)1024, sb);
935
+ txt = sb.ToString().Trim();
936
+ }
937
+ if (txt.Length > 0 && !seen.Contains(txt))
938
+ {
939
+ seen.Add(txt);
940
+ lines.Add(txt);
941
+ }
942
+ return true;
943
+ }, IntPtr.Zero);
944
+
945
+ List<string> escaped = new List<string>();
946
+ foreach (string line in lines) escaped.Add(Q(line));
947
+ return "{\"ok\":true,\"title\":" + Q(title) + ",\"lines\":[" + string.Join(",", escaped.ToArray()) + "]}";
948
+ }
949
+
710
950
  public static string RefSetValue(int id, string text, bool replace)
711
951
  {
712
952
  AutomationElement e = Ref(id);
@@ -1052,16 +1292,47 @@ while ($true) {
1052
1292
  }
1053
1293
  'wait' { Start-Sleep -Milliseconds ([int](Get-Prop $cmd 'ms' 1000)); $res = '{"ok":true}' }
1054
1294
  'capture_screen' {
1055
- $res = [Choco]::CaptureScreen([string](Get-Prop $cmd 'path' ''), [int](Get-Prop $cmd 'maxLongEdge' 1568), [bool](Get-Prop $cmd 'cursor' $true))
1295
+ $res = [Choco]::CaptureScreen(
1296
+ [string](Get-Prop $cmd 'path' ''),
1297
+ [int](Get-Prop $cmd 'maxLongEdge' 1568),
1298
+ [bool](Get-Prop $cmd 'cursor' $true),
1299
+ [bool](Get-Prop $cmd 'grid' $false),
1300
+ [int](Get-Prop $cmd 'rx1' 0),
1301
+ [int](Get-Prop $cmd 'ry1' 0),
1302
+ [int](Get-Prop $cmd 'rx2' 0),
1303
+ [int](Get-Prop $cmd 'ry2' 0)
1304
+ )
1056
1305
  }
1057
1306
  'capture_window' {
1058
- $res = [Choco]::CaptureWindow((Resolve-Hwnd $cmd), [string](Get-Prop $cmd 'path' ''), [int](Get-Prop $cmd 'maxLongEdge' 1568), [bool](Get-Prop $cmd 'clientOnly' $false))
1307
+ $res = [Choco]::CaptureWindow(
1308
+ (Resolve-Hwnd $cmd),
1309
+ [string](Get-Prop $cmd 'path' ''),
1310
+ [int](Get-Prop $cmd 'maxLongEdge' 1568),
1311
+ [bool](Get-Prop $cmd 'clientOnly' $false),
1312
+ [bool](Get-Prop $cmd 'grid' $false),
1313
+ [int](Get-Prop $cmd 'rx1' 0),
1314
+ [int](Get-Prop $cmd 'ry1' 0),
1315
+ [int](Get-Prop $cmd 'rx2' 0),
1316
+ [int](Get-Prop $cmd 'ry2' 0)
1317
+ )
1059
1318
  }
1060
1319
  'ui_snapshot' {
1061
1320
  $res = [Choco]::Snapshot((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'max' 250), [string](Get-Prop $cmd 'filter' ''), [bool](Get-Prop $cmd 'interactiveOnly' $true))
1062
1321
  }
1063
1322
  'wait_change' { $res = [Choco]::WaitChange((Resolve-Hwnd $cmd), [int](Get-Prop $cmd 'timeoutMs' 3000)) }
1323
+ 'wait_visual_change' {
1324
+ $res = [Choco]::WaitVisualChange(
1325
+ (Resolve-Hwnd $cmd),
1326
+ [int](Get-Prop $cmd 'timeoutMs' 3000),
1327
+ [int](Get-Prop $cmd 'rx1' 0),
1328
+ [int](Get-Prop $cmd 'ry1' 0),
1329
+ [int](Get-Prop $cmd 'rx2' 0),
1330
+ [int](Get-Prop $cmd 'ry2' 0)
1331
+ )
1332
+ }
1064
1333
  'ui_click' { $res = [Choco]::RefInvoke([int](Get-Prop $cmd 'ref' 0)) }
1334
+ 'ui_scroll_into_view' { $res = [Choco]::RefScrollIntoView([int](Get-Prop $cmd 'ref' 0)) }
1335
+ 'extract_text' { $res = [Choco]::ExtractWindowText((Resolve-Hwnd $cmd)) }
1065
1336
  'ui_set_value' { $res = [Choco]::RefSetValue([int](Get-Prop $cmd 'ref' 0), [string](Get-Prop $cmd 'text' ''), [bool](Get-Prop $cmd 'replace' $false)) }
1066
1337
  'ui_focus' { $res = [Choco]::RefFocus([int](Get-Prop $cmd 'ref' 0)) }
1067
1338
  'ui_rect' { $res = [Choco]::RefRect([int](Get-Prop $cmd 'ref' 0)) }
@@ -26,6 +26,7 @@
26
26
  export const SLASH_COMMANDS = [
27
27
  { name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
28
28
  { name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
29
+ { name: "/rewind", description: "Volver a como estaba antes de un turno (sobrevive a cerrar la sesión)", args: "[n]" },
29
30
  { name: "/export", description: "Exportar bitácora de la sesión a archivo Markdown", args: "[nombre.md]" },
30
31
  { name: "/plan", description: "Alternar Modo Plan (solo lectura e investigación previa)" },
31
32
  { name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" },
@@ -77,8 +77,28 @@ export function App() {
77
77
  // un proceso hijo lo suelten por el camino. Ver ink/teclado.ts: es el fallo
78
78
  // del "no puedo escribir hasta que pulso Enter".
79
79
  useTecladoSiempreNuestro(turno.esperando);
80
- const cols = Math.max(20, stdout?.columns || 80);
81
- const filas = Math.max(6, stdout?.rows || 24);
80
+ const [tamano, setTamano] = useState(() => ({
81
+ cols: Math.max(20, (stdout?.columns && stdout.columns > 0 ? stdout.columns : process.stdout?.columns) || 80),
82
+ filas: Math.max(6, (stdout?.rows && stdout.rows > 0 ? stdout.rows : process.stdout?.rows) || 24),
83
+ }));
84
+ // Adaptar el árbol y todos los marcos dinámicamente cuando se maximiza o redimensiona la terminal
85
+ useEffect(() => {
86
+ const alRedimensionar = () => {
87
+ setTamano({
88
+ cols: Math.max(20, (stdout?.columns && stdout.columns > 0 ? stdout.columns : process.stdout?.columns) || 80),
89
+ filas: Math.max(6, (stdout?.rows && stdout.rows > 0 ? stdout.rows : process.stdout?.rows) || 24),
90
+ });
91
+ repintar();
92
+ };
93
+ stdout?.on("resize", alRedimensionar);
94
+ process.stdout?.on("resize", alRedimensionar);
95
+ return () => {
96
+ stdout?.off("resize", alRedimensionar);
97
+ process.stdout?.off("resize", alRedimensionar);
98
+ };
99
+ }, [stdout]);
100
+ const cols = tamano.cols;
101
+ const filas = tamano.filas;
82
102
  const permanentes = pendientes();
83
103
  const barra = turno.esperando ? [] : lineasDelPie();
84
104
  // EL TECHO DE LA ZONA DE ABAJO, Y POR QUÉ SE MIDE TAN DE CERCA
@@ -94,7 +114,7 @@ export function App() {
94
114
  const techo = Math.max(1, filas - 1);
95
115
  const sitioParaLaCola = Math.max(0, techo - (turno.esperando ? ALTO_DE_LA_CAJA : barra.length));
96
116
  const cola = recortarCola(colgando(), cols, sitioParaLaCola);
97
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: permanentes, children: (linea) => _jsx(Text, { children: conCuerpo(linea.texto) }, linea.id) }), _jsxs(Box, { flexDirection: "column", children: [cola.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, `cola-${i}`))), turno.esperando ? (_jsx(Prompt, { alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [],
117
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: permanentes, children: (linea) => _jsx(Text, { children: conCuerpo(linea.texto) }, linea.id) }), _jsxs(Box, { flexDirection: "column", children: [cola.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, `cola-${i}`))), turno.esperando ? (_jsx(Prompt, { cols: cols, filas: filas, alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [],
98
118
  // NO SE DESMONTA EL ÁRBOL AL ENVIAR
99
119
  //
100
120
  // El prompt nació montándose y desmontándose en cada vuelta, y para
@@ -56,5 +56,9 @@ export interface PromptProps {
56
56
  * como si estuviera solo.
57
57
  */
58
58
  alto?: number;
59
+ /** Columnas disponibles en el terminal. */
60
+ cols?: number;
61
+ /** Filas totales de la ventana del terminal. */
62
+ filas?: number;
59
63
  }
60
- export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar, alto, }: PromptProps): React.ReactElement;
64
+ export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar, alto, cols: colsProp, filas: filasProp, }: PromptProps): React.ReactElement;