chocolatito-code 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +66 -0
  2. package/dist/agent/huecos.d.ts +58 -0
  3. package/dist/agent/huecos.js +81 -0
  4. package/dist/agent/loop.js +218 -41
  5. package/dist/agent/salidaDelAgente.d.ts +43 -0
  6. package/dist/agent/salidaDelAgente.js +39 -0
  7. package/dist/agent/toolGate.js +6 -1
  8. package/dist/config/engine.d.ts +21 -1
  9. package/dist/config/engine.js +22 -1
  10. package/dist/config/nodeVersion.d.ts +13 -0
  11. package/dist/config/nodeVersion.js +26 -0
  12. package/dist/index.js +22 -7
  13. package/dist/mcp/client.d.ts +28 -0
  14. package/dist/mcp/client.js +62 -8
  15. package/dist/mcp/ide.d.ts +76 -0
  16. package/dist/mcp/ide.js +160 -0
  17. package/dist/mcp/manager.js +9 -0
  18. package/dist/tools/browserExtension.js +153 -7
  19. package/dist/tools/computerUse.d.ts +1 -1
  20. package/dist/tools/computerUse.js +183 -2
  21. package/dist/tools/definitions.js +9 -2
  22. package/dist/tools/grepSearch.js +111 -23
  23. package/dist/tools/runner.js +1 -1
  24. package/dist/tools/toolDefsComputer.js +6 -2
  25. package/dist/tools/visionBridge.js +60 -3
  26. package/dist/tools/win/hostScript.js +154 -0
  27. package/dist/tools/writeFile.d.ts +16 -1
  28. package/dist/tools/writeFile.js +22 -1
  29. package/dist/ui/interrupt.d.ts +10 -0
  30. package/dist/ui/interrupt.js +53 -4
  31. package/dist/ui/keyboardGuard.d.ts +21 -0
  32. package/dist/ui/keyboardGuard.js +30 -1
  33. package/dist/ui/pegados.d.ts +50 -0
  34. package/dist/ui/pegados.js +83 -0
  35. package/dist/ui/pieFijo.d.ts +56 -0
  36. package/dist/ui/pieFijo.js +309 -0
  37. package/dist/ui/prompt.d.ts +5 -0
  38. package/dist/ui/prompt.js +109 -16
  39. package/dist/ui/reasoningStream.js +22 -12
  40. package/dist/ui/salida.d.ts +10 -0
  41. package/dist/ui/salida.js +50 -0
  42. package/dist/ui/spinner.d.ts +15 -8
  43. package/dist/ui/spinner.js +80 -42
  44. package/extension/background.js +191 -14
  45. package/extension/content.js +446 -48
  46. package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
  47. package/extension/iconos/128.png +0 -0
  48. package/extension/iconos/16.png +0 -0
  49. package/extension/iconos/32.png +0 -0
  50. package/extension/iconos/48.png +0 -0
  51. package/extension/iconos/ORIGEN.md +48 -0
  52. package/extension/iconos/logotipo.png +0 -0
  53. package/extension/manifest.json +27 -4
  54. package/extension/popup.html +16 -6
  55. package/package.json +3 -2
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Un pegado grande no se mete entero en el recuadro.
3
+ *
4
+ * EL PROBLEMA
5
+ *
6
+ * Pegar un error de compilacion de 300 lineas metia las 300 lineas dentro del
7
+ * prompt. El recuadro crecia hasta llenar la pantalla, el marco se volvia
8
+ * inutil, y para deshacerlo habia que pulsar borrar trescientas veces. Es el
9
+ * caso normal de usar un agente: uno pega un log, una traza, un archivo entero.
10
+ *
11
+ * COMO QUEDA
12
+ *
13
+ * El texto se guarda aparte y en el recuadro solo aparece una marca corta:
14
+ *
15
+ * ❯ arregla esto [pegado #1 · 347 lineas]
16
+ *
17
+ * Al enviar, la marca se sustituye por el texto completo, asi que el modelo
18
+ * recibe exactamente lo que se pego. Y un solo borrar se lleva la marca entera:
19
+ * lo que ocupa una linea se borra como una linea.
20
+ *
21
+ * POR QUE UNA MARCA DE TEXTO Y NO UNA ESTRUCTURA APARTE
22
+ *
23
+ * Porque el buffer del prompt es una cadena y todo lo demas -el cursor, el
24
+ * historial, el recorte del marco, la busqueda de menciones- cuenta caracteres.
25
+ * Meter ahi un objeto obligaria a tocar las seis cosas. Una marca de texto se
26
+ * lleva bien con todo lo que ya funciona.
27
+ */
28
+ /** A partir de aqui, un pegado se guarda aparte en vez de llenar el recuadro. */
29
+ export const MINIMO_LINEAS = 5;
30
+ export const MINIMO_CARACTERES = 400;
31
+ const guardados = new Map();
32
+ let siguiente = 1;
33
+ /** Reconoce una marca. El id es lo unico que hace falta para recuperar el texto. */
34
+ const MARCA = /\[pegado #(\d+) · [^\]]*\]/g;
35
+ function resumen(texto) {
36
+ const lineas = texto.split("\n").length;
37
+ if (lineas > 1)
38
+ return `${lineas.toLocaleString("es")} lineas`;
39
+ return `${texto.length.toLocaleString("es")} caracteres`;
40
+ }
41
+ /** Si merece la pena guardarlo aparte, o es lo bastante corto para verlo entero. */
42
+ export function esGrande(texto) {
43
+ return texto.length >= MINIMO_CARACTERES || texto.split("\n").length > MINIMO_LINEAS;
44
+ }
45
+ /** Guarda el texto y devuelve la marca que va en el recuadro. */
46
+ export function guardarPegado(texto) {
47
+ const id = siguiente++;
48
+ guardados.set(id, texto);
49
+ return `[pegado #${id} · ${resumen(texto)}]`;
50
+ }
51
+ /** Devuelve el texto de verdad, con las marcas sustituidas. Es lo que ve el modelo. */
52
+ export function expandirPegados(entrada) {
53
+ // Una marca de un id que ya no existe se deja tal cual: es texto que el
54
+ // usuario escribio a mano, y borrarselo seria peor que dejarlo.
55
+ return entrada.replace(MARCA, (todo, id) => guardados.get(Number(id)) ?? todo);
56
+ }
57
+ /**
58
+ * Si justo antes del cursor termina una marca, dice donde empieza.
59
+ *
60
+ * Es lo que hace que un solo borrar se lleve el pegado entero. Sin esto habria
61
+ * que borrar la marca letra a letra, que es un poco menos absurdo que borrar
62
+ * las 347 lineas pero sigue siendo absurdo.
63
+ */
64
+ export function marcaQueTerminaEn(buffer, cursor) {
65
+ if (cursor <= 0 || buffer[cursor - 1] !== "]")
66
+ return null;
67
+ const trozo = buffer.slice(0, cursor);
68
+ const inicio = trozo.lastIndexOf("[pegado #");
69
+ if (inicio < 0)
70
+ return null;
71
+ // Que lo que hay entre los corchetes sea de verdad una marca y no un
72
+ // "[pegado #" que el usuario escribiera a mano.
73
+ return /^\[pegado #\d+ · [^\]]*\]$/.test(trozo.slice(inicio)) ? { inicio } : null;
74
+ }
75
+ /** Se olvidan al cerrar el prompt: cada turno empieza con su propia cuenta. */
76
+ export function limpiarPegados() {
77
+ guardados.clear();
78
+ siguiente = 1;
79
+ }
80
+ /** Cuantos hay guardados ahora mismo. Solo para las pruebas. */
81
+ export function cuantosPegados() {
82
+ return guardados.size;
83
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Las secciones, de arriba abajo. El orden es fijo a proposito: si cada quien
3
+ * pintara donde le tocara, dos productores independientes -el razonamiento y el
4
+ * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
5
+ */
6
+ declare const ORDEN: readonly ["razonamiento", "spinner", "entrada"];
7
+ export type SeccionDelPie = (typeof ORDEN)[number];
8
+ /**
9
+ * Enciende o apaga el pie a mano. Con `null` vuelve a mandar el terminal.
10
+ *
11
+ * Las pruebas lo encienden porque en `node --test` no hay TTY y sin esto el pie
12
+ * seria un no-op silencioso: pasarian todas sin comprobar nada.
13
+ */
14
+ export declare function habilitarPie(valor: boolean | null): void;
15
+ /** ¿Hay algo pintado abajo que haya que respetar? */
16
+ export declare function hayPie(): boolean;
17
+ /**
18
+ * Pone (o reemplaza) una seccion del pie y repinta.
19
+ *
20
+ * Con la lista vacia la seccion desaparece, que es lo mismo que quitarla. Si el
21
+ * contenido es identico al que ya habia, no se repinta: el spinner llama ocho
22
+ * veces por segundo y la barra de escribir casi nunca cambia entre dos.
23
+ */
24
+ export declare function fijarSeccion(nombre: SeccionDelPie, lineas: string[]): void;
25
+ /**
26
+ * Varias secciones de una vez, con UN solo repintado.
27
+ *
28
+ * El spinner cambia su linea y la barra de escribir en el mismo tick. Puestas
29
+ * por separado se pintaba el bloque dos veces cada 120 ms, y la segunda pasada
30
+ * borraba y reescribia lo que la primera acababa de dejar: el doble de bytes
31
+ * para el mismo resultado, y un parpadeo que se nota en un terminal lento.
32
+ */
33
+ export declare function fijarSecciones(entradas: Array<[SeccionDelPie, string[]]>): void;
34
+ export declare function quitarSeccion(nombre: SeccionDelPie): void;
35
+ /**
36
+ * Escribe texto permanente sin que el pie se lo lleve por delante.
37
+ *
38
+ * Sin pie es un write normal y no cuesta nada; por eso puede sustituir a
39
+ * `console.log` en el camino del agente sin pensarlo dos veces.
40
+ */
41
+ export declare function escribirEncimaDelPie(texto: string): void;
42
+ /**
43
+ * Quita el pie entero y deja el terminal como si nunca hubiera estado.
44
+ *
45
+ * Lo que quede a medias de linea se conserva en pantalla: es texto del agente,
46
+ * no adorno, y borrarlo seria perder salida.
47
+ */
48
+ export declare function soltarPie(): void;
49
+ /**
50
+ * Olvida el estado sin tocar la pantalla.
51
+ *
52
+ * Para las pruebas y para la salida del proceso: ahi no queda terminal al que
53
+ * repintar, pero el cursor escondido si sobrevive y hay que devolverlo.
54
+ */
55
+ export declare function reiniciarPie(): void;
56
+ export {};
@@ -0,0 +1,309 @@
1
+ import process from "node:process";
2
+ import { dibujar } from "./salida.js";
3
+ /**
4
+ * EL PIE QUE NO SE VA CON EL SCROLL
5
+ *
6
+ * EL PROBLEMA
7
+ *
8
+ * El spinner y la linea de razonamiento se pintaban donde estuviera el cursor:
9
+ *
10
+ * process.stdout.write(`\r\x1b[2K${glyph} ${verb} (${segundos}s)`);
11
+ *
12
+ * Eso escribe en la fila actual y ahi se queda. En cuanto llega la respuesta,
13
+ * cada linea nueva empuja el contenido hacia arriba y se lleva el spinner con
14
+ * ella. El usuario lo describio mejor que ninguna documentacion: "la nubecita y
15
+ * el rayito suben con lo que me respondes". Y lo mismo la barra de escribir.
16
+ *
17
+ * El resultado es que mientras el modelo habla no hay NADA fijo en pantalla: ni
18
+ * cuanto lleva, ni cuantos tokens, ni donde escribir para encolar algo. Todo
19
+ * queda enterrado unas pantallas mas arriba.
20
+ *
21
+ * LA IDEA
22
+ *
23
+ * Un terminal no tiene "zonas". Se puede pedir una con la region de scroll
24
+ * (DECSTBM), pero lo que sale por arriba de una region asi NO entra en el
25
+ * historial del terminal en la mayoria de emuladores: se pierde. En un agente
26
+ * de codigo, donde media sesion consiste en subir a releer lo que salio, eso es
27
+ * inaceptable.
28
+ *
29
+ * Asi que se hace al reves, sin regiones: el pie se BORRA antes de cada
30
+ * escritura permanente y se REPINTA justo despues. El texto permanente se
31
+ * escribe con saltos de linea normales, entra en el historial como siempre, y
32
+ * el pie acaba de nuevo abajo del todo. Visto desde fuera, el pie no se mueve.
33
+ *
34
+ * LA INVARIANTE QUE LO SOSTIENE
35
+ *
36
+ * Tras pintar, el cursor queda al final de la ultima fila del bloque. Con eso
37
+ * basta para borrarlo entero: subir `filasPintadas - 1` filas, ir a la columna
38
+ * cero y borrar hacia abajo. Se cuentan filas FISICAS (lo que el terminal parte
39
+ * al llegar al ancho), no elementos del array: contar lineas logicas es
40
+ * exactamente el fallo que dejaba barras huerfanas en el prompt.
41
+ *
42
+ * Y LA SEGUNDA, LA QUE EVITA CORROMPER LA SALIDA AJENA
43
+ *
44
+ * Sin secciones puestas, el pie no pinta nada y la pantalla queda byte a byte
45
+ * como la habria dejado un `console.log` normal. Por eso el resto del programa
46
+ * -las herramientas, el renderizador, los avisos- puede seguir escribiendo
47
+ * directo sin saber que esto existe. Solo hay que pasar por aqui mientras haya
48
+ * pie, que es justo el rato que el bucle controla toda la salida.
49
+ */
50
+ const OCULTAR_CURSOR = "\x1b[?25l";
51
+ const MOSTRAR_CURSOR = "\x1b[?25h";
52
+ /**
53
+ * Las secciones, de arriba abajo. El orden es fijo a proposito: si cada quien
54
+ * pintara donde le tocara, dos productores independientes -el razonamiento y el
55
+ * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
56
+ */
57
+ const ORDEN = ["razonamiento", "spinner", "entrada"];
58
+ const secciones = new Map();
59
+ /**
60
+ * Texto permanente que aun no ha recibido su salto de linea.
61
+ *
62
+ * Un `flush()` de markdown puede dejar media linea escrita. Si el pie se
63
+ * pintara detras, empezaria en mitad de esa fila. En vez de calcular columnas
64
+ * -aritmetica que se rompe con cada emoji- se guarda ese resto AQUI y se
65
+ * repinta como primera linea del bloque. Deja de ser texto permanente hasta que
66
+ * llegue su salto, y entonces se escribe de verdad.
67
+ */
68
+ let colaSinSalto = "";
69
+ /** Filas fisicas que ocupo el bloque en el ultimo pintado. Es lo que se borra. */
70
+ let filasPintadas = 0;
71
+ /**
72
+ * Lo ultimo que escribimos NOSOTROS se quedo a medio renglon.
73
+ *
74
+ * Sin secciones puestas, la linea a medias deja de estar protegida y sale como
75
+ * texto permanente (ver `repintar`). Si despues vuelve a haber pie, tiene que
76
+ * empezar en un renglon nuevo y no pegado al final de esa frase.
77
+ */
78
+ let aMediaLinea = false;
79
+ let cursorEscondido = false;
80
+ /** `null` = decidirlo por el terminal. Las pruebas lo fuerzan. */
81
+ let habilitado = null;
82
+ function pieUsable() {
83
+ return habilitado ?? Boolean(process.stdout.isTTY);
84
+ }
85
+ /**
86
+ * Enciende o apaga el pie a mano. Con `null` vuelve a mandar el terminal.
87
+ *
88
+ * Las pruebas lo encienden porque en `node --test` no hay TTY y sin esto el pie
89
+ * seria un no-op silencioso: pasarian todas sin comprobar nada.
90
+ */
91
+ export function habilitarPie(valor) {
92
+ habilitado = valor;
93
+ }
94
+ function ancho() {
95
+ return Math.max(20, process.stdout.columns || 80);
96
+ }
97
+ function alto() {
98
+ return Math.max(4, process.stdout.rows || 24);
99
+ }
100
+ function sinColores(texto) {
101
+ return texto.replace(/\x1b\[[0-9;]*m/g, "");
102
+ }
103
+ /**
104
+ * Filas de pantalla que ocupa una linea con este ancho.
105
+ *
106
+ * `ceil(largo / cols)` coincide con el ajuste diferido de los terminales: tras
107
+ * escribir exactamente `cols` caracteres el cursor sigue en esa fila esperando,
108
+ * y eso es una fila, no dos.
109
+ */
110
+ function filasFisicas(linea, cols) {
111
+ return sinColores(linea)
112
+ .split("\n")
113
+ .reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
114
+ }
115
+ function lineasDelBloque() {
116
+ const lineas = [];
117
+ if (colaSinSalto)
118
+ lineas.push(colaSinSalto);
119
+ for (const nombre of ORDEN) {
120
+ const trozo = secciones.get(nombre);
121
+ if (trozo && trozo.length > 0)
122
+ lineas.push(...trozo);
123
+ }
124
+ return lineas;
125
+ }
126
+ /** Secuencia que deja el cursor donde empezaba el bloque, con todo borrado debajo. */
127
+ function borrado() {
128
+ if (filasPintadas === 0)
129
+ return "";
130
+ const subir = filasPintadas > 1 ? `\x1b[${filasPintadas - 1}A` : "";
131
+ return `\r${subir}\x1b[0J`;
132
+ }
133
+ /** Secuencia del bloque nuevo. Actualiza `filasPintadas`. */
134
+ function pintado() {
135
+ let lineas = lineasDelBloque();
136
+ if (lineas.length === 0) {
137
+ filasPintadas = 0;
138
+ return "";
139
+ }
140
+ const cols = ancho();
141
+ // Un pie mas alto que la ventana no se puede repintar: al pintarlo el
142
+ // terminal desplaza, y la cuenta de filas -que es lo unico que sabemos para
143
+ // borrarlo- deja de valer. Se recorta por abajo, que es donde esta lo menos
144
+ // urgente; la cola sin salto va primera y nunca se cae.
145
+ const tope = alto() - 1;
146
+ let filas = lineas.reduce((n, l) => n + filasFisicas(l, cols), 0);
147
+ while (filas > tope && lineas.length > 1) {
148
+ lineas = lineas.slice(0, -1);
149
+ filas = lineas.reduce((n, l) => n + filasFisicas(l, cols), 0);
150
+ }
151
+ filasPintadas = filas;
152
+ return lineas.join("\n");
153
+ }
154
+ /**
155
+ * El unico sitio que escribe. Borra lo de antes, suelta el texto permanente en
156
+ * medio, y vuelve a pintar el pie debajo. Todo en UNA escritura: un pie pintado
157
+ * a trozos es un pie que parpadea.
158
+ */
159
+ function repintar(permanente) {
160
+ const fuera = borrado();
161
+ // Sin secciones ya no hay de que proteger la linea a medias, y retenerla seria
162
+ // peor que soltarla: el pie seguiria contando esa fila como suya, y el
163
+ // siguiente repintado borraria lo que hubiera escrito cualquier otro por su
164
+ // cuenta -las herramientas imprimen con console.log y no pasan por aqui-.
165
+ // Soltandola, `filasPintadas` vuelve a cero y el pie no debe nada a nadie.
166
+ let suelto = "";
167
+ if (secciones.size === 0 && colaSinSalto) {
168
+ suelto = colaSinSalto;
169
+ colaSinSalto = "";
170
+ }
171
+ let dentro = pintado();
172
+ const texto = permanente + suelto;
173
+ if (texto)
174
+ aMediaLinea = !texto.endsWith("\n");
175
+ if (dentro && aMediaLinea) {
176
+ // El salto va DELANTE del bloque, y no cuenta como fila suya: el borrado
177
+ // sube hasta la primera fila del pie, no hasta la frase de encima.
178
+ dentro = `\n${dentro}`;
179
+ aMediaLinea = false;
180
+ }
181
+ let salida = fuera + texto + dentro;
182
+ if (dentro && !cursorEscondido) {
183
+ // Con el pie repintandose cada 120 ms, un cursor visible salta de sitio sin
184
+ // parar. Se esconde mientras dure y se devuelve al soltarlo.
185
+ salida = OCULTAR_CURSOR + salida;
186
+ cursorEscondido = true;
187
+ }
188
+ else if (!dentro && cursorEscondido) {
189
+ salida += MOSTRAR_CURSOR;
190
+ cursorEscondido = false;
191
+ }
192
+ if (salida)
193
+ dibujar(salida);
194
+ }
195
+ /** ¿Hay algo pintado abajo que haya que respetar? */
196
+ export function hayPie() {
197
+ return filasPintadas > 0;
198
+ }
199
+ function iguales(a, b) {
200
+ if (!a || a.length !== b.length)
201
+ return false;
202
+ return a.every((l, i) => l === b[i]);
203
+ }
204
+ /** Guarda la seccion. Devuelve si algo cambio, que es lo que decide repintar. */
205
+ function anotar(nombre, lineas) {
206
+ if (lineas.length === 0) {
207
+ return secciones.delete(nombre);
208
+ }
209
+ if (iguales(secciones.get(nombre), lineas))
210
+ return false;
211
+ secciones.set(nombre, lineas);
212
+ return true;
213
+ }
214
+ /**
215
+ * Pone (o reemplaza) una seccion del pie y repinta.
216
+ *
217
+ * Con la lista vacia la seccion desaparece, que es lo mismo que quitarla. Si el
218
+ * contenido es identico al que ya habia, no se repinta: el spinner llama ocho
219
+ * veces por segundo y la barra de escribir casi nunca cambia entre dos.
220
+ */
221
+ export function fijarSeccion(nombre, lineas) {
222
+ if (!pieUsable())
223
+ return;
224
+ if (anotar(nombre, lineas))
225
+ repintar("");
226
+ }
227
+ /**
228
+ * Varias secciones de una vez, con UN solo repintado.
229
+ *
230
+ * El spinner cambia su linea y la barra de escribir en el mismo tick. Puestas
231
+ * por separado se pintaba el bloque dos veces cada 120 ms, y la segunda pasada
232
+ * borraba y reescribia lo que la primera acababa de dejar: el doble de bytes
233
+ * para el mismo resultado, y un parpadeo que se nota en un terminal lento.
234
+ */
235
+ export function fijarSecciones(entradas) {
236
+ if (!pieUsable())
237
+ return;
238
+ let cambio = false;
239
+ for (const [nombre, lineas] of entradas) {
240
+ if (anotar(nombre, lineas))
241
+ cambio = true;
242
+ }
243
+ if (cambio)
244
+ repintar("");
245
+ }
246
+ export function quitarSeccion(nombre) {
247
+ fijarSeccion(nombre, []);
248
+ }
249
+ /**
250
+ * Escribe texto permanente sin que el pie se lo lleve por delante.
251
+ *
252
+ * Sin pie es un write normal y no cuesta nada; por eso puede sustituir a
253
+ * `console.log` en el camino del agente sin pensarlo dos veces.
254
+ */
255
+ export function escribirEncimaDelPie(texto) {
256
+ if (!texto)
257
+ return;
258
+ if (!pieUsable() || (filasPintadas === 0 && !colaSinSalto)) {
259
+ dibujar(texto);
260
+ return;
261
+ }
262
+ const completo = colaSinSalto + texto;
263
+ const corte = completo.lastIndexOf("\n");
264
+ const permanente = corte >= 0 ? completo.slice(0, corte + 1) : "";
265
+ colaSinSalto = corte >= 0 ? completo.slice(corte + 1) : completo;
266
+ repintar(permanente);
267
+ }
268
+ /**
269
+ * Quita el pie entero y deja el terminal como si nunca hubiera estado.
270
+ *
271
+ * Lo que quede a medias de linea se conserva en pantalla: es texto del agente,
272
+ * no adorno, y borrarlo seria perder salida.
273
+ */
274
+ export function soltarPie() {
275
+ if (secciones.size === 0 && filasPintadas === 0)
276
+ return;
277
+ secciones.clear();
278
+ repintar("");
279
+ }
280
+ /**
281
+ * Olvida el estado sin tocar la pantalla.
282
+ *
283
+ * Para las pruebas y para la salida del proceso: ahi no queda terminal al que
284
+ * repintar, pero el cursor escondido si sobrevive y hay que devolverlo.
285
+ */
286
+ export function reiniciarPie() {
287
+ secciones.clear();
288
+ colaSinSalto = "";
289
+ filasPintadas = 0;
290
+ aMediaLinea = false;
291
+ if (cursorEscondido) {
292
+ dibujar(MOSTRAR_CURSOR);
293
+ cursorEscondido = false;
294
+ }
295
+ }
296
+ /**
297
+ * Un cursor escondido sobrevive al proceso: la shell de despues se queda sin
298
+ * cursor y el usuario no tiene forma de saber por que. Se devuelve pase lo que
299
+ * pase, tambien si el programa se cae.
300
+ */
301
+ process.on("exit", () => {
302
+ if (cursorEscondido) {
303
+ try {
304
+ process.stdout.write(MOSTRAR_CURSOR);
305
+ }
306
+ catch { }
307
+ cursorEscondido = false;
308
+ }
309
+ });
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Imprime sin romper el prompt. Uselo en vez de `console.log` para todo lo que
3
+ * pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
4
+ */
5
+ export declare function imprimirSobreElPrompt(texto: string): void;
1
6
  export interface SlashCommandItem {
2
7
  name: string;
3
8
  description: string;
package/dist/ui/prompt.js CHANGED
@@ -6,8 +6,45 @@ import os from "node:os";
6
6
  import chalk from "chalk";
7
7
  import { cycleMode, describeMode, getMode } from "./modes.js";
8
8
  import { releaseKeyboard, tomarTecladoParaPrompt, devolverTecladoDelPrompt } from "./interrupt.js";
9
- import { repararTeclado, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
9
+ import { repararTeclado, reafirmarModoCrudo, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
10
10
  import { clearTyped } from "./typeAhead.js";
11
+ import { dibujar } from "./salida.js";
12
+ import { escribirEncimaDelPie, reiniciarPie } from "./pieFijo.js";
13
+ import { esGrande, guardarPegado, expandirPegados, marcaQueTerminaEn, limpiarPegados } from "./pegados.js";
14
+ /**
15
+ * Escribir algo mientras el prompt esta dibujado.
16
+ *
17
+ * Sale de una sesion real del usuario. Los servidores MCP se conectan en
18
+ * segundo plano para no retrasar el arranque, y cuando terminan sueltan su
19
+ * resumen con `console.log`. Si para entonces el prompt ya estaba en pantalla,
20
+ * el resumen se escribe ENCIMA del marco y lo destroza:
21
+ *
22
+ * ❯ Escribe una instruccion...
23
+ * 🔌 Servidores MCP:────────────────────────────────────
24
+ * ✔ ide (http): 4 herramientasb modo · esc cancelar ⇥ modo normal
25
+ *
26
+ * Ese "4 herramientasb modo" es el resumen pisando la linea de ayuda. Le pasa a
27
+ * cualquier cosa que se imprima tarde: el aviso de actualizacion, un servidor
28
+ * MCP lento, un hook que tarda.
29
+ *
30
+ * La regla es sencilla: si hay un prompt abierto, primero se borra su marco, se
31
+ * escribe el mensaje donde estaba, y se vuelve a dibujar debajo. Si no lo hay,
32
+ * es un console.log normal y ya.
33
+ */
34
+ let escribirEncimaDelPrompt = null;
35
+ /**
36
+ * Imprime sin romper el prompt. Uselo en vez de `console.log` para todo lo que
37
+ * pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
38
+ */
39
+ export function imprimirSobreElPrompt(texto) {
40
+ if (escribirEncimaDelPrompt) {
41
+ escribirEncimaDelPrompt(texto);
42
+ return;
43
+ }
44
+ // Sin prompt abierto puede haber pie: el spinner y la linea de razonamiento
45
+ // viven ahi. Un aviso que llegue tarde tiene que pasar por encima igual.
46
+ escribirEncimaDelPie(texto.endsWith("\n") ? texto : `${texto}\n`);
47
+ }
11
48
  export const SLASH_COMMANDS = [
12
49
  { name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
13
50
  { name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
@@ -251,12 +288,19 @@ export function buildPromptFrame(opts) {
251
288
  }
252
289
  export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.cwd()) {
253
290
  return new Promise((resolve) => {
291
+ // A partir de aqui el terminal es del prompt, que lleva su propia cuenta de
292
+ // filas. El pie fijo tiene que olvidar la suya SIN borrar nada: lo que dejo
293
+ // en pantalla es respuesta del agente, no adorno. Y devuelve el cursor, que
294
+ // lo escondia mientras se repintaba.
295
+ reiniciarPie();
254
296
  let inputBuffer = "";
255
297
  let cursorIndex = 0;
256
298
  let selectedIndex = 0;
257
299
  let isFinished = false;
258
300
  // Estado del pegado. Ver la nota PEGADO DE VARIAS LINEAS de arriba.
259
301
  let isPasting = false;
302
+ /** Lo que va llegando del pegado, antes de decidir si cabe o va como marca. */
303
+ let loPegado = "";
260
304
  let pendingSubmit = null;
261
305
  let lastWasCR = false;
262
306
  // Filas FISICAS, no elementos del array: es la unica cuenta con la que el
@@ -284,9 +328,10 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
284
328
  // Lo que quedara a medio escribir del turno anterior no es de este prompt.
285
329
  clearTyped();
286
330
  const wasRaw = process.stdin.isRaw;
287
- if (process.stdin.setRawMode) {
288
- process.stdin.setRawMode(true);
289
- }
331
+ // Reafirmar y no solo poner: si algo cambio el modo de la consola por fuera
332
+ // de Node, un setRawMode(true) a secas es un no-op y el prompt se abre sin
333
+ // teclado sin enterarse. Ver reafirmarModoCrudo().
334
+ reafirmarModoCrudo(process.stdin);
290
335
  process.stdin.resume();
291
336
  // A partir de aqui el teclado es de este prompt: un vigilante que se pare
292
337
  // tarde ya no puede devolver el modo crudo a como estaba antes del turno,
@@ -318,7 +363,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
318
363
  // Se pide el pegado entre corchetes solo si hay terminal delante; a un
319
364
  // fichero o a una tuberia esto seria basura en la salida.
320
365
  if (process.stdout.isTTY) {
321
- process.stdout.write(PASTE_START_REQUEST);
366
+ dibujar(PASTE_START_REQUEST);
322
367
  }
323
368
  function getAtMention() {
324
369
  const textBeforeCursor = inputBuffer.slice(0, cursorIndex);
@@ -401,12 +446,14 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
401
446
  out.push("\r");
402
447
  if (frame.cursorCol > 0)
403
448
  out.push(`\x1b[${frame.cursorCol}C`);
404
- process.stdout.write(out.join(""));
449
+ dibujar(out.join(""));
405
450
  hasRendered = true;
406
451
  lastCursorRow = frame.cursorRow;
407
452
  }
408
453
  function cleanup() {
409
454
  isFinished = true;
455
+ limpiarPegados();
456
+ escribirEncimaDelPrompt = null;
410
457
  clearInterval(vigilanteTeclado);
411
458
  devolverTecladoDelPrompt();
412
459
  if (pendingSubmit) {
@@ -418,7 +465,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
418
465
  process.stdin.setRawMode(wasRaw || false);
419
466
  }
420
467
  if (process.stdout.isTTY) {
421
- process.stdout.write(PASTE_STOP_REQUEST);
468
+ dibujar(PASTE_STOP_REQUEST);
422
469
  }
423
470
  }
424
471
  /** Mete texto donde esta el cursor. Un solo sitio para no repetir el corte del buffer. */
@@ -466,7 +513,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
466
513
  out.push("\r\x1b[0J");
467
514
  }
468
515
  out.push(`${bar}\n${symbolStr}${chalk.bold.white(submittedText)}\n${bar}\n\n`);
469
- process.stdout.write(out.join(""));
516
+ dibujar(out.join(""));
470
517
  hasRendered = false;
471
518
  lastCursorRow = 0;
472
519
  }
@@ -474,10 +521,14 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
474
521
  if (isFinished)
475
522
  return;
476
523
  pendingSubmit = null;
524
+ // El historial guarda la marca, no las 347 lineas: si no, el fichero de
525
+ // historial crece sin control y recorrerlo con las flechas es inutil.
477
526
  appendHistory(inputBuffer);
527
+ // Pero el modelo recibe el texto entero, que es lo que el usuario pego.
528
+ const completo = expandirPegados(inputBuffer);
478
529
  cleanup();
479
530
  finalizePromptOnSubmit(inputBuffer);
480
- resolve(inputBuffer);
531
+ resolve(completo);
481
532
  }
482
533
  /**
483
534
  * Apunta el envio para el final del tick. Todo el chunk de stdin se emite
@@ -505,10 +556,17 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
505
556
  const sequence = (key && key.sequence) || str || "";
506
557
  if ((key && key.name === "paste-start") || sequence === PASTE_START) {
507
558
  isPasting = true;
559
+ // Se recoge aparte: si resulta ser grande, en el recuadro va una marca
560
+ // corta en vez de las 347 lineas. Ver pegados.ts.
561
+ loPegado = "";
508
562
  return;
509
563
  }
510
564
  if ((key && key.name === "paste-end") || sequence === PASTE_END) {
511
565
  isPasting = false;
566
+ if (loPegado) {
567
+ insertText(esGrande(loPegado) ? guardarPegado(loPegado) : loPegado);
568
+ loPegado = "";
569
+ }
512
570
  render();
513
571
  return;
514
572
  }
@@ -526,10 +584,9 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
526
584
  const items = getDropdownItems();
527
585
  // Enter key
528
586
  if (isEnter) {
529
- // Dentro de un pegado, un salto de linea es texto y no envia nada.
587
+ // Dentro de un pegado, el salto va al buffer aparte, no al recuadro.
530
588
  if (isPasting) {
531
- insertText("\n");
532
- render();
589
+ loPegado += "\n";
533
590
  return;
534
591
  }
535
592
  // Segundo Enter del mismo tick: linea en blanco dentro del pegado.
@@ -629,8 +686,12 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
629
686
  // Backspace
630
687
  if (key && (key.name === "backspace" || (key.ctrl && key.name === "h"))) {
631
688
  if (cursorIndex > 0) {
632
- inputBuffer = inputBuffer.slice(0, cursorIndex - 1) + inputBuffer.slice(cursorIndex);
633
- cursorIndex--;
689
+ // Lo que ocupa una linea se borra como una linea: sin esto habria que
690
+ // pulsar borrar treinta veces para quitar una marca de pegado.
691
+ const marca = marcaQueTerminaEn(inputBuffer, cursorIndex);
692
+ const desde = marca ? marca.inicio : cursorIndex - 1;
693
+ inputBuffer = inputBuffer.slice(0, desde) + inputBuffer.slice(cursorIndex);
694
+ cursorIndex = desde;
634
695
  selectedIndex = 0;
635
696
  render();
636
697
  }
@@ -674,12 +735,44 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
674
735
  if (str.charCodeAt(0) === 0x1b)
675
736
  return;
676
737
  const cleanStr = str.replace(/[\r\n]/g, "");
677
- if (cleanStr.length > 0) {
678
- insertText(cleanStr);
738
+ if (cleanStr.length === 0)
739
+ return;
740
+ if (isPasting) {
741
+ // Va al buffer del pegado, no al recuadro: al cerrarse se decide si
742
+ // cabe entero o si merece una marca corta.
743
+ loPegado += cleanStr;
744
+ return;
745
+ }
746
+ // Sin marcas de pegado -conhost antiguo, algun terminal raro- el pegado
747
+ // llega como un chunk enorme en una sola pulsacion. Tecleando nunca
748
+ // llega mas de un caracter, asi que esto solo puede ser un pegado.
749
+ if (esGrande(cleanStr)) {
750
+ insertText(guardarPegado(cleanStr));
679
751
  render();
752
+ return;
680
753
  }
754
+ insertText(cleanStr);
755
+ render();
681
756
  }
682
757
  }
758
+ // A partir de aqui, lo que llegue tarde se dibuja SIN romper el marco.
759
+ escribirEncimaDelPrompt = (texto) => {
760
+ if (isFinished)
761
+ return;
762
+ let salida = "";
763
+ // Se borra el marco igual que hace render() al repintar.
764
+ if (hasRendered) {
765
+ if (lastCursorRow > 0)
766
+ salida += `\x1b[${lastCursorRow}A`;
767
+ salida += "\r\x1b[0J";
768
+ }
769
+ salida += texto.endsWith("\n") ? texto : `${texto}\n`;
770
+ dibujar(salida);
771
+ // Y el prompt se vuelve a dibujar limpio debajo del mensaje.
772
+ hasRendered = false;
773
+ lastCursorRow = 0;
774
+ render();
775
+ };
683
776
  process.stdin.on("keypress", onKeypress);
684
777
  render();
685
778
  });