chocolatito-code 1.6.6 → 1.6.7

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.
package/dist/index.js CHANGED
@@ -15,8 +15,8 @@ import { codigoDeSalida, EXIT_OK, EXIT_FALLO, EXIT_USO, EXIT_LICENCIA } from "./
15
15
  import { showBanner, animateStartupBanner, showHelp, showEffortMenu } from "./ui/banner.js";
16
16
  import { fijarMotor } from "./ui/motorActual.js";
17
17
  import { elegirDeLista } from "./ui/selector.js";
18
- import { entrarEnPantallaFija, pantallaFijaPedida } from "./ui/pantalla.js";
19
- import { montarArbol } from "./ui/ink/montarApp.js";
18
+ import { entrarEnPantallaFija, pantallaFijaPedida, salirDePantallaFija } from "./ui/pantalla.js";
19
+ import { montarArbol, desmontarArbol } from "./ui/ink/montarApp.js";
20
20
  import { reafirmarModoCrudo } from "./ui/keyboardGuard.js";
21
21
  import { askInteractivePrompt, SLASH_COMMANDS } from "./ui/prompt.js";
22
22
  import { changeDir } from "./tools/changeDir.js";
@@ -89,6 +89,7 @@ function handleFlags(argv) {
89
89
  "CONFIGURACION",
90
90
  " API key: variable CHOCOLATITO_API_KEY, un .env del proyecto o ~/.chocolatitorc.",
91
91
  " Reglas del proyecto: CHOCOLATITO.md. Permisos y hooks: .chocolatito/settings.json.",
92
+ " Interfaz clasica (sin la caja pegada abajo): variable CHOCOLATITO_PANTALLA=normal.",
92
93
  "",
93
94
  "CODIGOS DE SALIDA (modo orden unica)",
94
95
  " 0 La tarea se completo.",
@@ -136,9 +137,29 @@ function instalarRedDeSeguridad() {
136
137
  });
137
138
  // Una excepcion no capturada si es terminal, pero al menos se sale con la
138
139
  // terminal usable y con un mensaje legible en vez de una traza cruda.
