chocolatito-code 1.6.6 → 1.6.8

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 (76) hide show
  1. package/README.md +437 -341
  2. package/dist/agent/context.d.ts +6 -0
  3. package/dist/agent/context.js +69 -2
  4. package/dist/agent/loop.js +8 -0
  5. package/dist/agent/toolGate.js +13 -7
  6. package/dist/agent/verifier.js +2 -1
  7. package/dist/config/permissions.d.ts +17 -0
  8. package/dist/config/permissions.js +47 -7
  9. package/dist/config/plataforma.d.ts +62 -0
  10. package/dist/config/plataforma.js +109 -0
  11. package/dist/config/updater.js +4 -1
  12. package/dist/hooks/manager.js +2 -1
  13. package/dist/index.js +164 -39
  14. package/dist/memory/manager.d.ts +15 -5
  15. package/dist/memory/manager.js +146 -29
  16. package/dist/servidor/captura.d.ts +36 -0
  17. package/dist/servidor/captura.js +74 -0
  18. package/dist/servidor/pagina.d.ts +27 -0
  19. package/dist/servidor/pagina.js +268 -0
  20. package/dist/servidor/puente.d.ts +34 -0
  21. package/dist/servidor/puente.js +115 -0
  22. package/dist/servidor/servidor.d.ts +61 -0
  23. package/dist/servidor/servidor.js +249 -0
  24. package/dist/sessions/manager.d.ts +8 -0
  25. package/dist/sessions/manager.js +44 -0
  26. package/dist/sessions/resume.d.ts +12 -0
  27. package/dist/sessions/resume.js +10 -0
  28. package/dist/tools/backgroundTask.d.ts +64 -0
  29. package/dist/tools/backgroundTask.js +264 -0
  30. package/dist/tools/browserExtension.d.ts +2 -1
  31. package/dist/tools/browserExtension.js +48 -0
  32. package/dist/tools/computerUse.js +51 -9
  33. package/dist/tools/definitions.js +93 -1
  34. package/dist/tools/gitAudit.d.ts +41 -0
  35. package/dist/tools/gitAudit.js +282 -0
  36. package/dist/tools/runCommand.js +5 -2
  37. package/dist/tools/runner.js +36 -2
  38. package/dist/tools/safety.d.ts +1 -0
  39. package/dist/tools/safety.js +3 -0
  40. package/dist/tools/todoTool.d.ts +2 -0
  41. package/dist/tools/todoTool.js +29 -0
  42. package/dist/tools/toolDefsComputer.js +9 -0
  43. package/dist/tools/win/hostScript.js +7 -2
  44. package/dist/ui/comandos.js +2 -0
  45. package/dist/ui/ink/App.d.ts +0 -23
  46. package/dist/ui/ink/App.js +86 -57
  47. package/dist/ui/ink/Prompt.d.ts +16 -6
  48. package/dist/ui/ink/Prompt.js +37 -62
  49. package/dist/ui/ink/montarApp.d.ts +34 -0
  50. package/dist/ui/ink/montarApp.js +93 -1
  51. package/dist/ui/ink/prestamo.d.ts +63 -0
  52. package/dist/ui/ink/prestamo.js +92 -0
  53. package/dist/ui/ink/teclado.d.ts +48 -0
  54. package/dist/ui/ink/teclado.js +96 -0
  55. package/dist/ui/ink/transcripcion.d.ts +114 -0
  56. package/dist/ui/ink/transcripcion.js +180 -0
  57. package/dist/ui/interrupt.js +4 -1
  58. package/dist/ui/loopPrompt.d.ts +6 -7
  59. package/dist/ui/loopPrompt.js +14 -1
  60. package/dist/ui/marco.d.ts +3 -0
  61. package/dist/ui/marco.js +22 -3
  62. package/dist/ui/pantalla.d.ts +45 -17
  63. package/dist/ui/pantalla.js +150 -92
  64. package/dist/ui/permissionPrompt.d.ts +19 -17
  65. package/dist/ui/permissionPrompt.js +44 -1
  66. package/dist/ui/pieFijo.d.ts +1 -1
  67. package/dist/ui/pieFijo.js +27 -5
  68. package/dist/ui/renderer.js +8 -0
  69. package/dist/ui/selector.d.ts +6 -4
  70. package/dist/ui/selector.js +14 -1
  71. package/extension/background.js +80 -0
  72. package/extension/content.js +106 -0
  73. package/extension/manifest.json +3 -2
  74. package/package.json +3 -2
  75. package/dist/ui/historial.d.ts +0 -49
  76. package/dist/ui/historial.js +0 -92
@@ -1,139 +1,197 @@
1
1
  import process from "node:process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
2
+ import { format } from "node:util";
5
3
  import { redirigirDibujo } from "./salida.js";
6
- import { apuntar } from "./historial.js";
4
+ import { anotar } from "./ink/transcripcion.js";
7
5
  import { habilitarPie } from "./pieFijo.js";
