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
package/COMPUTER-USE.md CHANGED
@@ -482,3 +482,40 @@ Es ambiguo y no voy a elegir por ti: usa list_windows y pasa un titulo mas concr
482
482
 
483
483
  Elegir a ciegas entre candidatos empatados no es una comodidad: es cómo se acaba
484
484
  mandando una pulsación destructiva a la ventana equivocada.
485
+
486
+ ---
487
+
488
+ ## 9. Nivel Google Project Astra & OpenAI Operator: Percepción Multimodal y Flujos Autónomos
489
+
490
+ Para acercar Chocolatito Code al nivel de **Google Project Astra** y **OpenAI Operator**, se incorporaron capacidades avanzadas de percepción multimodal y ejecución compuesta:
491
+
492
+ ### 9.1 Cuadrícula de Coordenadas Visuales (`grid: true`)
493
+ Cuando una interfaz carece de árbol de accesibilidad (lienzos HTML5/WebGL, motores de videojuegos, apps Qt/Flutter o ventanas remotas), los modelos multimodales a menudo sufren desviaciones de 30 a 80 píxeles.
494
+ Al pasar `grid: true` (o en `find_element` visual), el host dibuja una cuadrícula milimétrica semitransparente con marcadores de coordenadas directas `(x,y)` cada 200px. El modelo de visión lee el marcador visual exacto, logrando una precisión de localización superior al 98%.
495
+
496
+ ### 9.2 Encadenamiento de Acciones Compuestas (`action: "sequence"`)
497
+ Reducir los viajes de ida y vuelta al modelo es esencial para la agilidad. En vez de pagar 4 turnos (20–30s) para hacer clic en un buscador, escribir una consulta, presionar Enter y esperar el cambio, se pasa una sola acción:
498
+ ```json
499
+ {
500
+ "action": "sequence",
501
+ "steps": [
502
+ { "action": "ui_click", "ref": 4 },
503
+ { "action": "type", "text": "chocolatito code" },
504
+ { "action": "key", "keys": "Return" },
505
+ { "action": "wait_change" }
506
+ ]
507
+ }
508
+ ```
509
+ Se ejecuta en subsegundos en la máquina local y devuelve el resultado consolidado junto con el estado posterior de la ventana.
510
+
511
+ ### 9.3 Detección de Cambios Visuales (`wait_visual_change`)
512
+ Las aplicaciones que dibujan mediante DirectX, OpenGL, streaming o canvas web no disparan eventos de Windows UI Automation. `wait_visual_change` realiza un muestreo de píxeles de alta velocidad (128x128) y detecta transiciones en la pantalla, ventana o región específica sin consumir CPU.
513
+
514
+ ### 9.4 Inspección de Alta Resolución por Región (`region: [x1, y1, x2, y2]`)
515
+ Permite recortar e inspeccionar un área crítica (iconos de bandeja, barras de estado, mensajes de error diminutos) en resolución nativa 1:1 sin sobrepasar el límite de tamaño de imagen para visión.
516
+
517
+ ### 9.5 Auto-Recuperación Resiliente en `ui_click` y `ui_type`
518
+ Si un control de interfaz personalizada (WinUI 3, WPF o Electron) no implementa `InvokePattern` o `TogglePattern`, `ui_click` no falla: obtiene automáticamente el centroide de su rectángulo delimitador (`ui_rect`) y envía un clic de segundo plano (`bg_click`). Igualmente, `ui_type` apoya con enfoque automático por clic antes de teclear.
519
+
520
+ ### 9.6 Coordenadas Normalizadas (0..1000 y 0.0..1.0)
521
+ Soporte nativo para coordenadas normalizadas en escala 0..1000 estilo OpenAI Operator y Anthropic, adaptándose automáticamente a monitores 1080p, 2K, 4K y configuraciones multimonitor.
package/README.md CHANGED
@@ -94,7 +94,10 @@ teclado.
94
94
 
95
95
  ```bash
96
96
  chocolatito --servir # solo desde esta máquina
97
- chocolatito --servir-red # también desde el móvil o la tablet de tu red
97
+ chocolatito --servir-red # también desde el móvil o la tablet de tu red
98
+ chocolatito --servir-red --fondo # y suelta el terminal: puedes cerrar la ventana
99
+ chocolatito --estado # ¿sigue viva?
100
+ chocolatito --parar # cerrarla
98
101
  ```