140
+ //
141
+ // NO SE USA `console.log` AQUI, Y ES A PROPOSITO
142
+ //
143
+ // Con el arbol montado la consola esta interceptada: un `console.log` va a la
144
+ // transcripcion, que la pinta el arbol... en el proximo dibujado, que ya no
145
+ // llega porque la linea siguiente cierra el proceso. El mensaje se escribe en
146
+ // un sitio que nadie va a leer nunca.
147
+ //
148
+ // Es exactamente el fallo que impidio diagnosticar la pantalla en negro dos
149
+ // veces: el programa se cerraba "sin decir nada" y si lo decia, pero a una
150
+ // pantalla que se iba con el. Aqui se suelta el arbol primero -que devuelve la
151
+ // consola de verdad- y se escribe directo a stderr, que no lo intercepta nadie.
139
152
  process.on("uncaughtException", (err) => {
140
153
  restaurarTerminal();
141
- console.log(chalk.red(`\n✖ Error inesperado: ${err?.message || String(err)}\n`) +
154
+ try {
155
+ desmontarArbol();
156
+ }
157
+ catch { }
158
+ try {
159
+ salirDePantallaFija();
160
+ }
161
+ catch { }
162
+ process.stderr.write(chalk.red(`\n✖ Error inesperado: ${err?.message || String(err)}\n`) +
142
163
  chalk.gray(`${(err?.stack || "").split("\n").slice(1, 4).join("\n")}\n`));
143
164
  process.exit(EXIT_FALLO);
144
165
  });
@@ -190,8 +211,8 @@ async function main() {
190
211
  // errores raros dentro de una dependencia. Ver config/nodeVersion.ts.
191
212
  const nodeViejo = comprobarNode();
192
213
  if (nodeViejo) {
193
- console.error(chalk.red(`
194
- ${nodeViejo}
214
+ console.error(chalk.red(`
215
+ ${nodeViejo}
195
216
  `));
196
217
  process.exit(EXIT_USO);
197
218
  }
@@ -237,16 +258,6 @@ ${nodeViejo}
237
258
  const hookManager = new HookManager(context.cwd);
238
259
  const sessionManager = new SessionManager();
239
260
  const sessionId = `session_${Date.now()}`;
240
- // La barra de abajo solo puede quedarse clavada si el programa toma la
241
- // pantalla entera. Se pide con CHOCOLATITO_PANTALLA=fija y se paga con el
242
- // historial del terminal: ver ui/pantalla.ts.
243
- if (pantallaFijaPedida()) {
244
- entrarEnPantallaFija();
245
- // Entrar y empezar a dibujar son la misma decision: entrar sin montar el
246
- // arbol deja una consola en blanco de la que solo se sale cerrando la
247
- // ventana.
248
- montarArbol();
249
- }
250
261
  let currentModel = DEFAULT_ESPECTRO;
251
262
  // La barra de estado lo lee de aqui. Fijarlo al arrancar y no solo al
252
263
  // cambiarlo: si no, hasta el primer /effort la barra no dice nada.
@@ -350,6 +361,27 @@ ${nodeViejo}
350
361
  chalk.gray(" shift+tab cambia el modo de permisos · puedes escribir mientras trabaja y se encola\n\n"));
351
362
  if (getMode() !== "default")
352
363
  console.log(` ${modeAnnouncement()}\n`);
364
+ // EL ARBOL SE MONTA AQUI Y NO ANTES
365
+ //
366
+ // Va de serie. CHOCOLATITO_PANTALLA=normal devuelve la clasica. Ver ui/pantalla.ts.
367
+ //
368
+ // Estaba montado mucho mas arriba, antes del zorro. Y el zorro se dibuja con
369
+ // animacion: mueve el cursor y repinta. Dos cosas moviendo el cursor a la vez
370
+ // descuadran la cuenta de filas de Ink desde el primer fotograma, y lo que se
371
+ // ve es "sale una barra y lo demas negro".
372
+ //
373
+ // Aqui no queda nada por animar: el zorro ya salio, y lo que venga sale por el
374
+ // arbol o por el parche de console de Ink.
375
+ //
376
+ // Y el arbol solo se monta si se entro de VERDAD: sin terminal -salida a un
377
+ // archivo, un script, CI- no se entra, e Ink sin terminal escribe su marco
378
+ // como texto suelto. La salida del programa saldria con la caja de escribir
379
+ // metida dentro.
380
+ if (pantallaFijaPedida() && entrarEnPantallaFija()) {
381
+ // Entrar y empezar a dibujar son la misma decision: desviar el dibujo sin
382
+ // que haya nadie pintandolo deja la consola muda.
383
+ montarArbol();
384
+ }
353
385
  // Lo que el usuario escribio mientras el agente trabajaba entra como siguiente
354
386
  // orden, sin tener que volver a teclearlo.
355
387
  let pendiente = null;
@@ -421,15 +453,15 @@ ${nodeViejo}
421
453
  .filter((m) => m.role === "user" || m.role === "assistant")
422
454
  .map((m) => `### 👤 ${m.role === "user" ? "Usuario" : "Chocolatito"}\n\n${typeof m.content === "string" ? m.content : JSON.stringify(m.content, null, 2)}\n`)
423
455
  .join("\n---\n\n");
424
- const mdContent = `# 🦊 Bitácora de Sesión — Chocolatito Code
425
- **Fecha:** ${new Date().toLocaleString()}
426
- **Directorio:** \`${agent.currentCwd}\`
427
- **Motor:** ${currentModel.name}
428
- **Inversión Total:** ${agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel).formattedCost}
429
-
430
- ---
431
-
432
- ${sessionData}
456
+ const mdContent = `# 🦊 Bitácora de Sesión — Chocolatito Code
457
+ **Fecha:** ${new Date().toLocaleString()}
458
+ **Directorio:** \`${agent.currentCwd}\`
459
+ **Motor:** ${currentModel.name}
460
+ **Inversión Total:** ${agent.tracker.calculateCost(undefined, undefined, undefined, currentModel.underlyingModel).formattedCost}
461
+
462
+ ---
463
+
464
+ ${sessionData}
433
465
  `;
434
466
  fs.writeFileSync(exportPath, mdContent, "utf-8");
435
467
  console.log(chalk.green(`\n✔ Bitácora exportada exitosamente en: ${chalk.bold.white(exportPath)}\n`));
@@ -523,12 +555,12 @@ ${sessionData}
523
555
  }
524
556
  console.log(chalk.bold.hex("#A855F7")("\n🎯 INICIANDO MODO META AUTÓNOMA (GOAL MODE):"));
525
557
  console.log(chalk.gray(` Objetivo: "${goalText}"\n`));
526
- const autonomousPrompt = `[MODO META AUTÓNOMA ACTIVADO]
527
- Tu objetivo principal es: "${goalText}"
528
- Directivas de ejecución:
529
- 1. Formula un plan de ejecución mental paso a paso.
530
- 2. Ejecuta cada paso utilizando las herramientas correspondientes (creación, edición, ejecución, verificación).
531
- 3. Si algo falla o hay errores, autocorregir inmediatamente y volver a probar.
558
+ const autonomousPrompt = `[MODO META AUTÓNOMA ACTIVADO]
559
+ Tu objetivo principal es: "${goalText}"
560
+ Directivas de ejecución:
561
+ 1. Formula un plan de ejecución mental paso a paso.
562
+ 2. Ejecuta cada paso utilizando las herramientas correspondientes (creación, edición, ejecución, verificación).
563
+ 3. Si algo falla o hay errores, autocorregir inmediatamente y volver a probar.
532
564
  4. No te detengas hasta verificar que el objetivo esté 100% completado con éxito.`;
533
565
  renderUserPrompt(`/goal ${goalText}`);
534
566
  await agent.runTask(autonomousPrompt);
@@ -632,12 +664,12 @@ Directivas de ejecución:
632
664
  const refreshedContext = await getProjectContext(agent.currentCwd);
633
665
  const updatedPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
634
666
  agent.setEspectroModel(currentModel, updatedPrompt);
635
- console.log(chalk.green(`
667
+ console.log(chalk.green(`
636
668
  ✔ Nivel actualizado a `) +
637
669
  chalk.bold.hex("#A855F7")(currentModel.name) +
638
670
  chalk.gray(` (${currentModel.effortLevel})`) +
639
- `
640
- ${chalk.italic.gray(currentModel.description)}
671
+ `
672
+ ${chalk.italic.gray(currentModel.description)}
641
673
  `);
642
674
  // EL TECLADO SE QUEDABA TRABADO AL CAMBIAR DE MOTOR
643
675
  //
@@ -1,25 +1,2 @@
1
1
  import React from "react";
2
- /**
3
- * EL ÁRBOL ÚNICO
4
- *
5
- * POR QUE EXISTE
6
- *
7
- * Antes Ink solo era dueño del prompt: se montaba para pedir una orden y se
8
- * desmontaba al enviarla. Mientras el agente trabajaba, el terminal volvía a ser
9
- * de siempre y la barra de abajo la pintaba `pieFijo` borrando y repintando a
10
- * mano.
11
- *
12
- * Eso son DOS dueños del mismo trozo de pantalla turnándose, y de ahí salían
13
- * casi todas las quejas: la barra que sube con el scroll, el hueco que aparece,
14
- * el marco que se descuadra. Cada arreglo era coordinar mejor a los dos dueños.
15
- *
16
- * Aquí hay uno solo, y vive toda la sesión. Lo que sale del agente entra en el
17
- * historial (historial.ts) y se pinta arriba; abajo va SIEMPRE la barra, sea la
18
- * de escribir o la de trabajar. Nadie tiene que ponerse de acuerdo con nadie.
19
- *
20
- * Y POR ESO LA BARRA SE QUEDA QUIETA
21
- *
22
- * No porque se repinte rápido, sino porque es la última fila de un árbol que
23
- * ocupa la ventana entera. No hay nada que pueda moverla, ni la rueda del ratón.
24
- */
25
2
  export declare function App(): React.ReactElement;
@@ -1,79 +1,108 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
- import { Box, Text, useStdout } from "ink";
3
+ import { Box, Static, Text, useApp, useStdout } from "ink";
4
4
  import { Prompt } from "./Prompt.js";
5
- import { alEscribir, tope, ventana } from "../historial.js";
5
+ import { colgando, pendientes, recortarCola } from "./transcripcion.js";
6
+ import { alCambiar } from "./transcripcion.js";
6
7
  import { cuandoCambieElPie, lineasDelPie } from "../pieFijo.js";
7
8
  import { alCambiarElTurno, estadoDelTurno } from "./control.js";
9
+ import { registrarPrestamo } from "./prestamo.js";
10
+ import { useTecladoSiempreNuestro } from "./teclado.js";
8
11
  /**
9
- * EL ÁRBOL ÚNICO
12
+ * EL ÁRBOL ÚNICO, EN LA PANTALLA DE SIEMPRE
10
13
  *
11
- * POR QUE EXISTE
14
+ * DE DÓNDE VIENE ESTE ARCHIVO
12
15
  *
13
- * Antes Ink solo era dueño del prompt: se montaba para pedir una orden y se
14
- * desmontaba al enviarla. Mientras el agente trabajaba, el terminal volvía a ser
15
- * de siempre y la barra de abajo la pintaba `pieFijo` borrando y repintando a
16
- * mano.
16
+ * La versión anterior tomaba la pantalla alternativa —la de vim o htop— para
17
+ * clavar la barra de abajo. Funcionaba en el emulador y se publicó rota dos
18
+ * veces. Y sobre todo: no era lo que hace Claude Code, que era lo que se pedía.
17
19
  *
18
- * Eso son DOS dueños del mismo trozo de pantalla turnándose, y de ahí salían
19
- * casi todas las quejas: la barra que sube con el scroll, el hueco que aparece,
20
- * el marco que se descuadra. Cada arreglo era coordinar mejor a los dos dueños.
20
+ * Claude Code renderiza en el buffer NORMAL. La conversación se emite una sola
21
+ * vez con `<Static>` y pasa al historial del terminal; debajo queda una zona que
22
+ * se repinta con el spinner y la caja de escribir. Por eso allí la rueda del
23
+ * ratón funciona y se puede releer la sesión entera.
21
24
  *
22
- * Aquí hay uno solo, y vive toda la sesión. Lo que sale del agente entra en el
23
- * historial (historial.ts) y se pinta arriba; abajo va SIEMPRE la barra, sea la
24
- * de escribir o la de trabajar. Nadie tiene que ponerse de acuerdo con nadie.
25
+ * LO QUE ESO SIGNIFICA, DICHO CLARO
25
26
  *
26
- * Y POR ESO LA BARRA SE QUEDA QUIETA
27
+ * La barra está siempre pegada al final de lo que ha salido, y cualquier línea
28
+ * nueva te devuelve ahí. Pero si subes con la rueda, la barra sube con el resto:
29
+ * es texto del terminal, no una capa encima. En Claude Code pasa exactamente lo
30
+ * mismo. Clavarla de verdad solo se consigue tomando la pantalla entera, y el
31
+ * precio es quedarse sin historial ni rueda del ratón.
27
32
  *
28
- * No porque se repinte rápido, sino porque es la última fila de un árbol que
29
- * ocupa la ventana entera. No hay nada que pueda moverla, ni la rueda del ratón.
33
+ * LAS TRES ZONAS
34
+ *
35
+ * <Static> la conversación, escrita una vez y para siempre
36
+ * cola el trozo de línea que el modelo está escribiendo ahora mismo
37
+ * barra el spinner, o la caja de escribir
38
+ *
39
+ * Las dos últimas se repintan; la primera no puede.
40
+ */
41
+ /** Filas que se reservan para la caja cuando el prompt está puesto. */
42
+ const ALTO_DE_LA_CAJA = 6;
43
+ /**
44
+ * UNA LÍNEA VACÍA TIENE QUE OCUPAR SU FILA
45
+ *
46
+ * Un `<Text>` sin contenido mide cero y desaparece. Y las líneas en blanco de la
47
+ * respuesta no son adorno: son lo que separa un párrafo del siguiente, y lo que
48
+ * deja aire entre el mensaje que mandaste y lo que contesta el modelo.
49
+ *
50
+ * Sin esto, la conversación sale apelmazada: los párrafos pegados unos a otros y
51
+ * la lista de herramientas encima del texto sin un respiro. Se vio al mirar la
52
+ * pantalla emulada; ninguna prueba lo cazaba, porque todas comprobaban qué
53
+ * líneas salen y ninguna cuántas.
30
54
  */
55
+ function conCuerpo(texto) {
56
+ return texto.length > 0 ? texto : " ";
57
+ }
31
58
  export function App() {
32
59
  const { stdout } = useStdout();
33
- // UNA FILA MENOS QUE LA VENTANA, Y ES LA CLAVE
34
- //
35
- // Ink escribe su arbol y detras un salto de linea. Si el arbol mide justo lo
36
- // que la ventana, ese salto la desborda: el terminal desplaza una fila, y como
37
- // Ink repinta subiendo el numero de filas que escribio, a partir de ahi borra
38
- // una fila que ya no es la suya. En la pantalla alternativa, que no tiene
39
- // historial al que recurrir, eso degenera en pantalla en negro.
40
- //
41
- // Es lo que se publico roto en la 1.6.0: "esta fallando, no sale nada". Las
42
- // pruebas no lo cazan porque el terminal de mentira de la libreria no desplaza.
43
- const filas = Math.max(6, (stdout?.rows || 24) - 1);
60
+ const { suspendTerminal } = useApp();
44
61
  const [, setRedibujar] = useState(0);
45
- const [subido, setSubido] = useState(0);
46
62
  const [turno, setTurno] = useState(estadoDelTurno());
47
63
  const repintar = () => setRedibujar((n) => n + 1);
48
- // Tres fuentes de cambio, todas fuera de React: lo que escribe el agente, lo
49
- // que publica el spinner, y el paso de "esperando orden" a "trabajando".
50
- useEffect(() => alEscribir(repintar), []);
64
+ // Tres fuentes de cambio, las tres fuera de React: lo que escribe el agente,
65
+ // lo que publica el spinner, y el paso de "esperando orden" a "trabajando".
66
+ useEffect(() => alCambiar(repintar), []);
51
67
  useEffect(() => cuandoCambieElPie(repintar), []);
52
68
  useEffect(() => alCambiarElTurno((nuevo) => setTurno(nuevo)), []);
53
- // Al empezar un turno se vuelve al final: lo interesante es lo que va a salir,
54
- // no donde estabas mirando.
69
+ // El diálogo de permisos y los menús necesitan el teclado para ellos solos, y
70
+ // se abren con el árbol vivo. Aquí se deja apuntado el préstamo que Ink ya
71
+ // trae para eso; ver ink/prestamo.ts.
55
72
  useEffect(() => {
56
- if (turno.esperando)
57
- return;
58
- setSubido(0);
59
- }, [turno.esperando]);
60
- const barraDeTrabajo = turno.esperando ? [] : lineasDelPie();
61
- const altoBarra = turno.esperando ? 0 : barraDeTrabajo.length;
62
- const altoAviso = subido > 0 ? 1 : 0;
63
- // Cuando espera orden, el hueco de abajo lo ocupa el prompt y su alto lo
64
- // decide él; se le pasa cuánto sitio hay para lo de arriba.
65
- const altoDelTexto = Math.max(1, filas - altoBarra - altoAviso - (turno.esperando ? 5 : 0));
66
- const visible = ventana(altoDelTexto, subido);
67
- const huecos = Math.max(0, altoDelTexto - visible.length);
68
- return (_jsxs(Box, { flexDirection: "column", height: filas, children: [Array.from({ length: huecos }, (_, i) => (_jsx(Text, { children: " " }, `hueco-${i}`))), visible.map((linea, i) => (_jsx(Text, { children: linea }, `h-${i}`))), subido > 0 && (_jsx(Text, { dimColor: true, children: ` ↑ ${subido} lineas mas arriba · Av Pag para volver al final` })), turno.esperando ? (_jsx(Prompt, { alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [], sinHistorial: true, alDesplazar: (direccion) => setSubido((n) => {
69
- // El salto sale del alto SIN el aviso de "estas mirando atras".
70
- // Si se contara, subir y bajar usarian bases distintas -el aviso
71
- // solo existe cuando ya has subido- y bajar no deshacia lo que
72
- // acababas de subir: el scroll derivaba solo.
73
- const base = Math.max(1, filas - altoBarra - 5);
74
- const salto = Math.max(1, Math.floor(base / 2));
75
- return direccion > 0
76
- ? Math.min(tope(base), n + salto)
77
- : Math.max(0, n - salto);
78
- }) })) : (barraDeTrabajo.map((linea, i) => _jsx(Text, { children: linea }, `t-${i}`)))] }));
73
+ registrarPrestamo(suspendTerminal);
74
+ return () => registrarPrestamo(null);
75
+ }, [suspendTerminal]);
76
+ // El teclado tiene que seguir siendo de la caja aunque un diálogo, un menú o
77
+ // un proceso hijo lo suelten por el camino. Ver ink/teclado.ts: es el fallo
78
+ // del "no puedo escribir hasta que pulso Enter".
79
+ useTecladoSiempreNuestro(turno.esperando);
80
+ const cols = Math.max(20, stdout?.columns || 80);
81
+ const filas = Math.max(6, stdout?.rows || 24);
82
+ const permanentes = pendientes();
83
+ const barra = turno.esperando ? [] : lineasDelPie();
84
+ // EL TECHO DE LA ZONA DE ABAJO, Y POR QUÉ SE MIDE TAN DE CERCA
85
+ //
86
+ // En Windows basta con que el bloque dinámico MIDA lo que la ventana —no que
87
+ // se pase, que lo iguale— para que Ink cambie de repintado por diferencias a
88
+ // `clearTerminal` + reimprimir toda la conversación acumulada, en cada
89
+ // fotograma (ink.js:100-102). Con el spinner a 30 por segundo eso es la
90
+ // pantalla entera parpadeando y el coste creciendo con la sesión.
91
+ //
92
+ // Así que la zona de abajo se queda SIEMPRE por debajo de las filas de la
93
+ // ventana. Una fila de margen, y quien se pasa es la cola, que se recorta.
94
+ const techo = Math.max(1, filas - 1);
95
+ const sitioParaLaCola = Math.max(0, techo - (turno.esperando ? ALTO_DE_LA_CAJA : barra.length));
96
+ const cola = recortarCola(colgando(), cols, sitioParaLaCola);
97
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: permanentes, children: (linea) => _jsx(Text, { children: conCuerpo(linea.texto) }, linea.id) }), _jsxs(Box, { flexDirection: "column", children: [cola.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, `cola-${i}`))), turno.esperando ? (_jsx(Prompt, { alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [],
98
+ // NO SE DESMONTA EL ÁRBOL AL ENVIAR
99
+ //
100
+ // El prompt nació montándose y desmontándose en cada vuelta, y para
101
+ // eso llamaba a `exit()` de Ink al enviar. Dentro del árbol único ese
102
+ // `exit()` no cierra un prompt: cierra la aplicación ENTERA.
103
+ //
104
+ // Es el fallo que se publicó como "escribo y se cierra": la primera
105
+ // vez que pulsabas Enter el árbol moría, ya no quedaba nadie
106
+ // pintando, y la promesa que esperaba la orden no se resolvía jamás.
107
+ cerrarAlEnviar: false, alto: Math.max(3, techo - cola.length) })) : (barra.map((linea, i) => _jsx(Text, { children: conCuerpo(linea) }, `t-${i}`)))] })] }));
79
108
  }
@@ -39,12 +39,22 @@ export interface PromptProps {
39
39
  /** Mensajes que llegaron tarde y hay que dejar por encima del marco. */
40
40
  avisos: string[];
41
41
  /**
42
- * El historial lo pinta otro (el arbol de App). Aqui solo la barra.
42
+ * ¿Al enviar hay que cerrar el árbol de Ink?
43
43
  *
44
- * Sin esto se pintaria dos veces: una desde App y otra desde aqui.
44
+ * cuando el prompt ES el árbol —se monta para pedir una orden y se
45
+ * desmonta al recibirla— y NO cuando vive dentro del árbol único de App: allí
46
+ * `exit()` no cierra el prompt, cierra la aplicación entera y deja la promesa
47
+ * de la orden colgada para siempre. Ese fue el "escribo y se cierra".
45
48
  */
46
- sinHistorial?: boolean;
47
- /** Re Pag / Av Pag: +1 sube, -1 baja. El salto lo decide quien recibe. */
48
- alDesplazar?: (direccion: number) => void;
49
+ cerrarAlEnviar?: boolean;
50
+ /**
51
+ * Filas de las que puede disponer el marco.
52
+ *
53
+ * App se lo dice porque ahí arriba hay más cosas —el trozo de línea que el
54
+ * modelo está escribiendo— y el bloque de abajo entero tiene que quedarse por
55
+ * debajo del alto de la ventana. Sin esto, el marco se mide contra la ventana
56
+ * como si estuviera solo.
57
+ */
58
+ alto?: number;
49
59
  }
50
- export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHistorial, alDesplazar, }: PromptProps): React.ReactElement;
60
+ export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar, alto, }: PromptProps): React.ReactElement;
@@ -1,15 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { useMemo, useRef, useState } from "react";
3
3
  import { Box, Text, useApp, useInput, useStdout } from "ink";
4
4
  import { buildPromptFrame } from "../marco.js";
5
5
  import { cycleMode } from "../modes.js";
6
6
  import { aplicarOpcion, archivosDelProyecto, leerHistorial, mencionEnCurso, opcionesDelDesplegable, } from "../entrada.js";
7
7
  import { esGrande, guardarPegado, marcaQueTerminaEn } from "../pegados.js";
8
- import { alEscribir, tope, ventana } from "../historial.js";
9
- import { enPantallaFija } from "../pantalla.js";
10
8
  /** Marca del cursor. Ink esconde el de verdad, así que este se dibuja. */
11
9
  const CARET = "█";
12
- export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHistorial = false, alDesplazar, }) {
10
+ /**
11
+ * UNA LÍNEA VACÍA TIENE QUE OCUPAR SU FILA
12
+ *
13
+ * Un `<Text>` sin contenido mide cero y desaparece. Y el marco empieza con dos
14
+ * líneas en blanco a propósito: es el aire entre la conversación y la caja, que
15
+ * se pidió expresamente ("pon más espacio ahí para que se vea exacto y chévere",
16
+ * y después "que sea bien pues").
17
+ *
18
+ * Sin esto el aire se lo come Ink y la caja queda pegada a la última frase. No
19
+ * lo cazaba ninguna prueba: todas miraban QUÉ líneas salen, ninguna CUÁNTAS.
20
+ */
21
+ function conCuerpo(texto) {
22
+ return texto.length > 0 ? texto : " ";
23
+ }
24
+ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar = true, alto, }) {
13
25
  const { stdout } = useStdout();
14
26
  const { exit } = useApp();
15
27
  const [texto, setTexto] = useState("");
@@ -19,18 +31,6 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
19
31
  const [, setRedibujar] = useState(0);
20
32
  const historial = useRef(leerHistorial());
21
33
  const [posHistorial, setPosHistorial] = useState(historial.current.length);
22
- // Cuanto se ha subido con Re Pag. Cero es el fondo, que es donde se esta
23
- // mientras se trabaja. Solo se usa en pantalla fija.
24
- const [subido, setSubido] = useState(0);
25
- const fija = enPantallaFija();
26
- // En pantalla fija el terminal ya no guarda nada: lo que sale lo pintamos
27
- // nosotros, asi que hay que repintar en cada escritura del agente. El
28
- // historial vive fuera de React y React no puede adivinar que ha crecido.
29
- useEffect(() => {
30
- if (!fija)
31
- return;
32
- return alEscribir(() => setRedibujar((n) => n + 1));
33
- }, [fija]);
34
34
  // Se escanea UNA vez por prompt, no en cada tecla: recorrer el proyecto entero
35
35
  // por cada letra se nota en cuanto el proyecto pasa de mediano.
36
36
  const archivos = useMemo(() => {
@@ -44,7 +44,9 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
44
44
  const opciones = opcionesDelDesplegable(texto, cursor, archivos);
45
45
  const indice = opciones.length > 0 ? ((elegido % opciones.length) + opciones.length) % opciones.length : 0;
46
46
  const cols = Math.max(20, stdout?.columns || 80);
47
- const rows = stdout?.rows || 24;
47
+ // El alto que da App ya viene descontado de lo que hay por encima; sin él, la
48
+ // ventana entera.
49
+ const rows = Math.max(6, alto ?? stdout?.rows ?? 24);
48
50
  // El cursor se dibuja: Ink esconde el del terminal.
49
51
  const conCaret = texto.slice(0, cursor) + CARET + texto.slice(cursor);
50
52
  const marco = buildPromptFrame({
@@ -56,51 +58,33 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
56
58
  items: opciones,
57
59
  selectedIndex: indice,
58
60
  });
59
- // Filas que le quedan a la conversacion una vez puesta la barra. En pantalla
60
- // fija es lo que se pinta; en la normal no se usa, que ahi el historial es del
61
- // terminal.
62
- // Cuando se ha subido, una fila se va en el aviso de que se esta mirando
63
- // atras. Sin descontarla, el arbol mide una fila mas que la ventana y el
64
- // terminal desplaza: la barra dejaria de estar clavada justo al usar el scroll,
65
- // que es para lo que existe este modo.
66
- // El alto SIN el aviso de "estas mirando atras". El salto se calcula sobre
67
- // esto y no sobre el alto real: el aviso solo existe cuando ya has subido, asi
68
- // que contarlo hacia que bajar no deshiciera lo que acababas de subir y el
69
- // scroll derivara solo.
70
- const baseDelSalto = Math.max(1, rows - marco.lines.length);
71
- const altoDelTexto = Math.max(1, baseDelSalto - (subido > 0 ? 1 : 0));
61
+ // CERRAR EL ARBOL O NO
62
+ //
63
+ // Dentro del arbol unico, `exit()` de Ink desmonta la aplicacion entera. Ver
64
+ // la nota larga en la prop `cerrarAlEnviar`.
65
+ function cerrar() {
66
+ if (cerrarAlEnviar)
67
+ exit();
68
+ }
72
69
  function enviar() {
73
70
  const orden = texto.trim();
74
71
  if (!orden)
75
72
  return;
76
73
  alEnviar(orden);
77
- exit();
74
+ cerrar();
78
75
  }
79
76
  useInput((entrada, tecla) => {
80
- // ----------------------------------------------- recorrer lo que salio
77
+ // RE PAG Y AV PAG NO SE TOCAN, Y ESO ES LO CORRECTO
81
78
  //
82
- // Solo Re Pag y Av Pag. Las flechas y los caracteres son para escribir:
83
- // robarselos aqui dejaria al usuario sin poder teclear.
84
- if (tecla.pageUp || tecla.pageDown) {
85
- const salto = Math.max(1, Math.floor(baseDelSalto / 2));
86
- // Con el arbol de App al mando se le manda la DIRECCION, no el salto:
87
- // aqui no se sabe cuanto sitio hay arriba, y calcular el salto en los dos
88
- // sitios con alturas distintas hacia que bajar no deshiciera lo que
89
- // acababa de subir. Con la direccion, el salto sale de un solo sitio y las
90
- // dos teclas son simetricas.
91
- if (alDesplazar) {
92
- alDesplazar(tecla.pageUp ? 1 : -1);
93
- return;
94
- }
95
- if (fija) {
96
- setSubido((n) => tecla.pageUp ? Math.min(tope(baseDelSalto), n + salto) : Math.max(0, n - salto));
97
- return;
98
- }
99
- }
79
+ // Las llevaba este componente cuando la conversacion vivia en memoria y la
80
+ // pintabamos nosotros. Ahora la conversacion es del terminal: la rueda del
81
+ // raton, Re Pag y la barra de scroll de la ventana funcionan solas y mejor
82
+ // que cualquier cosa que hicieramos aqui. Interceptarlas seria quitarle al
83
+ // usuario un scroll que ya tiene.
100
84
  // ------------------------------------------------------------- salir
101
85
  if (tecla.ctrl && entrada === "c") {
102
86
  alCancelar();
103
- exit();
87
+ cerrar();
104
88
  return;
105
89
  }
106
90
  // ------------------------------------------- cambiar el modo de permisos
@@ -141,7 +125,7 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
141
125
  if (arroba || soloLaBarra)
142
126
  return;
143
127
  alEnviar(aplicado.texto.trim());
144
- exit();
128
+ cerrar();
145
129
  return;
146
130
  }
147
131
  }
@@ -231,14 +215,5 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
231
215
  setElegido(0);
232
216
  }
233
217
  });
234
- if (!fija || sinHistorial) {
235
- return (_jsxs(Box, { flexDirection: "column", children: [avisos.map((a, i) => (_jsx(Text, { children: a }, `aviso-${i}`))), marco.lines.map((linea, i) => (_jsx(Text, { children: linea }, i)))] }));
236
- }
237
- // PANTALLA FIJA: el arbol ocupa la ventana entera y la barra va la ultima, asi
238
- // que no puede moverse. Lo de arriba se rellena con huecos cuando hay poca
239
- // conversacion; sin eso la barra subiria a media pantalla, que es justo la
240
- // queja que este modo viene a resolver.
241
- const visible = ventana(altoDelTexto, subido);
242
- const huecos = Math.max(0, altoDelTexto - visible.length);
243
- return (_jsxs(Box, { flexDirection: "column", height: rows, children: [Array.from({ length: huecos }, (_, i) => (_jsx(Text, { children: " " }, `hueco-${i}`))), visible.map((linea, i) => (_jsx(Text, { children: linea }, `h-${i}`))), subido > 0 && (_jsx(Text, { dimColor: true, children: ` ↑ ${subido} lineas mas arriba · Av Pag para volver al final` })), marco.lines.map((linea, i) => (_jsx(Text, { children: linea }, `b-${i}`)))] }));
218
+ return (_jsxs(Box, { flexDirection: "column", children: [avisos.map((a, i) => (_jsx(Text, { children: conCuerpo(a) }, `aviso-${i}`))), marco.lines.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, i)))] }));
244
219
  }
@@ -1,2 +1,36 @@
1
1
  export declare function montarArbol(): void;
2
+ /**
3
+ * Suelta el árbol.
4
+ *
5
+ * `darPorPintado()` ANTES de desmontar, y el orden importa: si el árbol se
6
+ * vuelve a montar —un diálogo que necesita el teclado para él solo—, el
7
+ * `<Static>` nuevo empieza su cuenta de cero y reimprimiría la conversación
8
+ * entera. En la pantalla normal esas líneas siguen ahí, en el terminal; la marca
9
+ * dice por dónde iba para que el montaje nuevo solo escriba lo que falta.
10
+ *
11
+ * `clear()` borra el marco de abajo antes de irse. Sin eso queda una copia
12
+ * muerta de la barra en mitad del historial, y en la próxima sesión de scroll el
13
+ * usuario se encuentra dos.
14
+ */
2
15
  export declare function desmontarArbol(): void;
16
+ export declare function hayArbol(): boolean;
17
+ /**
18
+ * Borra la pantalla de verdad. Es lo que hacen /clear, /cd y Ctrl+L.
19
+ *
20
+ * POR QUE NO VALE UN `console.clear()`
21
+ *
22
+ * `console.clear()` escribe la secuencia de borrado directa al terminal, y no
23
+ * pasa por la interceptación de consola: ahí solo van `log`, `error`, `warn` e
24
+ * `info`. Con el árbol montado eso borra filas que Ink cree suyas, y a partir de
25
+ * ahí Ink borra y repinta sobre un sitio que ya no existe. Lo que se ve es
26
+ * basura, o nada.
27
+ *
28
+ * Aquí se hace en el orden que no rompe nada: se borra el marco con la cuenta
29
+ * todavía buena, se suelta el árbol, se limpia la pantalla —y el historial del
30
+ * terminal con `3J`, que es lo que el usuario espera de un /clear— y se vuelve a
31
+ * montar de cero.
32
+ *
33
+ * Y se vacía la transcripción: si no, el <Static> nuevo tendría por delante toda
34
+ * la conversación que se acaba de borrar.
35
+ */
36
+ export declare function limpiarLaPantalla(): void;