6
+ import { limpiarLaPantalla } from "./ink/montarApp.js";
8
7
  /**
9
- * LA PANTALLA ALTERNATIVA
8
+ * QUIÉN ES DUEÑO DE LA PANTALLA
10
9
  *
11
- * La barra de abajo sube al hacer scroll porque ES una línea del historial del
12
- * terminal, no una capa encima. Ninguna secuencia de escape la clava ahí; lo
13
- * único que lo consigue es que el programa tome la pantalla entera, como hacen
14
- * vim o htop.
10
+ * LO QUE HABÍA AQUÍ ANTES, Y POR QUÉ SE FUE
15
11
  *
16
- * QUÉ CAMBIA AL ENTRAR
12
+ * Este archivo tomaba la pantalla alternativa —la de vim o htop— para clavar la
13
+ * barra de abajo. Es la única forma de clavarla de verdad, y se pidió cinco
14
+ * veces, así que se hizo. Se publicó rota dos veces: "no sale nada, se bugea" y
15
+ * "sale una barra y lo demás negro, escribo y se cierra".
17
16
  *
18
- * El terminal deja de guardar historial: la rueda del ratón no sirve y al salir
19
- * la sesión desaparece de la consola. Por eso, mientras dura, TODO lo que se
20
- * imprime se guarda en memoria (historial.ts) y lo pinta el árbol de Ink.
17
+ * Y resultó que además era la arquitectura equivocada. Claude Code, que era el
18
+ * ejemplo a copiar, NO usa la pantalla alternativa: renderiza en el buffer
19
+ * normal y emite la conversación con `<Static>` de Ink, que la escribe una vez y
20
+ * la deja en el historial del terminal. Por eso allí la rueda del ratón funciona.
21
+ * Y por eso allí la barra tampoco se queda quieta si subes: se va con el resto.
21
22
  *
22
- * Y por eso hay que interceptar también `console.log`. Aquí no vale con desviar
23
- * el conducto de dibujo: media docena de sitios —el arranque, la licencia, los
24
- * avisos— siguen imprimiendo directo, y en la pantalla alternativa eso escribe
25
- * encima de lo que Ink acaba de dibujar y descuadra el árbol entero.
23
+ * QUÉ HACE AHORA
26
24
  *
27
- * EL INTERCAMBIO
25
+ * Enciende el modo de árbol único: un solo componente de Ink vivo toda la
26
+ * sesión, en la pantalla de siempre. Lo que cambia respecto al modo clásico:
28
27
  *
29
- * Cambiar el historial del terminal por uno propio no es gratis: quien use la
30
- * rueda del ratón para releer sale perdiendo, y a cambio se recorre con Re Pág
31
- * y Av Pág. Se pidió así cinco veces, así que va de serie;
32
- * CHOCOLATITO_PANTALLA=normal devuelve lo de antes.
28
+ * - El dibujo del agente deja de ir al terminal y entra en la transcripción,
29
+ * que es lo que `<Static>` va escribiendo (ink/transcripcion.ts).
30
+ * - `pieFijo` deja de pintar. Sigue guardando el spinner y la línea de
31
+ * razonamiento, pero quien los dibuja pasa a ser el árbol. Dos dueños del
32
+ * mismo trozo de pantalla es el problema que esto viene a quitar.
33
+ *
34
+ * Y SE SIGUE INTERCEPTANDO `console`, PERO POR OTRO MOTIVO
35
+ *
36
+ * Ink trae su propio `patchConsole`, y hace lo correcto: borra su marco, escribe
37
+ * el mensaje y repinta debajo. Aquí no se usa, y no es por desconfianza: es por
38
+ * el ORDEN.
39
+ *
40
+ * Lo que escribe el agente entra en la transcripción y sale cuando React confirma
41
+ * el dibujado. Un `console.log` con el parche de Ink sale en el acto. Dos caminos
42
+ * con tiempos distintos para el mismo hilo de conversación, y entonces el aviso
43
+ * de un hook puede aparecer por encima de la frase que lo provocó. Es de los
44
+ * fallos que no se notan hasta que confundes a alguien.
45
+ *
46
+ * Mandando los dos por la transcripción hay una sola cola y el orden es el de
47
+ * verdad. Se cubren `log`, `error`, `warn` e `info` —el parche de antes solo
48
+ * cubría `log`— y se formatea con `util.format`, que es lo que hace `console` de
49
+ * serie: sin eso, un `console.log("%s pruebas", 12)` sale con el `%s` puesto.
33
50
  */
34
- const ENTRAR = "\x1b[?1049h";
35
- const SALIR = "\x1b[?1049l";
36
51
  let dentro = false;
