chocolatito-code 1.6.5 → 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 +65 -33
- package/dist/ui/ink/App.d.ts +0 -23
- package/dist/ui/ink/App.js +86 -57
- package/dist/ui/ink/Prompt.d.ts +16 -6
- package/dist/ui/ink/Prompt.js +37 -62
- package/dist/ui/ink/montarApp.d.ts +34 -0
- package/dist/ui/ink/montarApp.js +93 -1
- package/dist/ui/ink/prestamo.d.ts +63 -0
- package/dist/ui/ink/prestamo.js +92 -0
- package/dist/ui/ink/teclado.d.ts +48 -0
- package/dist/ui/ink/teclado.js +96 -0
- package/dist/ui/ink/transcripcion.d.ts +114 -0
- package/dist/ui/ink/transcripcion.js +180 -0
- package/dist/ui/interrupt.js +4 -1
- package/dist/ui/loopPrompt.d.ts +6 -7
- package/dist/ui/loopPrompt.js +14 -1
- package/dist/ui/marco.d.ts +2 -0
- package/dist/ui/marco.js +18 -1
- package/dist/ui/pantalla.d.ts +47 -26
- package/dist/ui/pantalla.js +155 -68
- package/dist/ui/permissionPrompt.d.ts +10 -17
- package/dist/ui/permissionPrompt.js +14 -1
- package/dist/ui/pieFijo.js +26 -4
- package/dist/ui/selector.d.ts +6 -4
- package/dist/ui/selector.js +14 -1
- package/extension/manifest.json +60 -60
- package/package.json +1 -1
- package/dist/ui/historial.d.ts +0 -49
- package/dist/ui/historial.js +0 -92
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LA CONVERSACIÓN, EN CAMINO AL HISTORIAL DEL TERMINAL
|
|
3
|
+
*
|
|
4
|
+
* QUÉ SUSTITUYE, Y POR QUÉ
|
|
5
|
+
*
|
|
6
|
+
* Antes esto era `historial.ts`: un buffer en memoria con las últimas 5000
|
|
7
|
+
* líneas, que hacía falta porque la pantalla alternativa deja al terminal sin
|
|
8
|
+
* historial propio. Aquí no hay pantalla alternativa. Lo que se escribe entra en
|
|
9
|
+
* el historial DEL TERMINAL —el de verdad, el de la rueda del ratón— a través de
|
|
10
|
+
* `<Static>` de Ink, y esto es solo la cinta transportadora que lo lleva hasta
|
|
11
|
+
* allí.
|
|
12
|
+
*
|
|
13
|
+
* POR QUÉ HAY QUE PARTIR EN LÍNEAS
|
|
14
|
+
*
|
|
15
|
+
* `<Static>` emite un elemento y lo da por escrito para siempre: una vez fuera,
|
|
16
|
+
* ni Ink ni nadie puede volver a tocarlo, porque ya es texto del terminal. Así
|
|
17
|
+
* que solo puede entrar lo que está terminado. El modelo, en cambio, escribe a
|
|
18
|
+
* chorros que cortan a mitad de palabra.
|
|
19
|
+
*
|
|
20
|
+
* Aquí se separan las dos cosas: lo que ya tiene su salto de línea pasa a
|
|
21
|
+
* `<Static>` y se vuelve permanente; el trozo que cuelga se queda en `resto` y
|
|
22
|
+
* lo pinta la zona de abajo, que sí se repinta. Cuando llega el salto, el resto
|
|
23
|
+
* entero cruza al otro lado. Visto desde fuera es una línea que se va
|
|
24
|
+
* escribiendo, sin saltos ni parpadeos.
|
|
25
|
+
*
|
|
26
|
+
* LAS DOS REGLAS DE <STATIC>, QUE NO SON NEGOCIABLES
|
|
27
|
+
*
|
|
28
|
+
* Están medidas en `node_modules/ink/build/components/Static.js:11-20`, que es
|
|
29
|
+
* todo el mecanismo: guarda un índice, renderiza `items.slice(indice)` y en el
|
|
30
|
+
* commit siguiente adelanta el índice a `items.length`. No compara nada.
|
|
31
|
+
*
|
|
32
|
+
* 1. La lista SOLO crece. Un `splice`, un `filter` o un vaciado hacen retroceder
|
|
33
|
+
* el índice, y todo lo que se añada por encima se REIMPRIME: la conversación
|
|
34
|
+
* sale duplicada en el historial del terminal y ya no hay forma de borrarla.
|
|
35
|
+
* 2. Cada elemento necesita `key` propia y estable. React reutiliza nodos por
|
|
36
|
+
* posición, y la lista que ve cambia de longitud en cada commit; sin `key`
|
|
37
|
+
* acabas emitiendo el texto de otro. Por eso cada línea lleva su `id`.
|
|
38
|
+
*
|
|
39
|
+
* Y POR QUÉ EXISTE `base`
|
|
40
|
+
*
|
|
41
|
+
* Cuando el árbol se desmonta y se vuelve a montar —un diálogo de permisos que
|
|
42
|
+
* necesita el teclado para él solo— el `<Static>` nuevo empieza de cero y
|
|
43
|
+
* volvería a escribir la conversación entera. Pero en la pantalla normal esas
|
|
44
|
+
* líneas siguen ahí, en el terminal, que no se ha borrado. `base` marca por
|
|
45
|
+
* dónde iba, y el montaje nuevo solo recibe lo que aún no se ha pintado.
|
|
46
|
+
*/
|
|
47
|
+
/** Lo que se ha ido escribiendo, en orden. Solo se añade al final. */
|
|
48
|
+
let lineas = [];
|
|
49
|
+
/** Cuántas de esas ya están escritas en el terminal. Solo cambia al remontar. */
|
|
50
|
+
let base = 0;
|
|
51
|
+
/** El trozo sin terminar. Todavía no puede cruzar a <Static>. */
|
|
52
|
+
let resto = "";
|
|
53
|
+
let siguienteId = 1;
|
|
54
|
+
const oyentes = [];
|
|
55
|
+
function avisar() {
|
|
56
|
+
for (const cb of oyentes) {
|
|
57
|
+
try {
|
|
58
|
+
cb();
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Entra texto del agente. Acepta trozos a media línea, que es lo normal.
|
|
65
|
+
*
|
|
66
|
+
* El `\r` suelto se descarta con su línea: en un flujo de markdown no es un
|
|
67
|
+
* retorno de carro de verdad, viene de un `\r\n` partido por la mitad entre dos
|
|
68
|
+
* trozos, y dejarlo pasar mete una línea vacía por cada salto.
|
|
69
|
+
*/
|
|
70
|
+
export function anotar(texto) {
|
|
71
|
+
if (!texto)
|
|
72
|
+
return;
|
|
73
|
+
const completo = resto + String(texto).replace(/\r\n/g, "\n");
|
|
74
|
+
const partes = completo.split("\n");
|
|
75
|
+
resto = partes.pop() ?? "";
|
|
76
|
+
for (const l of partes) {
|
|
77
|
+
lineas.push({ id: siguienteId++, texto: l });
|
|
78
|
+
}
|
|
79
|
+
avisar();
|
|
80
|
+
}
|
|
81
|
+
/** Lo último que se entregó, para no rehacerlo en cada dibujado. */
|
|
82
|
+
let entregado = [];
|
|
83
|
+
let entregadoLargo = -1;
|
|
84
|
+
let entregadoBase = -1;
|
|
85
|
+
/**
|
|
86
|
+
* Las líneas que <Static> todavía no ha escrito.
|
|
87
|
+
*
|
|
88
|
+
* DEVUELVE UN ARRAY NUEVO CADA VEZ QUE HAY ALGO NUEVO, Y NO ES UN CAPRICHO
|
|
89
|
+
*
|
|
90
|
+
* Este fue el fallo que dejó la conversación entera sin escribirse, y costó
|
|
91
|
+
* encontrarlo porque no parecía un fallo: la primera versión devolvía el array
|
|
92
|
+
* interno tal cual, al que `anotar` le hace `push`.
|
|
93
|
+
*
|
|
94
|
+
* <Static> memoriza lo que va a emitir con `useMemo(() => items.slice(index),
|
|
95
|
+
* [items, index])` (Static.js:12-14). Con la MISMA referencia de array y el
|
|
96
|
+
* mismo índice, React da por buena la memoria y devuelve lo de antes: el `[]`
|
|
97
|
+
* del primer dibujado. El array crecía, la pantalla no. Nada de emitir de más
|
|
98
|
+
* ni de menos: sencillamente no se emitía NUNCA.
|
|
99
|
+
*
|
|
100
|
+
* Así que la referencia tiene que cambiar cuando cambia el contenido. Se
|
|
101
|
+
* reconstruye solo cuando ha crecido de verdad —una vez por línea terminada, no
|
|
102
|
+
* una por dibujado—, que a treinta fotogramas por segundo es la diferencia entre
|
|
103
|
+
* reconstruirlo treinta veces por segundo o ninguna mientras nadie escribe.
|
|
104
|
+
*/
|
|
105
|
+
export function pendientes() {
|
|
106
|
+
if (lineas.length !== entregadoLargo || base !== entregadoBase) {
|
|
107
|
+
entregado = lineas.slice(base);
|
|
108
|
+
entregadoLargo = lineas.length;
|
|
109
|
+
entregadoBase = base;
|
|
110
|
+
}
|
|
111
|
+
return entregado;
|
|
112
|
+
}
|
|
113
|
+
/** El trozo que cuelga, el que pinta la zona de abajo. */
|
|
114
|
+
export function colgando() {
|
|
115
|
+
return resto;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* "Lo de hasta aquí ya está en el terminal."
|
|
119
|
+
*
|
|
120
|
+
* Se llama SOLO justo antes de desmontar el árbol. Llamarlo con el árbol vivo
|
|
121
|
+
* encogería la lista que ve <Static>, que es la regla 1 de arriba.
|
|
122
|
+
*/
|
|
123
|
+
export function darPorPintado() {
|
|
124
|
+
base = lineas.length;
|
|
125
|
+
}
|
|
126
|
+
export function alCambiar(cb) {
|
|
127
|
+
oyentes.push(cb);
|
|
128
|
+
return () => {
|
|
129
|
+
const i = oyentes.indexOf(cb);
|
|
130
|
+
if (i >= 0)
|
|
131
|
+
oyentes.splice(i, 1);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Para las pruebas. En producción la conversación no se tira nunca. */
|
|
135
|
+
export function vaciarTranscripcion() {
|
|
136
|
+
lineas = [];
|
|
137
|
+
base = 0;
|
|
138
|
+
resto = "";
|
|
139
|
+
siguienteId = 1;
|
|
140
|
+
entregado = [];
|
|
141
|
+
entregadoLargo = -1;
|
|
142
|
+
entregadoBase = -1;
|
|
143
|
+
}
|
|
144
|
+
/** Cuántas líneas terminadas van. Para las pruebas y para medir. */
|
|
145
|
+
export function cuantasLineas() {
|
|
146
|
+
return lineas.length;
|
|
147
|
+
}
|
|
148
|
+
const SOLO_COLOR = /\x1b\[[0-9;]*m/g;
|
|
149
|
+
/**
|
|
150
|
+
* Recorta el trozo que cuelga para que quepa en las filas que le tocan.
|
|
151
|
+
*
|
|
152
|
+
* NO ES COSMÉTICO, ES LA RED DE SEGURIDAD DEL RENDIMIENTO
|
|
153
|
+
*
|
|
154
|
+
* En Windows, si el bloque de abajo llega a medir lo que la ventana, Ink deja de
|
|
155
|
+
* repintar por diferencias y pasa a `clearTerminal` + reimprimir la conversación
|
|
156
|
+
* ENTERA en cada fotograma (ink.js:100-102 y 769). Con el spinner a 30 por
|
|
157
|
+
* segundo eso es parpadeo, historial perdido y coste creciente con la sesión.
|
|
158
|
+
*
|
|
159
|
+
* El caso que lo dispara: un párrafo largo sin un solo salto de línea. Mientras
|
|
160
|
+
* no lo termine, ese párrafo entero es "lo que cuelga".
|
|
161
|
+
*
|
|
162
|
+
* Cuando hay que recortar se quitan también los colores: cortar una cadena por
|
|
163
|
+
* el medio parte los códigos de escape y lo que sale es basura de colores
|
|
164
|
+
* enganchada al resto de la sesión. Es un trozo que en cuanto llegue su salto de
|
|
165
|
+
* línea se reemplaza por la línea entera y bien pintada, así que el recorte no
|
|
166
|
+
* dura ni un parpadeo.
|
|
167
|
+
*/
|
|
168
|
+
export function recortarCola(texto, cols, filas) {
|
|
169
|
+
if (!texto)
|
|
170
|
+
return [];
|
|
171
|
+
const ancho = Math.max(1, cols);
|
|
172
|
+
const techo = Math.max(0, filas);
|
|
173
|
+
if (techo === 0)
|
|
174
|
+
return [];
|
|
175
|
+
const visible = texto.replace(SOLO_COLOR, "");
|
|
176
|
+
if (Math.ceil(visible.length / ancho) <= techo)
|
|
177
|
+
return [texto];
|
|
178
|
+
const pelado = visible.slice(-(ancho * techo));
|
|
179
|
+
return [pelado];
|
|
180
|
+
}
|
package/dist/ui/interrupt.js
CHANGED
|
@@ -177,7 +177,10 @@ export class InterruptWatcher {
|
|
|
177
177
|
}
|
|
178
178
|
else {
|
|
179
179
|
this.stop();
|
|
180
|
-
process.stdout.write
|
|
180
|
+
// `dibujar` y no `process.stdout.write`: con el arbol montado, una
|
|
181
|
+
// escritura cruda es una fila que Ink no cuenta, y el borrado del marco
|
|
182
|
+
// al salir se come una linea de la conversacion.
|
|
183
|
+
dibujar("\n");
|
|
181
184
|
process.exit(0);
|
|
182
185
|
}
|
|
183
186
|
return;
|
package/dist/ui/loopPrompt.d.ts
CHANGED
|
@@ -5,13 +5,12 @@ export interface RespuestaDeBucle {
|
|
|
5
5
|
indicacion?: string;
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Se pide el terminal prestado antes de abrir esto.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* el atasco, y el turno sigue.
|
|
10
|
+
* Con el árbol de Ink montado hay un dueño de la pantalla, y esta función es de
|
|
11
|
+
* las que necesitan el teclado para ellas solas: escribe directo a stdout y abre
|
|
12
|
+
* su propia lectura de teclas. Sin el préstamo, las dos cuentas de filas se pisan
|
|
13
|
+
* y el marco de Ink no se recupera. Sin árbol, `conElTerminalPrestado` ejecuta la
|
|
14
|
+
* tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
|
|
16
15
|
*/
|
|
17
16
|
export declare function preguntarPorBucle(aviso: AvisoDeBucle): Promise<RespuestaDeBucle>;
|
package/dist/ui/loopPrompt.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
+
import { conElTerminalPrestado } from "./ink/prestamo.js";
|
|
4
5
|
/**
|
|
5
6
|
* Que hacer cuando el agente lleva tres vueltas haciendo exactamente lo mismo.
|
|
6
7
|
*
|
|
@@ -11,7 +12,7 @@ import chalk from "chalk";
|
|
|
11
12
|
* mirando. Parar no mata el turno: le devuelve al modelo un mensaje explicando
|
|
12
13
|
* el atasco, y el turno sigue.
|
|
13
14
|
*/
|
|
14
|
-
|
|
15
|
+
async function preguntarPorBucleDirecto(aviso) {
|
|
15
16
|
const cabecera = chalk.yellow(`\n↻ Parece un bucle: `) +
|
|
16
17
|
chalk.white(`"${aviso.herramienta}"`) +
|
|
17
18
|
chalk.yellow(` lleva ${aviso.repeticiones} llamadas identicas con el mismo resultado.`);
|
|
@@ -57,3 +58,15 @@ export async function preguntarPorBucle(aviso) {
|
|
|
57
58
|
});
|
|
58
59
|
});
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Se pide el terminal prestado antes de abrir esto.
|
|
63
|
+
*
|
|
64
|
+
* Con el árbol de Ink montado hay un dueño de la pantalla, y esta función es de
|
|
65
|
+
* las que necesitan el teclado para ellas solas: escribe directo a stdout y abre
|
|
66
|
+
* su propia lectura de teclas. Sin el préstamo, las dos cuentas de filas se pisan
|
|
67
|
+
* y el marco de Ink no se recupera. Sin árbol, `conElTerminalPrestado` ejecuta la
|
|
68
|
+
* tarea tal cual y no cuesta nada. Ver ui/ink/prestamo.ts.
|
|
69
|
+
*/
|
|
70
|
+
export function preguntarPorBucle(aviso) {
|
|
71
|
+
return conElTerminalPrestado(() => preguntarPorBucleDirecto(aviso));
|
|
72
|
+
}
|
package/dist/ui/marco.d.ts
CHANGED
|
@@ -32,4 +32,6 @@ export interface PromptFrame {
|
|
|
32
32
|
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
33
33
|
* lugar de estimarse por otro lado y descuadrarse.
|
|
34
34
|
*/
|
|
35
|
+
/** Cuantas sugerencias se ensenan como mucho. Ver la nota en el desplegable. */
|
|
36
|
+
export declare const MAXIMO_DEL_DESPLEGABLE = 8;
|
|
35
37
|
export declare function buildPromptFrame(opts: PromptFrameOptions): PromptFrame;
|
package/dist/ui/marco.js
CHANGED
|
@@ -55,6 +55,8 @@ function truncatePlain(text, width) {
|
|
|
55
55
|
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
56
56
|
* lugar de estimarse por otro lado y descuadrarse.
|
|
57
57
|
*/
|
|
58
|
+
/** Cuantas sugerencias se ensenan como mucho. Ver la nota en el desplegable. */
|
|
59
|
+
export const MAXIMO_DEL_DESPLEGABLE = 8;
|
|
58
60
|
export function buildPromptFrame(opts) {
|
|
59
61
|
const cols = Math.max(20, opts.cols || 80);
|
|
60
62
|
const viewportRows = Math.max(6, opts.rows || 24);
|
|
@@ -95,7 +97,22 @@ export function buildPromptFrame(opts) {
|
|
|
95
97
|
// Sin contarla, el desplegable pide una fila mas de las que caben y el marco
|
|
96
98
|
// se sale por arriba de la ventana.
|
|
97
99
|
const room = Math.max(1, viewportRows - 1 - (2 + 1 + inputRows + 1 + 1));
|
|
98
|
-
|
|
100
|
+
// Y ademas un tope propio, mas bajo que lo que cabe.
|
|
101
|
+
//
|
|
102
|
+
// POR QUE NO SE DEJA CRECER HASTA LLENAR LA VENTANA
|
|
103
|
+
//
|
|
104
|
+
// Con solo "/" hay dieciocho comandos. En una consola de 24 filas eso hacia un
|
|
105
|
+
// marco de veinte, o sea que al escribir UNA barra la caja se comia la pantalla
|
|
106
|
+
// entera y tapaba la conversacion; y a la letra siguiente se encogia a nueve.
|
|
107
|
+
// Cada tecla movia veinte filas arriba y abajo.
|
|
108
|
+
//
|
|
109
|
+
// Eso es feo, y ademas es el terreno donde aparecen los fallos de repintado: un
|
|
110
|
+
// marco que crece hasta el borde obliga al terminal a desplazar, y a partir de
|
|
111
|
+
// ahi Ink borra contando filas que ya no son suyas.
|
|
112
|
+
//
|
|
113
|
+
// Ocho es lo que se ve de un vistazo sin buscar. Y el desplegable filtra segun
|
|
114
|
+
// escribes, asi que llegar al que quieres son dos letras, no dieciocho lineas.
|
|
115
|
+
const shown = Math.min(items.length, room, MAXIMO_DEL_DESPLEGABLE);
|
|
99
116
|
const dropdownOffset = shown > 0 && items.length > shown
|
|
100
117
|
? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
|
|
101
118
|
: 0;
|
package/dist/ui/pantalla.d.ts
CHANGED
|
@@ -1,37 +1,58 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
2
|
+
* De serie SÍ, desde la 1.6.7, y con salida de emergencia.
|
|
3
|
+
*
|
|
4
|
+
* DOS VECES ESTUVO AQUÍ Y DOS VECES SALIÓ ROTO
|
|
5
|
+
*
|
|
6
|
+
* La 1.6.0 salió en negro ("no sale nada, se bugea") y la 1.6.5 con "una barra y
|
|
7
|
+
* lo demás negro, escribo y se cierra". Las dos veces pasaban las 820 pruebas:
|
|
8
|
+
* un emulador prueba lo que sabe emular, y una consola de verdad hace cosas que
|
|
9
|
+
* no sabe.
|
|
10
|
+
*
|
|
11
|
+
* Esta vez el orden fue el otro: se entregó detrás de la variable, el dueño lo
|
|
12
|
+
* corrió en SU consola, y solo después pasó a ser lo normal. Lo hizo, y lo
|
|
13
|
+
* aprobó sabiendo lo que no le iba a gustar: que la caja no se queda clavada al
|
|
14
|
+
* hacer scroll. No se queda porque es texto del terminal, y en Claude Code
|
|
15
|
+
* tampoco se queda. Es el precio de recuperar la rueda del ratón y el historial.
|
|
16
|
+
*
|
|
17
|
+
* CHOCOLATITO_PANTALLA=normal devuelve la interfaz clásica. Existe porque una
|
|
18
|
+
* consola rara —una que no sea la suya ni la mía— puede seguir haciendo algo que
|
|
19
|
+
* no habíamos visto, y entonces el usuario necesita poder trabajar hoy, no
|
|
20
|
+
* esperar al parche.
|
|
18
21
|
*/
|
|
19
22
|
export declare function pantallaFijaPedida(): boolean;
|
|
23
|
+
export declare function forzarPantallaFija(valor: boolean | null): void;
|
|
24
|
+
export declare function enPantallaFija(): boolean;
|
|
20
25
|
/**
|
|
21
|
-
* Enciende
|
|
26
|
+
* Enciende el modo de árbol único. Devuelve si de verdad se encendió.
|
|
27
|
+
*
|
|
28
|
+
* EL BOOLEANO NO SOBRA, Y SE VOLVIÓ IMPRESCINDIBLE AL PONERLO POR DEFECTO
|
|
22
29
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
30
|
+
* Sin terminal —una salida redirigida a un archivo, un `chocolatito "tarea"` en
|
|
31
|
+
* un script, CI— aquí no se entra. Pero quien llama montaba el árbol de todas
|
|
32
|
+
* formas, y Ink sin terminal escribe su marco como texto suelto: la salida del
|
|
33
|
+
* programa sale con la caja de escribir dentro.
|
|
34
|
+
*
|
|
35
|
+
* Mientras esto iba detrás de una variable no pasaba nunca, porque nadie la pone
|
|
36
|
+
* en un script. De serie, es el caso normal.
|
|
26
37
|
*/
|
|
27
|
-
export declare function
|
|
28
|
-
|
|
29
|
-
|
|
38
|
+
export declare function entrarEnPantallaFija(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Manda `console` a la transcripción.
|
|
41
|
+
*
|
|
42
|
+
* Se exporta para poder probarlo: `entrarEnPantallaFija` no se puede llamar en
|
|
43
|
+
* las pruebas porque exige un terminal de verdad, y un parche de consola sin
|
|
44
|
+
* probar es de las cosas que fallan en silencio —lo que se pierde no se ve, que
|
|
45
|
+
* es justo el problema—.
|
|
46
|
+
*/
|
|
47
|
+
export declare function interceptarConsola(): void;
|
|
48
|
+
export declare function devolverConsola(): void;
|
|
30
49
|
/**
|
|
31
50
|
* Devuelve el terminal como estaba.
|
|
32
51
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
52
|
+
* Aquí ya no hay ninguna pantalla que abandonar —esa era la versión de antes—,
|
|
53
|
+
* pero sí hay que devolver el conducto de dibujo y el pie: si el proceso sigue
|
|
54
|
+
* vivo después de esto (una prueba, una sesión que cambia de modo) y el dibujo
|
|
55
|
+
* se queda desviado, todo lo que se escriba va a una transcripción que ya no
|
|
56
|
+
* pinta nadie.
|
|
36
57
|
*/
|
|
37
58
|
export declare function salirDePantallaFija(): void;
|
package/dist/ui/pantalla.js
CHANGED
|
@@ -1,110 +1,197 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
+
import { format } from "node:util";
|
|
2
3
|
import { redirigirDibujo } from "./salida.js";
|
|
3
|
-
import {
|
|
4
|
+
import { anotar } from "./ink/transcripcion.js";
|
|
4
5
|
import { habilitarPie } from "./pieFijo.js";
|
|
6
|
+
import { limpiarLaPantalla } from "./ink/montarApp.js";
|
|
5
7
|
/**
|
|
6
|
-
* LA PANTALLA
|
|
8
|
+
* QUIÉN ES DUEÑO DE LA PANTALLA
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* terminal, no una capa encima. Ninguna secuencia de escape la clava ahí; lo
|
|
10
|
-
* único que lo consigue es que el programa tome la pantalla entera, como hacen
|
|
11
|
-
* vim o htop.
|
|
10
|
+
* LO QUE HABÍA AQUÍ ANTES, Y POR QUÉ SE FUE
|
|
12
11
|
*
|
|
13
|
-
*
|
|
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".
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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.
|
|
18
22
|
*
|
|
19
|
-
*
|
|
20
|
-
* el conducto de dibujo: media docena de sitios —el arranque, la licencia, los
|
|
21
|
-
* avisos— siguen imprimiendo directo, y en la pantalla alternativa eso escribe
|
|
22
|
-
* encima de lo que Ink acaba de dibujar y descuadra el árbol entero.
|
|
23
|
+
* QUÉ HACE AHORA
|
|
23
24
|
*
|
|
24
|
-
*
|
|
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:
|
|
25
27
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
30
50
|
*/
|
|
31
|
-
const ENTRAR = "\x1b[?1049h";
|
|
32
|
-
const SALIR = "\x1b[?1049l";
|
|
33
51
|
let dentro = false;
|
|
34
|
-
|
|
52
|
+
const original = {};
|
|
35
53
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
|
57
|
+
*
|
|
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.
|
|
62
|
+
*
|
|
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.
|
|
52
73
|
*/
|
|
53
74
|
export function pantallaFijaPedida() {
|
|
54
75
|
return process.env.CHOCOLATITO_PANTALLA !== "normal";
|
|
55
76
|
}
|
|
56
77
|
/** `null` = manda el estado real. Las pruebas lo fuerzan. */
|
|
57
78
|
let forzado = null;
|
|
58
|
-
/**
|
|
59
|
-
* Enciende o apaga el modo a mano, SIN tocar el terminal.
|
|
60
|
-
*
|
|
61
|
-
* Las pruebas lo necesitan: entrar de verdad escribiría la secuencia de la
|
|
62
|
-
* pantalla alternativa en la consola de quien corre las pruebas y le dejaría el
|
|
63
|
-
* informe en una pantalla que desaparece al acabar.
|
|
64
|
-
*/
|
|
65
79
|
export function forzarPantallaFija(valor) {
|
|
66
80
|
forzado = valor;
|
|
67
81
|
}
|
|
68
82
|
export function enPantallaFija() {
|
|
69
83
|
return forzado ?? dentro;
|
|
70
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
|
+
*/
|
|
71
98
|
export function entrarEnPantallaFija() {
|
|
72
|
-
if (dentro
|
|
73
|
-
return;
|
|
99
|
+
if (dentro)
|
|
100
|
+
return true;
|
|
101
|
+
if (!process.stdout.isTTY)
|
|
102
|
+
return false;
|
|
74
103
|
dentro = true;
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
redirigirDibujo((texto) =>
|
|
78
|
-
// Y el pie deja de pintar. Sigue guardando sus secciones -el spinner y la
|
|
79
|
-
// linea de razonamiento publican ahi sin enterarse de nada- pero quien las
|
|
80
|
-
// dibuja pasa a ser el arbol. Dos dueños del mismo trozo de pantalla es
|
|
81
|
-
// 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));
|
|
82
107
|
habilitarPie(false);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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();
|
|
86
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
|
+
}
|
|
87
157
|
}
|
|
88
158
|
/**
|
|
89
159
|
* Devuelve el terminal como estaba.
|
|
90
160
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
161
|
+
* Aquí ya no hay ninguna pantalla que abandonar —esa era la versión de antes—,
|
|
162
|
+
* pero sí 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.
|
|
94
166
|
*/
|
|
95
167
|
export function salirDePantallaFija() {
|
|
96
168
|
if (!dentro)
|
|
97
169
|
return;
|
|
98
170
|
dentro = false;
|
|
99
|
-
if (logOriginal) {
|
|
100
|
-
console.log = logOriginal;
|
|
101
|
-
logOriginal = null;
|
|
102
|
-
}
|
|
103
171
|
redirigirDibujo(null);
|
|
104
172
|
habilitarPie(null);
|
|
105
|
-
|
|
106
|
-
process.stdout.write(SALIR);
|
|
107
|
-
}
|
|
108
|
-
catch { }
|
|
173
|
+
devolverConsola();
|
|
109
174
|
}
|
|
110
175
|
process.on("exit", salirDePantallaFija);
|
|
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)`.
|
|
182
|
+
*
|
|
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.
|
|
188
|
+
*
|
|
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.
|
|
193
|
+
*
|
|
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.
|
|
197
|
+
*/
|