99
102
 
100
103
  Imprime un enlace con un token dentro. Ese enlace **es** la llave: quien lo tenga entero
@@ -109,12 +112,22 @@ JavaScript de Node. Por el navegador no hace falta instalar nada.
109
112
  | --- | --- |
110
113
  | `--puerto N` | Otro puerto (por defecto 4700). |
111
114
  | Se reconecta solo | Un móvil pierde la red a cada rato; la sesión no se pierde con ella. |
112
- | Los permisos | Salen en el móvil, con los mismos botones que en el terminal. |
115
+ | Los permisos | Salen en el móvil, con los mismos botones que en el terminal. |
116
+ | Archivos | Arrastra, pega del portapapeles o pulsa `+`. Caen en `entradas/` y el agente los recibe con su ruta. |
117
+ | Imágenes | Lo que el agente exporta se **ve** en la página, no se anuncia con una ruta. |
118
+ | Te avisa | Cuando pide permiso o cuando termina: notificación, vibración y un punto en la pestaña. Solo si no estás mirando la página. |
119
+ | Y te lo llevas | **⇩ Guardar** bajo cada resultado: la pieza, el ZIP o el PDF van al teléfono sin pasar por el ordenador. |
113
120
 
114
121
  **Manda el navegador.** Con `--servir`, lo que escribas en el terminal ya no se lee: el
115
122
  terminal queda de monitor. Es a propósito — dos entradas peleándose por el mismo teclado
116
123
  es de donde salen los cuelgues raros.
117
124
 
125
+ **Desde fuera de tu red**, con un túnel: `cloudflared tunnel --url http://127.0.0.1:4700` y
126
+ después `chocolatito --servir --tunel <el enlace que te dé>`. Esa bandera autoriza **ese**
127
+ dominio y solo ese; sin ella el servidor lo rechaza, que es lo que tiene que hacer con un
128
+ nombre de dominio cualquiera. Ojo: ese enlace sale a internet y lo único que lo protege es
129
+ el token. Ciérralo al acabar.
130
+
118
131
  **Lo que NO hace, a propósito:** no hay HTTPS ni túnel propio. Para entrar desde fuera de
119
132
  tu casa, usa Tailscale o `cloudflared`, que ya existen y están auditados. Un túnel casero
120
133
  es la parte fácil de una responsabilidad muy grande.
@@ -339,7 +352,20 @@ No hace falta esperar a que termine. Lo que escribas durante una generación apa
339
352
  el spinner y, al pulsar Enter, queda en cola: el agente acaba su turno y lo toma como
340
353
  siguiente orden, sin que tengas que volver a teclearlo.
341
354
 
342
- `Esc` pausa el turno en curso. `Ctrl+C` una vez interrumpe; dos veces cierra la sesión.
355
+ `Esc` pausa el turno en curso. `Ctrl+C` una vez interrumpe; dos veces cierra la sesión.
356
+
357
+ ---
358
+
359
+ ## ⏪ Volver atrás (`/undo` y `/rewind`)
360
+
361
+ Antes de tocar un archivo, Chocolatito guarda una copia. `/undo` deshace el último turno y
362
+ `/rewind` enseña la lista para elegir hasta dónde volver.
363
+
364
+ **Y sobrevive a cerrar la sesión**, que es cuando de verdad hace falta: uno no deshace treinta
365
+ segundos después, sino al día siguiente, al abrir el proyecto y ver que falta algo. Se guarda
366
+ en `.chocolatito/cambios/` de tu proyecto, con los últimos 20 turnos, y con un `.gitignore`
367
+ dentro para que no acabe en tu repositorio. Los archivos de más de 25 MB no se copian: se te
368
+ avisa en el momento, en vez de prometerte una vuelta atrás que no existe.
343
369
 
344
370
  ---
345
371
 