37
- let logOriginal = null;
52
+ const original = {};
38
53
  /**
39
- * Fija SOLO SI SE PIDE. Van dos veces que la pongo de serie y se rompe.
54
+ * De serie SÍ, desde la 1.6.7, y con salida de emergencia.
55
+ *
56
+ * DOS VECES ESTUVO AQUÍ Y DOS VECES SALIÓ ROTO
40
57
  *
41
- * La segunda: "ahora sale una barra y lo demas negro, escribo y se cierra".
58
+ * La 1.6.0 salió en negro ("no sale nada, se bugea") y la 1.6.5 con "una barra y
59
+ * lo demás negro, escribo y se cierra". Las dos veces pasaban las 820 pruebas:
60
+ * un emulador prueba lo que sabe emular, y una consola de verdad hace cosas que
61
+ * no sabe.
42
62
  *
43
- * La regla que saco de esto, y que no pienso volver a saltarme: esto no vuelve
44
- * a ser lo normal hasta que el dueño confirme que lo ha visto funcionar en SU
45
- * consola con la variable puesta. Un emulador -por bueno que sea, y el de
46
- * tests/terminal.test.mjs lo es- prueba lo que sabe emular. Ha hecho falta que
47
- * falle dos veces para aprenderlo.
63
+ * Esta vez el orden fue el otro: se entregó detrás de la variable, el dueño lo
64
+ * corrió en SU consola, y solo después pasó a ser lo normal. Lo hizo, y lo
65
+ * aprobó sabiendo lo que no le iba a gustar: que la caja no se queda clavada al
66
+ * hacer scroll. No se queda porque es texto del terminal, y en Claude Code
67
+ * tampoco se queda. Es el precio de recuperar la rueda del ratón y el historial.
68
+ *
69
+ * CHOCOLATITO_PANTALLA=normal devuelve la interfaz clásica. Existe porque una
70
+ * consola rara —una que no sea la suya ni la mía— puede seguir haciendo algo que
71
+ * no habíamos visto, y entonces el usuario necesita poder trabajar hoy, no
72
+ * esperar al parche.
48
73
  */
49
74
  export function pantallaFijaPedida() {
50
- return process.env.CHOCOLATITO_PANTALLA === "fija";
75
+ return process.env.CHOCOLATITO_PANTALLA !== "normal";
51
76
  }
52
77
  /** `null` = manda el estado real. Las pruebas lo fuerzan. */
53
78
  let forzado = null;
54
- /**
55
- * Enciende o apaga el modo a mano, SIN tocar el terminal.
56
- *
57
- * Las pruebas lo necesitan: entrar de verdad escribiría la secuencia de la
58
- * pantalla alternativa en la consola de quien corre las pruebas y le dejaría el
59
- * informe en una pantalla que desaparece al acabar.
60
- */
61
79
  export function forzarPantallaFija(valor) {
62
80
  forzado = valor;
63
81
  }
64
82
  export function enPantallaFija() {
65
83
  return forzado ?? dentro;
66
84
  }
85
+ /**
86
+ * Enciende el modo de árbol único. Devuelve si de verdad se encendió.
87
+ *
88
+ * EL BOOLEANO NO SOBRA, Y SE VOLVIÓ IMPRESCINDIBLE AL PONERLO POR DEFECTO
89
+ *
90
+ * Sin terminal —una salida redirigida a un archivo, un `chocolatito "tarea"` en
91
+ * un script, CI— aquí no se entra. Pero quien llama montaba el árbol de todas
92
+ * formas, y Ink sin terminal escribe su marco como texto suelto: la salida del
93
+ * programa sale con la caja de escribir dentro.
94
+ *
95
+ * Mientras esto iba detrás de una variable no pasaba nunca, porque nadie la pone
96
+ * en un script. De serie, es el caso normal.
97
+ */
67
98
  export function entrarEnPantallaFija() {
68
- if (dentro || !process.stdout.isTTY)
69
- return;
99
+ if (dentro)
100
+ return true;
101
+ if (!process.stdout.isTTY)
102
+ return false;
70
103
  dentro = true;
71
- process.stdout.write(ENTRAR);
72
- // Lo que se dibujaba en el terminal pasa a ser historial nuestro.
73
- redirigirDibujo((texto) => apuntar(texto));
74
- // Y el pie deja de pintar. Sigue guardando sus secciones -el spinner y la
75
- // linea de razonamiento publican ahi sin enterarse de nada- pero quien las
76
- // dibuja pasa a ser el arbol. Dos dueños del mismo trozo de pantalla es
77
- // exactamente el problema que esto viene a quitar.
104
+ // Lo que el agente dibuja pasa a ser texto de la transcripción. De ahí lo
105
+ // recoge <Static> y lo escribe en el historial del terminal, una sola vez.
106
+ redirigirDibujo((texto) => anotar(texto));
78
107
  habilitarPie(false);
79
- logOriginal = console.log;
80
- console.log = (...args) => {
81
- apuntar(args.map((a) => (typeof a === "string" ? a : String(a))).join(" ") + "\n");
108
+ interceptarConsola();
109
+ return true;
110
+ }
111
+ const CANALES = ["log", "error", "warn", "info"];
112
+ /**
113
+ * `console.clear` va aparte porque no es un mensaje, es una orden al terminal.
114
+ *
115
+ * Y estaba fuera de la lista, que era un agujero de los caros: /clear, /cd y el
116
+ * Ctrl+L del prompt clásico llaman a `console.clear()`, y con el árbol montado
117
+ * eso borra filas que Ink cree suyas. A partir de ahí Ink repinta sobre un sitio
118
+ * que ya no existe y lo que se ve es basura.
119
+ *
120
+ * Se manda a `limpiarLaPantalla`, que hace lo mismo pero en el orden que no
121
+ * rompe nada. Ver ui/ink/montarApp.tsx.
122
+ */
123
+ let clearOriginal = null;
124
+ /**
125
+ * Manda `console` a la transcripción.
126
+ *
127
+ * Se exporta para poder probarlo: `entrarEnPantallaFija` no se puede llamar en
128
+ * las pruebas porque exige un terminal de verdad, y un parche de consola sin
129
+ * probar es de las cosas que fallan en silencio —lo que se pierde no se ve, que
130
+ * es justo el problema—.
131
+ */
132
+ export function interceptarConsola() {
133
+ if (original.log)
134
+ return;
135
+ clearOriginal = console.clear;
136
+ console.clear = () => {
137
+ limpiarLaPantalla();
82
138
  };
139
+ for (const nombre of CANALES) {
140
+ original[nombre] = console[nombre];
141
+ console[nombre] = (...args) => {
142
+ anotar(`${format(...args)}\n`);
143
+ };
144
+ }
145
+ }
146
+ export function devolverConsola() {
147
+ if (clearOriginal) {
148
+ console.clear = clearOriginal;
149
+ clearOriginal = null;
150
+ }
151
+ for (const nombre of CANALES) {
152
+ const previo = original[nombre];
153
+ if (previo)
154
+ console[nombre] = previo;
155
+ delete original[nombre];
156
+ }
83
157
  }
84
158
  /**
85
159
  * Devuelve el terminal como estaba.
86
160
  *
87
- * Se llama al salir del programa, y también si algo revienta: quedarse en la
88
- * pantalla alternativa deja al usuario en una consola en blanco sin su prompt,
89
- * y la única salida es cerrar la ventana.
161
+ * Aquí ya no hay ninguna pantalla que abandonar —esa era la versión de antes—,
162
+ * pero hay que devolver el conducto de dibujo y el pie: si el proceso sigue
163
+ * vivo después de esto (una prueba, una sesión que cambia de modo) y el dibujo
164
+ * se queda desviado, todo lo que se escriba va a una transcripción que ya no
165
+ * pinta nadie.
90
166
  */
91
167
  export function salirDePantallaFija() {
92
168
  if (!dentro)
93
169
  return;
94
170
  dentro = false;
95
- if (logOriginal) {
96
- console.log = logOriginal;
97
- logOriginal = null;
98
- }
99
171
  redirigirDibujo(null);
100
172
  habilitarPie(null);
101
- try {
102
- process.stdout.write(SALIR);
103
- }
104
- catch { }
173
+ devolverConsola();
105
174
  }
106
175
  process.on("exit", salirDePantallaFija);
107
- /**
108
- * UN ERROR DENTRO DE LA PANTALLA ALTERNATIVA SE PIERDE
176
+ /*
177
+ * AQUÍ HABÍA UN `unhandledRejection` QUE MATABA EL PROGRAMA. SE HA QUITADO.
178
+ *
179
+ * Existía para que un error dentro de la pantalla alternativa no se perdiera al
180
+ * salir de ella. Pero se registraba al importar el módulo, o sea SIEMPRE, con
181
+ * pantalla fija o sin ella, y hacía `process.exit(1)`.
109
182
  *
110
- * Y esa es la razon de que no haya podido diagnosticar el negro en dos intentos.
111
- * Si algo revienta despues de montar el arbol, el mensaje se imprime DENTRO de
112
- * la pantalla alternativa; acto seguido el proceso sale, `salirDePantallaFija`
113
- * devuelve la pantalla normal, y el error se va con la que lo contenia.
183
+ * index.ts tiene su propia red de seguridad para lo mismo, y su criterio es el
184
+ * contrario y el bueno: "una promesa sin capturar no debería costarle al usuario
185
+ * la sesión entera, con todo su contexto. Se avisa y se sigue." Node ejecuta los
186
+ * dos manejadores, pero el de aquí se registraba antes y salía, así que ganaba
187
+ * siempre.
114
188
  *
115
- * Desde fuera se ve exactamente como lo conto el dueño: una barra, todo negro, y
116
- * se cierra sin decir nada.
189
+ * Consecuencia en la versión publicada: cualquier promesa de fondo que fallara
190
+ * —un servidor MCP que no levanta, el chequeo de licencia, la comprobación de
191
+ * actualizaciones— cerraba Chocolatito de golpe. Sin mensaje, y sin tener nada
192
+ * que ver con la interfaz.
117
193
  *
118
- * Aqui se sale de la pantalla PRIMERO y se escribe despues, para que el mensaje
119
- * quede donde se puede leer. Y se guarda ademas en un archivo, porque en una
120
- * consola que se cierra sola no da tiempo ni a mirarlo.
194
+ * Con el buffer normal el motivo original ya no existe: un error impreso se
195
+ * queda en pantalla, porque no hay ninguna pantalla que se lleve el mensaje al
196
+ * cerrarse.
121
197
  */
122
- function contarLoQueFallo(err) {
123
- salirDePantallaFija();
124
- const detalle = err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err);
125
- try {
126
- fs.writeFileSync(path.join(os.homedir(), ".chocolatito-error.txt"), `${new Date().toISOString()}\n${detalle}\n`, "utf-8");
127
- }
128
- catch { }
129
- process.stderr.write(`\nChocolatito se cerro por un error:\n${detalle}\n`);
130
- process.stderr.write(`Guardado en ${path.join(os.homedir(), ".chocolatito-error.txt")}\n`);
131
- }
132
- process.on("uncaughtException", (err) => {
133
- contarLoQueFallo(err);
134
- process.exit(1);
135
- });
136
- process.on("unhandledRejection", (err) => {
137
- contarLoQueFallo(err);
138
- process.exit(1);
139
- });
@@ -4,23 +4,6 @@ export interface PermissionPromptResult {
4
4
  userFeedback?: string;
5
5
  }