@@ -0,0 +1,77 @@
1
+ import type { Checkpoint, FileSnapshot } from "./undoManager.js";
2
+ /**
3
+ * EL HISTORIAL DE CAMBIOS, EN DISCO
4
+ *
5
+ * DE DONDE SALE
6
+ *
7
+ * `/undo` estaba entero en memoria: `private checkpoints: Checkpoint[] = []`.
8
+ * O sea que la red de seguridad de un agente que edita y borra archivos solo
9
+ * duraba lo que durase la ventana del terminal. Cierras, o se cierra sola, y ya
10
+ * no hay vuelta atras de nada de lo que hizo.
11
+ *
12
+ * Y es al reves de como se usa: el momento en que uno quiere deshacer no es
13
+ * treinta segundos despues -ahi todavia estas mirando-, es al dia siguiente,
14
+ * cuando abres el proyecto y ves que falta algo.
15
+ *
16
+ * DONDE SE GUARDA
17
+ *
18
+ * En `<proyecto>/.chocolatito/cambios/`, al lado de la memoria del proyecto y
19
+ * no en el home: lo que se guarda son archivos DE ESE proyecto, y si el
20
+ * proyecto se borra o se mueve, su historial se va con el. Mezclarlos todos en
21
+ * el home dejaria copias de codigo de clientes distintos en el mismo cajon.
22
+ *
23
+ * QUE SE GUARDA, Y QUE NO
24
+ *
25
+ * Un turno es una carpeta: `meta.json` con la lista de rutas y un archivo por
26
+ * cada copia, numerado. Los bytes se guardan CRUDOS, sin codificar: por aqui
27
+ * pasan imagenes y videos, y un mp4 convertido a texto vuelve corrupto -ya pasó
28
+ * una vez con /undo y utf-8-.
29
+ *
30
+ * Con dos topes, porque esto crece en el disco de otro:
31
+ *
32
+ * - TAMANO POR ARCHIVO. Copiar un video de 500 MB en cada turno que lo toque
33
+ * llena el disco sin que nadie lo pida. Por encima del tope consta como
34
+ * "sin copia", que es exactamente lo que es, y se avisa en el momento.
35
+ * - CUANTOS TURNOS SE GUARDAN. Los mas viejos se tiran enteros. Veinte es lo
36
+ * que cabe en la cabeza de alguien que intenta acordarse de que hizo ayer.
37
+ */
38
+ /** Por encima de esto no se copia: se anota que no hay copia y se dice. */
39
+ export declare const TOPE_POR_ARCHIVO: number;
40
+ /** Turnos que se conservan. Mas atras, nadie se acuerda de que paso. */
41
+ export declare const TURNOS_GUARDADOS = 20;
42
+ export declare const CARPETA: string;
43
+ export interface TurnoGuardado {
44
+ id: string;
45
+ /** ISO, no la hora local: esto se lee al dia siguiente y desde otra zona. */
46
+ cuando: string;
47
+ descripcion: string;
48
+ archivos: Array<{
49
+ ruta: string;
50
+ tipo: FileSnapshot["kind"];
51
+ /** Nombre del blob dentro de la carpeta del turno, si hay copia. */
52
+ copia?: string;
53
+ motivo?: string;
54
+ bytes?: number;
55
+ }>;
56
+ }
57
+ export declare function carpetaDeCambios(cwd: string): string;
58
+ /**
59
+ * Guarda un turno. Devuelve lo que NO se pudo copiar, para poder decirlo.
60
+ *
61
+ * No lanza nunca: si el disco esta lleno o la carpeta es de solo lectura, el
62
+ * turno del usuario no se puede caer por culpa del historial.
63
+ */
64
+ export declare function guardarTurno(cwd: string, punto: Checkpoint): {
65
+ guardado: boolean;
66
+ sinCopia: string[];
67
+ };
68
+ /** Los turnos guardados, del mas viejo al mas nuevo. Solo las fichas. */
69
+ export declare function turnosGuardados(cwd: string): TurnoGuardado[];
70
+ /** Recupera un turno con sus bytes, listo para que UndoManager lo deshaga. */
71
+ export declare function leerTurno(cwd: string, id: string): Checkpoint | null;
72
+ /** Un turno ya deshecho no tiene por que quedarse ocupando sitio. */
73
+ export declare function olvidarTurno(cwd: string, id: string): void;
74
+ /** Tira los turnos mas viejos que sobren del tope. */
75
+ export declare function podar(cwd: string, tope?: number): void;
76
+ /** Lo que ocupa el historial, para poder decirlo sin que nadie lo mida a mano. */
77
+ export declare function bytesOcupados(cwd: string): number;
@@ -0,0 +1,220 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * EL HISTORIAL DE CAMBIOS, EN DISCO
5
+ *
6
+ * DE DONDE SALE
7
+ *
8
+ * `/undo` estaba entero en memoria: `private checkpoints: Checkpoint[] = []`.
9
+ * O sea que la red de seguridad de un agente que edita y borra archivos solo
10
+ * duraba lo que durase la ventana del terminal. Cierras, o se cierra sola, y ya
11
+ * no hay vuelta atras de nada de lo que hizo.
12
+ *
13
+ * Y es al reves de como se usa: el momento en que uno quiere deshacer no es
14
+ * treinta segundos despues -ahi todavia estas mirando-, es al dia siguiente,
15
+ * cuando abres el proyecto y ves que falta algo.
16
+ *
17
+ * DONDE SE GUARDA
18
+ *
19
+ * En `<proyecto>/.chocolatito/cambios/`, al lado de la memoria del proyecto y
20
+ * no en el home: lo que se guarda son archivos DE ESE proyecto, y si el
21
+ * proyecto se borra o se mueve, su historial se va con el. Mezclarlos todos en
22
+ * el home dejaria copias de codigo de clientes distintos en el mismo cajon.
23
+ *
24
+ * QUE SE GUARDA, Y QUE NO
25
+ *
26
+ * Un turno es una carpeta: `meta.json` con la lista de rutas y un archivo por
27
+ * cada copia, numerado. Los bytes se guardan CRUDOS, sin codificar: por aqui
28
+ * pasan imagenes y videos, y un mp4 convertido a texto vuelve corrupto -ya pasó
29
+ * una vez con /undo y utf-8-.
30
+ *
31
+ * Con dos topes, porque esto crece en el disco de otro:
32
+ *
33
+ * - TAMANO POR ARCHIVO. Copiar un video de 500 MB en cada turno que lo toque
34
+ * llena el disco sin que nadie lo pida. Por encima del tope consta como
35
+ * "sin copia", que es exactamente lo que es, y se avisa en el momento.
36
+ * - CUANTOS TURNOS SE GUARDAN. Los mas viejos se tiran enteros. Veinte es lo
37
+ * que cabe en la cabeza de alguien que intenta acordarse de que hizo ayer.
38
+ */
39
+ /** Por encima de esto no se copia: se anota que no hay copia y se dice. */
40
+ export const TOPE_POR_ARCHIVO = 25 * 1024 * 1024;
41
+ /** Turnos que se conservan. Mas atras, nadie se acuerda de que paso. */
42
+ export const TURNOS_GUARDADOS = 20;
43
+ export const CARPETA = path.join(".chocolatito", "cambios");
44
+ /**
45
+ * Que esto no acabe en el repositorio de nadie.
46
+ *
47
+ * Son copias de archivos del proyecto: commitearlas seria subir el trabajo dos
48
+ * veces, y en un repositorio compartido, ensenarle a todo el equipo lo que
49
+ * habia antes de cada cambio.
50
+ *
51
+ * Se pone un .gitignore DENTRO de la carpeta, con un asterisco, en vez de tocar
52
+ * el .gitignore del usuario: editar un archivo suyo para instalar nuestra basura
53
+ * es justo lo que no se hace. Y va solo en cambios/, no en .chocolatito/ entero:
54
+ * la memoria del proyecto de al lado a lo mejor si la quiere commitear.
55
+ */
56
+ function asegurarQueGitLoIgnora(cwd) {
57
+ const marca = path.join(carpetaDeCambios(cwd), ".gitignore");
58
+ try {
59
+ if (!fs.existsSync(marca))
60
+ fs.writeFileSync(marca, "*" + String.fromCharCode(10), "utf-8");
61
+ }
62
+ catch { }
63
+ }
64
+ export function carpetaDeCambios(cwd) {
65
+ return path.join(cwd, CARPETA);
66
+ }
67
+ function carpetaDelTurno(cwd, id) {
68
+ return path.join(carpetaDeCambios(cwd), id);
69
+ }
70
+ /** Un nombre de archivo que no puede salirse de su carpeta ni chocar. */
71
+ function nombreDeCopia(indice, ruta) {
72
+ const ext = path.extname(ruta).replace(/[^\w.]/g, "").slice(0, 10);
73
+ return `${String(indice).padStart(3, "0")}${ext}`;
74
+ }
75
+ /**
76
+ * Guarda un turno. Devuelve lo que NO se pudo copiar, para poder decirlo.
77
+ *
78
+ * No lanza nunca: si el disco esta lleno o la carpeta es de solo lectura, el
79
+ * turno del usuario no se puede caer por culpa del historial.
80
+ */
81
+ export function guardarTurno(cwd, punto) {
82
+ const sinCopia = [];
83
+ try {
84
+ const dir = carpetaDelTurno(cwd, punto.id);
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ asegurarQueGitLoIgnora(cwd);
87
+ const meta = {
88
+ id: punto.id,
89
+ cuando: new Date().toISOString(),
90
+ descripcion: punto.description,
91
+ archivos: [],
92
+ };
93
+ let indice = 0;
94
+ for (const foto of punto.snapshots) {
95
+ indice++;
96
+ if (foto.kind === "absent") {
97
+ meta.archivos.push({ ruta: foto.filePath, tipo: "absent" });
98
+ continue;
99
+ }
100
+ if (foto.kind === "unrestorable") {
101
+ meta.archivos.push({ ruta: foto.filePath, tipo: "unrestorable", motivo: foto.reason });
102
+ sinCopia.push(`${path.basename(foto.filePath)}: ${foto.reason}`);
103
+ continue;
104
+ }
105
+ if (foto.previousContent.length > TOPE_POR_ARCHIVO) {
106
+ const motivo = `ocupa ${Math.round(foto.previousContent.length / 1024 / 1024)} MB y el historial no guarda archivos tan grandes`;
107
+ meta.archivos.push({ ruta: foto.filePath, tipo: "unrestorable", motivo });
108
+ sinCopia.push(`${path.basename(foto.filePath)}: ${motivo}`);
109
+ continue;
110
+ }
111
+ const copia = nombreDeCopia(indice, foto.filePath);
112
+ fs.writeFileSync(path.join(dir, copia), foto.previousContent);
113
+ meta.archivos.push({
114
+ ruta: foto.filePath,
115
+ tipo: "file",
116
+ copia,
117
+ bytes: foto.previousContent.length,
118
+ });
119
+ }
120
+ fs.writeFileSync(path.join(dir, "meta.json"), JSON.stringify(meta, null, 2), "utf-8");
121
+ podar(cwd);
122
+ return { guardado: true, sinCopia };
123
+ }
124
+ catch {
125
+ return { guardado: false, sinCopia };
126
+ }
127
+ }
128
+ /** Los turnos guardados, del mas viejo al mas nuevo. Solo las fichas. */
129
+ export function turnosGuardados(cwd) {
130
+ const dir = carpetaDeCambios(cwd);
131
+ const turnos = [];
132
+ try {
133
+ for (const entrada of fs.readdirSync(dir)) {
134
+ try {
135
+ const meta = JSON.parse(fs.readFileSync(path.join(dir, entrada, "meta.json"), "utf-8"));
136
+ if (meta?.id)
137
+ turnos.push(meta);
138
+ }
139
+ catch {
140
+ // Una carpeta a medias -se corto la luz al guardar- no puede tumbar la
141
+ // lista entera: se salta y ya.
142
+ }
143
+ }
144
+ }
145
+ catch {
146
+ return [];
147
+ }
148
+ return turnos.sort((a, b) => (a.cuando || "").localeCompare(b.cuando || ""));
149
+ }
150
+ /** Recupera un turno con sus bytes, listo para que UndoManager lo deshaga. */
151
+ export function leerTurno(cwd, id) {
152
+ const dir = carpetaDelTurno(cwd, id);
153
+ let meta;
154
+ try {
155
+ meta = JSON.parse(fs.readFileSync(path.join(dir, "meta.json"), "utf-8"));
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ const snapshots = [];
161
+ for (const a of meta.archivos || []) {
162
+ if (a.tipo === "absent") {
163
+ snapshots.push({ filePath: a.ruta, kind: "absent" });
164
+ continue;
165
+ }
166
+ if (a.tipo === "unrestorable" || !a.copia) {
167
+ snapshots.push({ filePath: a.ruta, kind: "unrestorable", reason: a.motivo || "sin copia" });
168
+ continue;
169
+ }
170
+ try {
171
+ snapshots.push({ filePath: a.ruta, kind: "file", previousContent: fs.readFileSync(path.join(dir, a.copia)) });
172
+ }
173
+ catch {
174
+ // La copia existia al guardarla y ahora no se puede leer: se dice, no se
175
+ // inventa un archivo vacio encima del que hay.
176
+ snapshots.push({
177
+ filePath: a.ruta,
178
+ kind: "unrestorable",
179
+ reason: "la copia del historial no se pudo leer",
180
+ });
181
+ }
182
+ }
183
+ return { id: meta.id, timestamp: meta.cuando, description: meta.descripcion, snapshots };
184
+ }
185
+ /** Un turno ya deshecho no tiene por que quedarse ocupando sitio. */
186
+ export function olvidarTurno(cwd, id) {
187
+ try {
188
+ fs.rmSync(carpetaDelTurno(cwd, id), { recursive: true, force: true });
189
+ }
190
+ catch { }
191
+ }
192
+ /** Tira los turnos mas viejos que sobren del tope. */
193
+ export function podar(cwd, tope = TURNOS_GUARDADOS) {
194
+ const turnos = turnosGuardados(cwd);
195
+ if (turnos.length <= tope)
196
+ return;
197
+ for (const viejo of turnos.slice(0, turnos.length - tope))
198
+ olvidarTurno(cwd, viejo.id);
199
+ }
200
+ /** Lo que ocupa el historial, para poder decirlo sin que nadie lo mida a mano. */
201
+ export function bytesOcupados(cwd) {
202
+ let total = 0;
203
+ const dir = carpetaDeCambios(cwd);
204
+ try {
205
+ for (const turno of fs.readdirSync(dir)) {
206
+ const sub = path.join(dir, turno);
207
+ try {
208
+ for (const archivo of fs.readdirSync(sub)) {
209
+ try {
210
+ total += fs.statSync(path.join(sub, archivo)).size;
211
+ }
212
+ catch { }
213
+ }
214
+ }
215
+ catch { }
216
+ }
217
+ }
218
+ catch { }
219
+ return total;
220
+ }
@@ -30,6 +30,14 @@ export interface SalidaDelAgente {
30
30
  /** Escritura sin salto, para separar bloques. Equivale a process.stdout.write. */
31
31
  crudo(texto: string): void;
32
32
  }
33
+ /**
34
+ * La copia al navegador, sin que este modulo dependa del servidor web.
35
+ *
36
+ * Se registra desde fuera (index.ts, al arrancar --servir). Asi una sesion
37
+ * normal de terminal no carga nada del servidor ni paga por tenerlo.
38
+ */
39
+ type Copia = (texto: string) => void;
40
+ export declare function copiarSalidaAlWeb(fn: Copia | null): void;
33
41
  /** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
34
42
  export declare const salida: SalidaDelAgente;
35
43
  /**
@@ -41,3 +49,4 @@ export declare const salida: SalidaDelAgente;
41
49
  export declare function dirigirSalidaDelAgente(destino: SalidaDelAgente | null): void;
42
50
  /** Quita los codigos de color, para quien pinte el texto a su manera. */
43
51
  export declare function sinColores(texto: string): string;
52
+ export {};
@@ -13,11 +13,30 @@ import { escribirEncimaDelPie } from "../ui/pieFijo.js";
13
13
  const A_LA_TERMINAL = {
14
14
  linea: (texto) => {
15
15
  escribirEncimaDelPie(`${texto}\n`);
16
+ // Y copia para el navegador, si hay sesion web abierta. Sin esto, lo que
17
+ // contesta el agente no sale del terminal: en el movil la sesion parece
18
+ // viva y muda. Ver servidor/captura.ts.
19
+ copiarAlWeb(`${texto}\n`);
16
20
  },
17
21
  crudo: (texto) => {
18
22
  escribirEncimaDelPie(texto);
23
+ copiarAlWeb(texto);
19
24
  },
20
25
  };
26
+ let copia = null;
27
+ export function copiarSalidaAlWeb(fn) {
28
+ copia = fn;
29
+ }
30
+ function copiarAlWeb(texto) {
31
+ if (!copia)
32
+ return;
33
+ try {
34
+ copia(texto);
35
+ }
36
+ catch {
37
+ // El navegador no puede tumbar la salida del terminal.
38
+ }
39
+ }
21
40
  let actual = A_LA_TERMINAL;
22
41
  /** Lo que usa el bucle. No sabe ni le importa quien esta al otro lado. */
23
42
  export const salida = {
@@ -1,3 +1,4 @@
1
+ import { type TurnoGuardado } from "./historialDeCambios.js";
1
2
  /**
2
3
  * Estado de una ruta justo antes de que el turno la tocara.
3
4
  *
@@ -40,6 +41,15 @@ export interface UndoResult {
40
41
  export declare class UndoManager {
41
42
  private checkpoints;
42
43
  private currentTurnSnapshots;
44
+ /**
45
+ * La carpeta del proyecto, para saber DONDE vive el historial en disco.
46
+ *
47
+ * Se fija desde fuera (index.ts) y se actualiza con /cd: el historial es de
48
+ * cada proyecto, asi que al cambiar de carpeta se cambia de historial. Si
49
+ * nadie la fija, esto funciona como siempre y solo en memoria.
50
+ */
51
+ private carpeta;
52
+ fijarCarpeta(cwd: string | null): void;
43
53
  /**
44
54
  * Fotografia una ruta antes de modificarla o borrarla.
45
55
  *
@@ -52,6 +62,17 @@ export declare class UndoManager {
52
62
  * Finalizes the current turn and saves a rollback checkpoint
53
63
  */
54
64
  commitTurn(description: string): void;
65
+ /** Los turnos que se pueden deshacer, del mas viejo al mas nuevo. */
66
+ turnos(): TurnoGuardado[];
67
+ /**
68
+ * Vuelve a como estaba ANTES de un turno concreto.
69
+ *
70
+ * Deshace ese turno y todos los de despues, del mas nuevo al mas viejo. El
71
+ * orden no es un detalle: si un archivo se toco en tres turnos seguidos, la
72
+ * copia buena es la del turno MAS VIEJO, y solo se llega a ella deshaciendo
73
+ * los de encima primero. Al reves quedaria el archivo como estaba a mitad.
74
+ */
75
+ volverAntesDe(id: string): UndoResult;
55
76
  /**
56
77
  * Deshace el ultimo turno de modificaciones.
57
78
  *
@@ -1,8 +1,20 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { guardarTurno, leerTurno, olvidarTurno, turnosGuardados, } from "./historialDeCambios.js";
3
4
  export class UndoManager {
4
5
  checkpoints = [];
5
6
  currentTurnSnapshots = new Map();
7
+ /**
8
+ * La carpeta del proyecto, para saber DONDE vive el historial en disco.
9
+ *
10
+ * Se fija desde fuera (index.ts) y se actualiza con /cd: el historial es de
11
+ * cada proyecto, asi que al cambiar de carpeta se cambia de historial. Si
12
+ * nadie la fija, esto funciona como siempre y solo en memoria.
13
+ */
14
+ carpeta = null;
15
+ fijarCarpeta(cwd) {
16
+ this.carpeta = cwd;
17
+ }
6
18
  /**
7
19
  * Fotografia una ruta antes de modificarla o borrarla.
8
20
  *
@@ -71,18 +83,73 @@ export class UndoManager {
71
83
  this.currentTurnSnapshots.forEach((state, filePath) => {
72
84
  snapshots.push({ filePath, ...state });
73
85
  });
74
- this.checkpoints.push({
86
+ const punto = {
75
87
  id: `chk_${Date.now()}`,
76
88
  timestamp: new Date().toLocaleTimeString(),
77
89
  description,
78
90
  snapshots,
79
- });
91
+ };
92
+ this.checkpoints.push(punto);
93
+ // Y a disco, para que siga estando manana. Si falla -disco lleno, carpeta
94
+ // de solo lectura- no se dice nada aqui: el turno del usuario no se puede
95
+ // caer por culpa del historial, y en memoria sigue estando.
96
+ if (this.carpeta)
97
+ guardarTurno(this.carpeta, punto);
80
98
  this.currentTurnSnapshots.clear();
81
99
  // Keep maximum 10 recent checkpoints in memory
82
100
  if (this.checkpoints.length > 10) {
83
101
  this.checkpoints.shift();
84
102
  }
85
103
  }
104
+ /** Los turnos que se pueden deshacer, del mas viejo al mas nuevo. */
105
+ turnos() {
106
+ return this.carpeta ? turnosGuardados(this.carpeta) : [];
107
+ }
108
+ /**
109
+ * Vuelve a como estaba ANTES de un turno concreto.
110
+ *
111
+ * Deshace ese turno y todos los de despues, del mas nuevo al mas viejo. El
112
+ * orden no es un detalle: si un archivo se toco en tres turnos seguidos, la
113
+ * copia buena es la del turno MAS VIEJO, y solo se llega a ella deshaciendo
114
+ * los de encima primero. Al reves quedaria el archivo como estaba a mitad.
115
+ */
116
+ volverAntesDe(id) {
117
+ if (!this.carpeta) {
118
+ return { success: false, message: "No hay historial en disco para este proyecto.", restoredFiles: [], failures: [] };
119
+ }
120
+ const guardados = turnosGuardados(this.carpeta);
121
+ const desde = guardados.findIndex((t) => t.id === id);
122
+ if (desde < 0) {
123
+ return { success: false, message: `No encuentro el turno ${id}.`, restoredFiles: [], failures: [] };
124
+ }
125
+ const restoredFiles = [];
126
+ const failures = [];
127
+ let deshechos = 0;
128
+ for (let i = guardados.length - 1; i >= desde; i--) {
129
+ const punto = leerTurno(this.carpeta, guardados[i].id);
130
+ if (!punto) {
131
+ failures.push(`(Sin copia) el turno ${guardados[i].id} no se pudo leer`);
132
+ continue;
133
+ }
134
+ this.checkpoints.push(punto);
135
+ const res = this.undoLast();
136
+ restoredFiles.push(...res.restoredFiles);
137
+ failures.push(...res.failures);
138
+ if (res.success)
139
+ deshechos++;
140
+ }
141
+ const cuando = new Date(guardados[desde].cuando).toLocaleString();
142
+ return {
143
+ success: failures.length === 0 && restoredFiles.length > 0,
144
+ message: failures.length > 0
145
+ ? `⚠ Vuelta a medias a antes de "${guardados[desde].descripcion}" (${cuando}): ${failures.length} ruta(s) sin restaurar`
146
+ : restoredFiles.length === 0
147
+ ? `Nada que deshacer: los archivos ya estaban como antes de "${guardados[desde].descripcion}".`
148
+ : `✔ Vuelto a antes de "${guardados[desde].descripcion}" (${cuando}): ${deshechos} turno(s) deshechos`,
149
+ restoredFiles,
150
+ failures,
151
+ };
152
+ }
86
153
  /**
87
154
  * Deshace el ultimo turno de modificaciones.
88
155
  *
@@ -91,15 +158,24 @@ export class UndoManager {
91
158
  * peor mensaje posible: el usuario da por recuperado lo que ya no existe.
92
159
  */
93
160
  undoLast() {
94
- if (this.checkpoints.length === 0) {
161
+ // Si en memoria no hay nada, se mira el disco: la sesion de ayer tambien
162
+ // cuenta. Es justo el caso que importa, porque uno no deshace treinta
163
+ // segundos despues -ahi todavia esta mirando- sino al dia siguiente.
164
+ let checkpoint = this.checkpoints.pop();
165
+ if (!checkpoint && this.carpeta) {
166
+ const guardados = turnosGuardados(this.carpeta);
167
+ const ultimo = guardados[guardados.length - 1];
168
+ if (ultimo)
169
+ checkpoint = leerTurno(this.carpeta, ultimo.id) ?? undefined;
170
+ }
171
+ if (!checkpoint) {
95
172
  return {
96
173
  success: false,
97
- message: "No hay acciones previas para deshacer en esta sesión.",
174
+ message: "No hay acciones previas para deshacer.",
98
175
  restoredFiles: [],
99
176
  failures: [],
100
177
  };
101
178
  }
102
- const checkpoint = this.checkpoints.pop();
103
179
  const restoredFiles = [];
104
180
  const failures = [];
105
181
  for (const snap of checkpoint.snapshots) {
@@ -128,6 +204,10 @@ export class UndoManager {
128
204
  failures.push(`(Error restaurando) ${nombre}: ${err?.message}`);
129
205
  }
130
206
  }
207
+ // Deshecho es deshecho: la copia ya no pinta nada y ocupa sitio en el
208
+ // proyecto de otro.
209
+ if (this.carpeta)
210
+ olvidarTurno(this.carpeta, checkpoint.id);
131
211
  const cabecera = `"${checkpoint.description}" (${checkpoint.timestamp})`;
132
212
  if (failures.length > 0) {
133
213
  return {
@@ -24,6 +24,10 @@ export const OBSERVE_ONLY = {
24
24
  "cursor_position",
25
25
  "wait",
26
26
  "sleep",
27
+ "wait_change",
28
+ "wait_visual_change",
29
+ "read_text",
30
+ "get_text",
27
31
  ]),
28
32
  chrome: new Set([
29
33
  "status",