6
6
  export declare function setAutoApprove(value: boolean): void;
7
- /**
8
- * DIALOGO DE PERMISO
9
- *
10
- * La version anterior tenia dos fallos visibles en cuanto se usaba:
11
- *
12
- * 1. Al mover la seleccion subia el cursor 12 lineas fijas, pero la caja mide
13
- * entre 10 y 14 segun cuantos detalles tenga. Cada flecha dejaba media caja
14
- * huerfana en pantalla, y por eso salian marcos vacios apilados.
15
- *
16
- * 2. El titulo llevaba un simbolo de anchura ambigua. El terminal lo pinta
17
- * ocupando dos columnas y el calculo del ancho contaba una, asi que el borde
18
- * derecho quedaba descuadrado justo en esa linea.
19
- *
20
- * Aqui se dibuja la caja a mano, con anchura conocida, y se repinta subiendo
21
- * exactamente las lineas que se escribieron.
22
- */
23
- export declare function askToolPermission(toolName: string, args: Record<string, any>, cwd?: string): Promise<PermissionPromptResult>;
24
7
  export declare const PERMISSION_OPTIONS: {
25
8
  label: string;
26
9
  hint: string;
@@ -33,3 +16,22 @@ export declare const PERMISSION_OPTIONS: {
33
16
  * derecho no se ve leyendo el codigo, solo ejecutandolo.
34
17
  */
35
18
  export declare function buildPermissionBox(toolName: string, args: Record<string, any>, cwd: string, selectedIndex: number, width?: number): string[];
19
+ /**
20
+ * Se pide el terminal prestado antes de abrir esto.
21
+ *
22
+ * Con el árbol de Ink montado hay un dueño de la pantalla, y esta función es de
23
+ * las que necesitan el teclado para ellas solas: escribe directo a stdout y abre
24
+ * su propia lectura de teclas. Sin el préstamo, las dos cuentas de filas se pisan
25
+ * y el marco de Ink no se recupera. Sin árbol, `conElTerminalPrestado` ejecuta la
26
+ * tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
27
+ */
28
+ export declare function askToolPermission(toolName: string, args: Record<string, any>, cwd?: string): Promise<PermissionPromptResult>;
29
+ /**
30
+ * El mismo contenido del dialogo del terminal, sin color y sin marco, para
31
+ * pintarlo en HTML.
32
+ *
33
+ * Se reutiliza `details()` a proposito: dos sitios decidiendo por separado que
34
+ * hay que enseñar antes de aprobar un borrado acaban discrepando, y el que
35
+ * discrepa siempre es el que menos se mira.
36
+ */
37
+ export declare function detallesEnTextoPlano(toolName: string, args: Record<string, any>, cwd?: string): string[];
@@ -3,6 +3,8 @@ import process from "node:process";
3
3
  import chalk from "chalk";
4
4
  import { cycleMode, describeMode, getMode, modeAnnouncement } from "./modes.js";
5
5
  import { isOutsideProject, isSvgSource } from "../config/permissions.js";
6
+ import { conElTerminalPrestado } from "./ink/prestamo.js";
7
+ import { pedirPermisoWeb } from "../servidor/puente.js";
6
8
  /**
7
9
  * Aprobacion automatica: se activa con --yes / -y o con la variable de entorno
8
10
  * CHOCOLATITO_AUTO_APPROVE. Pensada para modo no interactivo y CI.
@@ -27,7 +29,7 @@ export function setAutoApprove(value) {
27
29
  * Aqui se dibuja la caja a mano, con anchura conocida, y se repinta subiendo
28
30
  * exactamente las lineas que se escribieron.
29
31
  */
30
- export async function askToolPermission(toolName, args, cwd = process.cwd()) {
32
+ async function askToolPermissionDirecto(toolName, args, cwd = process.cwd()) {
31
33
  if (autoApprove) {
32
34
  return { approved: true };
33
35
  }
@@ -351,3 +353,44 @@ function details(toolName, args, cwd, width) {
351
353
  out.push({ text: "", length: 0 });
352
354
  return out;
353
355
  }
356
+ /**
357
+ * Se pide el terminal prestado antes de abrir esto.
358
+ *
359
+ * Con el árbol de Ink montado hay un dueño de la pantalla, y esta función es de
360
+ * las que necesitan el teclado para ellas solas: escribe directo a stdout y abre
361
+ * su propia lectura de teclas. Sin el préstamo, las dos cuentas de filas se pisan
362
+ * y el marco de Ink no se recupera. Sin árbol, `conElTerminalPrestado` ejecuta la
363
+ * tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
364
+ */
365
+ export function askToolPermission(toolName, args, cwd = process.cwd()) {
366
+ // --yes primero: si ya se aprobo todo a mano, no hay que molestar a nadie ni
367
+ // abrir en el movil un dialogo que se contestaria solo.
368
+ if (autoApprove)
369
+ return Promise.resolve({ approved: true });
370
+ // Con `--servir` y alguien conectado, la pregunta va al navegador. Devuelve
371
+ // null cuando no hay ningun cliente, y entonces esto sigue como siempre: al
372
+ // terminal, que es donde hay un humano. Lo que NO pasa nunca es que se apruebe
373
+ // algo por no tener a quien preguntar.
374
+ const porWeb = pedirPermisoWeb(toolName, detallesEnTextoPlano(toolName, args, cwd), PERMISSION_OPTIONS);
375
+ if (porWeb)
376
+ return porWeb;
377
+ return conElTerminalPrestado(() => askToolPermissionDirecto(toolName, args, cwd));
378
+ }
379
+ /**
380
+ * El mismo contenido del dialogo del terminal, sin color y sin marco, para
381
+ * pintarlo en HTML.
382
+ *
383
+ * Se reutiliza `details()` a proposito: dos sitios decidiendo por separado que
384
+ * hay que enseñar antes de aprobar un borrado acaban discrepando, y el que
385
+ * discrepa siempre es el que menos se mira.
386
+ */
387
+ export function detallesEnTextoPlano(toolName, args, cwd = process.cwd()) {
388
+ const sinColor = (t) => t.replace(/\u001b\[[0-9;]*m/g, "");
389
+ const kind = classify(toolName, args);
390
+ return [
391
+ sinColor(kind.title).trim(),
392
+ ...details(toolName, args, cwd, 72)
393
+ .map((d) => sinColor(d.text).trim())
394
+ .filter((t) => t.length > 0),
395
+ ];
396
+ }
@@ -3,7 +3,7 @@
3
3
  * pintara donde le tocara, dos productores independientes -el razonamiento y el
4
4
  * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
5
5
  */
6
- declare const ORDEN: readonly ["razonamiento", "spinner", "entrada"];
6
+ declare const ORDEN: readonly ["tareas", "razonamiento", "spinner", "entrada"];
7
7
  export type SeccionDelPie = (typeof ORDEN)[number];
8
8
  /**
9
9
  * Enciende o apaga el pie a mano. Con `null` vuelve a mandar el terminal.
@@ -54,7 +54,7 @@ const MOSTRAR_CURSOR = "\x1b[?25h";
54
54
  * pintara donde le tocara, dos productores independientes -el razonamiento y el
55
55
  * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
56
56
  */
57
- const ORDEN = ["razonamiento", "spinner", "entrada"];
57
+ const ORDEN = ["tareas", "razonamiento", "spinner", "entrada"];
58
58
  const secciones = new Map();
59
59
  /**
60
60
  * Texto permanente que aun no ha recibido su salto de linea.
@@ -219,10 +219,24 @@ function anotar(nombre, lineas) {
219
219
  * veces por segundo y la barra de escribir casi nunca cambia entre dos.
220
220
  */
221
221
  export function fijarSeccion(nombre, lineas) {
222
- if (!pieUsable())
223
- return;
224
- if (anotar(nombre, lineas))
225
- repintar("");
222
+ // DELEGA, Y ESO ARREGLA UN FALLO GORDO
223
+ //
224
+ // Antes empezaba con `if (!pieUsable()) return;`, o sea que con el pie apagado
225
+ // -que es exactamente lo que pasa cuando manda el arbol de Ink- esta funcion no
226
+ // hacia NADA. Ni guardaba la seccion ni avisaba a quien la pinta.
227
+ //
228
+ // Dos consecuencias, las dos visibles:
229
+ //
230
+ // - La linea de razonamiento (el 💭) es el unico sitio que llama aqui
231
+ // (ui/reasoningStream.ts:122). Con el arbol montado no salia nunca.
232
+ // - `quitarSeccion` pasa por aqui. Asi que `quitarSeccion("spinner")` al
233
+ // acabar el turno tampoco hacia nada: el "✳ Trabajando…" se quedaba
234
+ // puesto para siempre, mucho despues de que el agente hubiera terminado.
235
+ // Eso es lo que se ve como "se queda trabado".
236
+ //
237
+ // `fijarSecciones` ya distingue bien las dos cosas -guardar y avisar SIEMPRE,
238
+ // pintar solo si el pie esta encendido-, asi que aqui basta con delegar.
239
+ fijarSecciones([[nombre, lineas]]);
226
240
  }
227
241
  /**
228
242
  * Varias secciones de una vez, con UN solo repintado.
@@ -312,6 +326,14 @@ export function soltarPie() {
312
326
  return;
313
327
  secciones.clear();
314
328
  repintar("");
329
+ // Y se avisa, que si no el arbol sigue pintando una barra que ya no existe
330
+ // hasta que otra cosa cualquiera le haga redibujar.
331
+ for (const cb of alCambiarElPie) {
332
+ try {
333
+ cb();
334
+ }
335
+ catch { }
336
+ }
315
337
  }
316
338
  /**
317
339
  * Olvida el estado sin tocar la pantalla.
@@ -179,6 +179,14 @@ export function formatToolName(name) {
179
179
  return "Move";
180
180
  case "run_command":
181
181
  return "Bash";
182
+ case "start_background_task":
183
+ return "BgTask";
184
+ case "read_task_output":
185
+ return "TaskOutput";
186
+ case "list_background_tasks":
187
+ return "ListTasks";
188
+ case "stop_background_task":
189
+ return "StopTask";
182
190
  case "list_dir":
183
191
  return "ListDir";
184
192
  case "grep_search":
@@ -28,10 +28,12 @@ export interface OpcionDeLista {
28
28
  detalle?: string;
29
29
  }
30
30
  /**
31
- * Enseña la lista y devuelve el id elegido. `null` si se sale con Esc.
31
+ * Se pide el terminal prestado antes de abrir esto.
32
32
  *
33
- * Sin terminal no hay nada que elegir: devuelve null en el acto en vez de
34
- * quedarse esperando una tecla que no va a llegar nunca. Es lo que pasaria al
35
- * correrlo desde un script o dentro de las pruebas.
33
+ * Con el árbol de Ink montado hay un dueño de la pantalla, y esta función es de
34
+ * las que necesitan el teclado para ellas solas: escribe directo a stdout y abre
35
+ * su propia lectura de teclas. Sin el préstamo, las dos cuentas de filas se pisan
36
+ * y el marco de Ink no se recupera. Sin árbol, `conElTerminalPrestado` ejecuta la
37
+ * tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
36
38
  */
37
39
  export declare function elegirDeLista(titulo: string, opciones: OpcionDeLista[], indiceInicial?: number): Promise<string | null>;
@@ -2,6 +2,7 @@ import chalk from "chalk";
2
2
  import process from "node:process";
3
3
  import readline from "node:readline";
4
4
  import { dibujar } from "./salida.js";
5
+ import { conElTerminalPrestado } from "./ink/prestamo.js";
5
6
  /**
6
7
  * Enseña la lista y devuelve el id elegido. `null` si se sale con Esc.
7
8
  *
@@ -9,7 +10,7 @@ import { dibujar } from "./salida.js";
9
10
  * quedarse esperando una tecla que no va a llegar nunca. Es lo que pasaria al
10
11
  * correrlo desde un script o dentro de las pruebas.
11
12
  */
12
- export async function elegirDeLista(titulo, opciones, indiceInicial = 0) {
13
+ async function elegirDeListaDirecto(titulo, opciones, indiceInicial = 0) {
13
14
  if (opciones.length === 0)
14
15
  return null;
15
16
  if (!process.stdin.isTTY)
@@ -89,3 +90,15 @@ export async function elegirDeLista(titulo, opciones, indiceInicial = 0) {
89
90
  process.stdin.on("keypress", alPulsar);
90
91
  });
91
92
  }
93
+ /**
94
+ * Se pide el terminal prestado antes de abrir esto.
95
+ *
96
+ * Con el árbol de Ink montado hay un dueño de la pantalla, y esta función es de
97
+ * las que necesitan el teclado para ellas solas: escribe directo a stdout y abre
98
+ * su propia lectura de teclas. Sin el préstamo, las dos cuentas de filas se pisan
99
+ * y el marco de Ink no se recupera. Sin árbol, `conElTerminalPrestado` ejecuta la
100
+ * tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
101
+ */
102
+ export function elegirDeLista(titulo, opciones, indiceInicial = 0) {
103
+ return conElTerminalPrestado(() => elegirDeListaDirecto(titulo, opciones, indiceInicial));
104
+ }
@@ -431,6 +431,68 @@ async function setTabOverlay(tabId, activo) {
431
431
  }
432
432
  }
433
433
 
434
+ // Telemetría de red: errores HTTP (>=400) y fallos de conexión por pestaña
435
+ const MAX_NETWORK_ERRORS = 100;
436
+ /** tabId -> Array<{ method: string, url: string, status?: number, error?: string, timestamp: number }> */
437
+ const networkErrorsByTab = new Map();
438
+
439
+ function registrarErrorRed(tabId, info) {
440
+ if (!tabId || tabId < 0) return;
441
+ let lista = networkErrorsByTab.get(tabId);
442
+ if (!lista) {
443
+ lista = [];
444
+ networkErrorsByTab.set(tabId, lista);
445
+ }
446
+ lista.push({
447
+ method: info.method || "GET",
448
+ url: info.url || "",
449
+ status: info.status !== undefined ? info.status : (info.statusCode !== undefined ? info.statusCode : undefined),
450
+ error: info.error || undefined,
451
+ timestamp: info.timestamp || Date.now(),
452
+ });
453
+ if (lista.length > MAX_NETWORK_ERRORS) {
454
+ lista.shift();
455
+ }
456
+ }
457
+
458
+ try {
459
+ if (typeof chrome !== "undefined" && chrome.webRequest && chrome.webRequest.onCompleted && typeof chrome.webRequest.onCompleted.addListener === "function") {
460
+ chrome.webRequest.onCompleted.addListener(
461
+ (details) => {
462
+ if (details && details.tabId > 0 && details.statusCode >= 400) {
463
+ registrarErrorRed(details.tabId, {
464
+ method: details.method,
465
+ url: details.url,
466
+ status: details.statusCode,
467
+ error: `HTTP ${details.statusCode}`,
468
+ timestamp: Math.round(details.timeStamp || Date.now()),
469
+ });
470
+ }
471
+ },
472
+ { urls: ["<all_urls>"] }
473
+ );
474
+ }
475
+ } catch (_) {}
476
+
477
+ try {
478
+ if (typeof chrome !== "undefined" && chrome.webRequest && chrome.webRequest.onErrorOccurred && typeof chrome.webRequest.onErrorOccurred.addListener === "function") {
479
+ chrome.webRequest.onErrorOccurred.addListener(
480
+ (details) => {
481
+ if (details && details.tabId > 0) {
482
+ registrarErrorRed(details.tabId, {
483
+ method: details.method,
484
+ url: details.url,
485
+ status: undefined,
486
+ error: details.error || "net::ERR_FAILED",
487
+ timestamp: Math.round(details.timeStamp || Date.now()),
488
+ });
489
+ }
490
+ },
491
+ { urls: ["<all_urls>"] }
492
+ );
493
+ }
494
+ } catch (_) {}
495
+
434
496
  // D8: Detección y reporte de descargas disparadas por acciones del agente
435
497
  const descargasRecientes = [];
436
498
  try {
@@ -1012,6 +1074,23 @@ async function handle(msg, st) {
1012
1074
  return res;
1013
1075
  }
1014
1076
 
1077
+ if (cmd === "getConsoleLogs") {
1078
+ const { tabId } = await readTab(msg.tabId, st);
1079
+ const res = await tell(tabId, 0, { cmd: "getConsoleLogs", clear: msg.clear === true });
1080
+ if (!res || !res.ok) throw new Error((res && res.error) || "no se pudieron obtener los logs de consola");
1081
+ return { ok: true, tabId, logs: res.logs || [] };
1082
+ }
1083
+
1084
+ if (cmd === "getNetworkErrors") {
1085
+ const { tabId } = await readTab(msg.tabId, st);
1086
+ const lista = networkErrorsByTab.get(tabId) || [];
1087
+ const errors = [...lista];
1088
+ if (msg.clear === true) {
1089
+ networkErrorsByTab.delete(tabId);
1090
+ }
1091
+ return { ok: true, tabId, errors };
1092
+ }
1093
+
1015
1094
  if (cmd === "eval") {
1016
1095
  // eval ejecuta JavaScript arbitrario en la pagina: puede pulsar, enviar
1017
1096
  // formularios y leerlo todo. Es actuacion, no lectura.
@@ -1138,6 +1217,7 @@ async function handle(msg, st) {
1138
1217
  try {
1139
1218
  chrome.tabs.onRemoved.addListener((tabId) => {
1140
1219
  setTabOverlay(tabId, false);
1220
+ networkErrorsByTab.delete(tabId);
1141
1221
  for (const link of links.values()) {
1142
1222
  if (!link.state) continue;
1143
1223
  link.state.propias.delete(tabId);