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 +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 +45 -17
- package/dist/ui/pantalla.js +150 -92
- 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
package/dist/ui/ink/montarApp.js
CHANGED
|
@@ -2,6 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { render } from "ink";
|
|
4
4
|
import { App } from "./App.js";
|
|
5
|
+
import { darPorPintado, vaciarTranscripcion } from "./transcripcion.js";
|
|
6
|
+
import { devolverTecladoDelPrompt, tomarTecladoParaPrompt } from "../interrupt.js";
|
|
5
7
|
/**
|
|
6
8
|
* MONTA EL ÁRBOL Y LO DEJA VIVO TODA LA SESIÓN
|
|
7
9
|
*
|
|
@@ -12,23 +14,113 @@ import { App } from "./App.js";
|
|
|
12
14
|
* parece a su causa. Con el montaje aparte, `index.ts` llama a los dos y no hay
|
|
13
15
|
* ciclo.
|
|
14
16
|
*
|
|
17
|
+
* LAS OPCIONES, UNA POR UNA
|
|
18
|
+
*
|
|
15
19
|
* `exitOnCtrlC: false` porque Ctrl+C ya lo trata el prompt: dejar que Ink cierre
|
|
16
20
|
* el proceso por su cuenta se salta el guardado de la sesión.
|
|
21
|
+
*
|
|
22
|
+
* `patchConsole: false` y esto necesita explicación, porque el de serie de Ink
|
|
23
|
+
* es bueno y aun así no se usa. Quedan cerca de cien `console.log` y
|
|
24
|
+
* `console.error` repartidos por el programa —los comandos de barra, los avisos
|
|
25
|
+
* de los hooks, la licencia— y escribir directo con un marco de Ink puesto lo
|
|
26
|
+
* descuadra. El parche de Ink resuelve eso: borra su marco, escribe y repinta.
|
|
27
|
+
*
|
|
28
|
+
* Pero los manda por un camino distinto que el texto del agente, y con otro
|
|
29
|
+
* tiempo: el del agente espera al dibujado de React, el del parche sale en el
|
|
30
|
+
* acto. Dos colas para la misma conversación acaban ordenándola mal. Aquí los dos
|
|
31
|
+
* van por la transcripción, que es una sola cola. Ver ui/pantalla.ts.
|
|
32
|
+
*
|
|
33
|
+
* `alternateScreen: false` es lo de serie y se pone a la vista a propósito: es
|
|
34
|
+
* justo lo que se quitó, y dejarlo escrito ahorra que alguien —yo, dentro de
|
|
35
|
+
* tres semanas— vuelva a probarlo.
|
|
17
36
|
*/
|
|
18
37
|
let arbol = null;
|
|
19
38
|
export function montarArbol() {
|
|
20
39
|
if (arbol)
|
|
21
40
|
return;
|
|
22
|
-
|
|
41
|
+
// EL TECLADO ES DE INK MIENTRAS EL ÁRBOL VIVA
|
|
42
|
+
//
|
|
43
|
+
// El vigilante de Esc guarda cómo estaba el modo del teclado al empezar el
|
|
44
|
+
// turno y lo restaura al pararse. Cuando se para tarde —y se para tarde a
|
|
45
|
+
// menudo: es un `keypress` que se desengancha en el siguiente tick— restaura
|
|
46
|
+
// "no crudo" con el árbol delante. El terminal vuelve a hacer eco por su
|
|
47
|
+
// cuenta, las teclas dejan de llegar a Ink, y Enter suelta la línea de golpe.
|
|
48
|
+
//
|
|
49
|
+
// Visto desde fuera: "con darle enter se va la traba". Es el teclado que se
|
|
50
|
+
// queda pillado después de un /effort.
|
|
51
|
+
//
|
|
52
|
+
// Esta marca ya existía para el prompt clásico, que la ponía al abrirse y la
|
|
53
|
+
// quitaba al cerrarse. Con el árbol único el prompt no se cierra nunca, así
|
|
54
|
+
// que se pone aquí y no se quita hasta desmontar.
|
|
55
|
+
tomarTecladoParaPrompt();
|
|
56
|
+
arbol = render(_jsx(App, {}), {
|
|
57
|
+
exitOnCtrlC: false,
|
|
58
|
+
patchConsole: false,
|
|
59
|
+
alternateScreen: false,
|
|
60
|
+
});
|
|
23
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Suelta el árbol.
|
|
64
|
+
*
|
|
65
|
+
* `darPorPintado()` ANTES de desmontar, y el orden importa: si el árbol se
|
|
66
|
+
* vuelve a montar —un diálogo que necesita el teclado para él solo—, el
|
|
67
|
+
* `<Static>` nuevo empieza su cuenta de cero y reimprimiría la conversación
|
|
68
|
+
* entera. En la pantalla normal esas líneas siguen ahí, en el terminal; la marca
|
|
69
|
+
* dice por dónde iba para que el montaje nuevo solo escriba lo que falta.
|
|
70
|
+
*
|
|
71
|
+
* `clear()` borra el marco de abajo antes de irse. Sin eso queda una copia
|
|
72
|
+
* muerta de la barra en mitad del historial, y en la próxima sesión de scroll el
|
|
73
|
+
* usuario se encuentra dos.
|
|
74
|
+
*/
|
|
24
75
|
export function desmontarArbol() {
|
|
25
76
|
if (!arbol)
|
|
26
77
|
return;
|
|
78
|
+
darPorPintado();
|
|
79
|
+
try {
|
|
80
|
+
arbol.clear();
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
27
83
|
try {
|
|
28
84
|
arbol.unmount();
|
|
29
85
|
}
|
|
30
86
|
catch { }
|
|
31
87
|
arbol = null;
|
|
88
|
+
devolverTecladoDelPrompt();
|
|
89
|
+
}
|
|
90
|
+
export function hayArbol() {
|
|
91
|
+
return arbol !== null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Borra la pantalla de verdad. Es lo que hacen /clear, /cd y Ctrl+L.
|
|
95
|
+
*
|
|
96
|
+
* POR QUE NO VALE UN `console.clear()`
|
|
97
|
+
*
|
|
98
|
+
* `console.clear()` escribe la secuencia de borrado directa al terminal, y no
|
|
99
|
+
* pasa por la interceptación de consola: ahí solo van `log`, `error`, `warn` e
|
|
100
|
+
* `info`. Con el árbol montado eso borra filas que Ink cree suyas, y a partir de
|
|
101
|
+
* ahí Ink borra y repinta sobre un sitio que ya no existe. Lo que se ve es
|
|
102
|
+
* basura, o nada.
|
|
103
|
+
*
|
|
104
|
+
* Aquí se hace en el orden que no rompe nada: se borra el marco con la cuenta
|
|
105
|
+
* todavía buena, se suelta el árbol, se limpia la pantalla —y el historial del
|
|
106
|
+
* terminal con `3J`, que es lo que el usuario espera de un /clear— y se vuelve a
|
|
107
|
+
* montar de cero.
|
|
108
|
+
*
|
|
109
|
+
* Y se vacía la transcripción: si no, el <Static> nuevo tendría por delante toda
|
|
110
|
+
* la conversación que se acaba de borrar.
|
|
111
|
+
*/
|
|
112
|
+
export function limpiarLaPantalla() {
|
|
113
|
+
const habia = arbol !== null;
|
|
114
|
+
if (habia)
|
|
115
|
+
desmontarArbol();
|
|
116
|
+
vaciarTranscripcion();
|
|
117
|
+
try {
|
|
118
|
+
// 2J la pantalla, 3J el historial de desplazamiento, H el cursor arriba.
|
|
119
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
120
|
+
}
|
|
121
|
+
catch { }
|
|
122
|
+
if (habia)
|
|
123
|
+
montarArbol();
|
|
32
124
|
}
|
|
33
125
|
// Si el proceso muere sin pasar por la salida ordenada, el árbol tiene que
|
|
34
126
|
// soltarse igual: un árbol vivo en un proceso que se cierra deja el terminal a
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRESTARLE EL TERMINAL A OTRO UN MOMENTO
|
|
3
|
+
*
|
|
4
|
+
* EL PROBLEMA
|
|
5
|
+
*
|
|
6
|
+
* Con el árbol de Ink montado, el terminal tiene un dueño: Ink pide el modo
|
|
7
|
+
* crudo, lleva la cuenta de las filas que ha pintado y repinta por diferencias.
|
|
8
|
+
*
|
|
9
|
+
* Pero hay tres sitios del programa que necesitan el teclado para ellos solos, y
|
|
10
|
+
* los tres se abren EN MITAD DE UN TURNO, con el árbol vivo:
|
|
11
|
+
*
|
|
12
|
+
* - el diálogo de permisos (ui/permissionPrompt.ts), que sube el cursor con
|
|
13
|
+
* `\x1b[nA` y repinta su caja a mano;
|
|
14
|
+
* - el aviso de bucle (ui/loopPrompt.ts), que abre `readline`;
|
|
15
|
+
* - el menú de /effort (ui/selector.ts), que repinta su lista.
|
|
16
|
+
*
|
|
17
|
+
* Los tres escriben directo a `process.stdout` y abren `readline` sobre el mismo
|
|
18
|
+
* `process.stdin` que Ink está leyendo. Dos dueños a la vez: el marco de Ink se
|
|
19
|
+
* descuadra desde la primera herramienta que pida permiso, y a partir de ahí
|
|
20
|
+
* repinta sobre filas que ya no son suyas.
|
|
21
|
+
*
|
|
22
|
+
* LA SOLUCIÓN, QUE YA VENÍA EN INK
|
|
23
|
+
*
|
|
24
|
+
* `suspendTerminal` existe para esto —está pensado para abrir `$EDITOR` o `less`
|
|
25
|
+
* desde una aplicación de Ink— y hace exactamente lo que hace falta: vacía lo
|
|
26
|
+
* pendiente, borra su marco, devuelve el cursor, suelta el modo crudo, y al
|
|
27
|
+
* volver reclama la entrada y fuerza un redibujado completo. Si la tarea revienta
|
|
28
|
+
* lo restaura igual.
|
|
29
|
+
*
|
|
30
|
+
* POR QUE HAY UN ARCHIVO SOLO PARA ESTO
|
|
31
|
+
*
|
|
32
|
+
* `suspendTerminal` solo se coge con `useApp()`, o sea desde dentro de un
|
|
33
|
+
* componente. Quien lo necesita —el diálogo de permisos, llamado desde el bucle
|
|
34
|
+
* del agente— está a diez capas de React. Aquí App lo deja apuntado al montarse
|
|
35
|
+
* y el resto del programa lo usa sin saber que Ink existe.
|
|
36
|
+
*
|
|
37
|
+
* SIN ÁRBOL NO PASA NADA
|
|
38
|
+
*
|
|
39
|
+
* En el modo clásico no hay nadie registrado y la tarea se ejecuta tal cual. Por
|
|
40
|
+
* eso se puede envolver a los tres sin condicionales en el sitio de la llamada.
|
|
41
|
+
*/
|
|
42
|
+
type Prestamo = (tarea: () => void | Promise<void>) => Promise<void>;
|
|
43
|
+
export declare function registrarPrestamo(fn: Prestamo | null): void;
|
|
44
|
+
export declare function hayPrestamo(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* ¿Está el terminal ahora mismo en manos de otro?
|
|
47
|
+
*
|
|
48
|
+
* Lo pregunta el vigilante del teclado del árbol: mientras un diálogo tiene el
|
|
49
|
+
* terminal, el modo crudo es SUYO y reafirmarlo por detrás le quitaría las
|
|
50
|
+
* teclas justo cuando está esperando una respuesta.
|
|
51
|
+
*/
|
|
52
|
+
export declare function terminalPrestado(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Corre `tarea` con el terminal en manos de quien lo pide.
|
|
55
|
+
*
|
|
56
|
+
* El guardián de anidamiento no es hipotético: un subagente puede pedir permiso
|
|
57
|
+
* mientras el principal ya tiene un diálogo abierto. Suspender dos veces lanza
|
|
58
|
+
* una excepción dentro de Ink, y el árbol se quedaría suspendido para siempre —
|
|
59
|
+
* pantalla muerta. Si ya está prestado, la tarea corre y ya está: el terminal ya
|
|
60
|
+
* es de quien lo pidió primero.
|
|
61
|
+
*/
|
|
62
|
+
export declare function conElTerminalPrestado<T>(tarea: () => Promise<T>): Promise<T>;
|
|
63
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PRESTARLE EL TERMINAL A OTRO UN MOMENTO
|
|
3
|
+
*
|
|
4
|
+
* EL PROBLEMA
|
|
5
|
+
*
|
|
6
|
+
* Con el árbol de Ink montado, el terminal tiene un dueño: Ink pide el modo
|
|
7
|
+
* crudo, lleva la cuenta de las filas que ha pintado y repinta por diferencias.
|
|
8
|
+
*
|
|
9
|
+
* Pero hay tres sitios del programa que necesitan el teclado para ellos solos, y
|
|
10
|
+
* los tres se abren EN MITAD DE UN TURNO, con el árbol vivo:
|
|
11
|
+
*
|
|
12
|
+
* - el diálogo de permisos (ui/permissionPrompt.ts), que sube el cursor con
|
|
13
|
+
* `\x1b[nA` y repinta su caja a mano;
|
|
14
|
+
* - el aviso de bucle (ui/loopPrompt.ts), que abre `readline`;
|
|
15
|
+
* - el menú de /effort (ui/selector.ts), que repinta su lista.
|
|
16
|
+
*
|
|
17
|
+
* Los tres escriben directo a `process.stdout` y abren `readline` sobre el mismo
|
|
18
|
+
* `process.stdin` que Ink está leyendo. Dos dueños a la vez: el marco de Ink se
|
|
19
|
+
* descuadra desde la primera herramienta que pida permiso, y a partir de ahí
|
|
20
|
+
* repinta sobre filas que ya no son suyas.
|
|
21
|
+
*
|
|
22
|
+
* LA SOLUCIÓN, QUE YA VENÍA EN INK
|
|
23
|
+
*
|
|
24
|
+
* `suspendTerminal` existe para esto —está pensado para abrir `$EDITOR` o `less`
|
|
25
|
+
* desde una aplicación de Ink— y hace exactamente lo que hace falta: vacía lo
|
|
26
|
+
* pendiente, borra su marco, devuelve el cursor, suelta el modo crudo, y al
|
|
27
|
+
* volver reclama la entrada y fuerza un redibujado completo. Si la tarea revienta
|
|
28
|
+
* lo restaura igual.
|
|
29
|
+
*
|
|
30
|
+
* POR QUE HAY UN ARCHIVO SOLO PARA ESTO
|
|
31
|
+
*
|
|
32
|
+
* `suspendTerminal` solo se coge con `useApp()`, o sea desde dentro de un
|
|
33
|
+
* componente. Quien lo necesita —el diálogo de permisos, llamado desde el bucle
|
|
34
|
+
* del agente— está a diez capas de React. Aquí App lo deja apuntado al montarse
|
|
35
|
+
* y el resto del programa lo usa sin saber que Ink existe.
|
|
36
|
+
*
|
|
37
|
+
* SIN ÁRBOL NO PASA NADA
|
|
38
|
+
*
|
|
39
|
+
* En el modo clásico no hay nadie registrado y la tarea se ejecuta tal cual. Por
|
|
40
|
+
* eso se puede envolver a los tres sin condicionales en el sitio de la llamada.
|
|
41
|
+
*/
|
|
42
|
+
let prestar = null;
|
|
43
|
+
/** Ink revienta si se suspende dos veces. Ver ink.js:873-875. */
|
|
44
|
+
let yaPrestado = false;
|
|
45
|
+
export function registrarPrestamo(fn) {
|
|
46
|
+
prestar = fn;
|
|
47
|
+
}
|
|
48
|
+
export function hayPrestamo() {
|
|
49
|
+
return prestar !== null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* ¿Está el terminal ahora mismo en manos de otro?
|
|
53
|
+
*
|
|
54
|
+
* Lo pregunta el vigilante del teclado del árbol: mientras un diálogo tiene el
|
|
55
|
+
* terminal, el modo crudo es SUYO y reafirmarlo por detrás le quitaría las
|
|
56
|
+
* teclas justo cuando está esperando una respuesta.
|
|
57
|
+
*/
|
|
58
|
+
export function terminalPrestado() {
|
|
59
|
+
return yaPrestado;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Corre `tarea` con el terminal en manos de quien lo pide.
|
|
63
|
+
*
|
|
64
|
+
* El guardián de anidamiento no es hipotético: un subagente puede pedir permiso
|
|
65
|
+
* mientras el principal ya tiene un diálogo abierto. Suspender dos veces lanza
|
|
66
|
+
* una excepción dentro de Ink, y el árbol se quedaría suspendido para siempre —
|
|
67
|
+
* pantalla muerta. Si ya está prestado, la tarea corre y ya está: el terminal ya
|
|
68
|
+
* es de quien lo pidió primero.
|
|
69
|
+
*/
|
|
70
|
+
export async function conElTerminalPrestado(tarea) {
|
|
71
|
+
if (!prestar || yaPrestado)
|
|
72
|
+
return tarea();
|
|
73
|
+
let resultado;
|
|
74
|
+
let fallo = null;
|
|
75
|
+
yaPrestado = true;
|
|
76
|
+
try {
|
|
77
|
+
await prestar(async () => {
|
|
78
|
+
try {
|
|
79
|
+
resultado = await tarea();
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
fallo = e;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
yaPrestado = false;
|
|
88
|
+
}
|
|
89
|
+
if (fallo)
|
|
90
|
+
throw fallo;
|
|
91
|
+
return resultado;
|
|
92
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EL TECLADO SIGUE SIENDO NUESTRO, SE COMPRUEBE CUANDO SE COMPRUEBE
|
|
3
|
+
*
|
|
4
|
+
* EL FALLO, CON SUS PALABRAS
|
|
5
|
+
*
|
|
6
|
+
* "al poner un / y poner algo luego se queda trabado y ya no se puede escribir a
|
|
7
|
+
* menos que pongas enter". Y antes: "al cambiar de potencia se queda pegado y no
|
|
8
|
+
* deja escribir, tengo que presionar enter". Y antes: "escribí 2 prompts y al
|
|
9
|
+
* siguiente ya no me deja escribir nada, teclee lo que teclee".
|
|
10
|
+
*
|
|
11
|
+
* Es siempre el mismo, y el "se arregla con Enter" es la firma que lo delata: en
|
|
12
|
+
* modo cocido la consola guarda lo que tecleas y no lo suelta hasta el Enter. O
|
|
13
|
+
* sea que el teclado no está muerto — está en manos del terminal, y quien hace
|
|
14
|
+
* el eco es PowerShell, no la caja.
|
|
15
|
+
*
|
|
16
|
+
* POR QUE VUELVE UNA Y OTRA VEZ
|
|
17
|
+
*
|
|
18
|
+
* Porque hay muchos sitios que apagan el modo crudo, y todos con razón: un
|
|
19
|
+
* `InterruptWatcher` que se para tarde y restaura el estado anterior, un
|
|
20
|
+
* `readline` que al cerrarse hace `pause()` y `setRawMode(false)`, un proceso
|
|
21
|
+
* hijo que hereda la consola y la devuelve en cocido. Basta con que UNA de las
|
|
22
|
+
* veinte salidas se escape.
|
|
23
|
+
*
|
|
24
|
+
* Y HAY UNA FORMA QUE NODE NI SIQUIERA VE
|
|
25
|
+
*
|
|
26
|
+
* Cuando el que la cambia es un proceso hijo, `process.stdin.isRaw` se queda
|
|
27
|
+
* diciendo `true`. Por debajo, libuv guarda el modo que cree tener y empieza con
|
|
28
|
+
* `if (tty->mode == mode) return 0;`: a partir de ahí todos los `setRawMode(true)`
|
|
29
|
+
* son un no-op. Node cree que manda y no manda. Por eso hay una reafirmación
|
|
30
|
+
* —pasar por `false` y volver— que sí rompe esa caché.
|
|
31
|
+
*
|
|
32
|
+
* POR QUE ESTE ARCHIVO ES NUEVO
|
|
33
|
+
*
|
|
34
|
+
* El vigilante existía y funcionaba, pero vivía DENTRO del prompt clásico
|
|
35
|
+
* (ui/prompt.ts), que se abría y se cerraba en cada vuelta. Al mudarse a Ink y
|
|
36
|
+
* después al árbol único, el prompt dejó de abrirse y cerrarse... y el vigilante
|
|
37
|
+
* se quedó atrás. El fallo volvió, y volvió idéntico.
|
|
38
|
+
*
|
|
39
|
+
* Aquí vive mientras viva el árbol, que es toda la sesión.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* @param esperando ¿está la caja abierta esperando una orden?
|
|
43
|
+
*
|
|
44
|
+
* Se usa para reafirmar en el momento exacto en que se abre. Ese instante es el
|
|
45
|
+
* que sigue a un comando de barra, a un menú o a un proceso hijo: justo cuando
|
|
46
|
+
* la consola puede acabar de volver en cocido sin que Node se entere.
|
|
47
|
+
*/
|
|
48
|
+
export declare function useTecladoSiempreNuestro(esperando: boolean): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { MS_ENTRE_COMPROBACIONES, reafirmarModoCrudo, repararTeclado, } from "../keyboardGuard.js";
|
|
4
|
+
import { terminalPrestado } from "./prestamo.js";
|
|
5
|
+
/**
|
|
6
|
+
* EL TECLADO SIGUE SIENDO NUESTRO, SE COMPRUEBE CUANDO SE COMPRUEBE
|
|
7
|
+
*
|
|
8
|
+
* EL FALLO, CON SUS PALABRAS
|
|
9
|
+
*
|
|
10
|
+
* "al poner un / y poner algo luego se queda trabado y ya no se puede escribir a
|
|
11
|
+
* menos que pongas enter". Y antes: "al cambiar de potencia se queda pegado y no
|
|
12
|
+
* deja escribir, tengo que presionar enter". Y antes: "escribí 2 prompts y al
|
|
13
|
+
* siguiente ya no me deja escribir nada, teclee lo que teclee".
|
|
14
|
+
*
|
|
15
|
+
* Es siempre el mismo, y el "se arregla con Enter" es la firma que lo delata: en
|
|
16
|
+
* modo cocido la consola guarda lo que tecleas y no lo suelta hasta el Enter. O
|
|
17
|
+
* sea que el teclado no está muerto — está en manos del terminal, y quien hace
|
|
18
|
+
* el eco es PowerShell, no la caja.
|
|
19
|
+
*
|
|
20
|
+
* POR QUE VUELVE UNA Y OTRA VEZ
|
|
21
|
+
*
|
|
22
|
+
* Porque hay muchos sitios que apagan el modo crudo, y todos con razón: un
|
|
23
|
+
* `InterruptWatcher` que se para tarde y restaura el estado anterior, un
|
|
24
|
+
* `readline` que al cerrarse hace `pause()` y `setRawMode(false)`, un proceso
|
|
25
|
+
* hijo que hereda la consola y la devuelve en cocido. Basta con que UNA de las
|
|
26
|
+
* veinte salidas se escape.
|
|
27
|
+
*
|
|
28
|
+
* Y HAY UNA FORMA QUE NODE NI SIQUIERA VE
|
|
29
|
+
*
|
|
30
|
+
* Cuando el que la cambia es un proceso hijo, `process.stdin.isRaw` se queda
|
|
31
|
+
* diciendo `true`. Por debajo, libuv guarda el modo que cree tener y empieza con
|
|
32
|
+
* `if (tty->mode == mode) return 0;`: a partir de ahí todos los `setRawMode(true)`
|
|
33
|
+
* son un no-op. Node cree que manda y no manda. Por eso hay una reafirmación
|
|
34
|
+
* —pasar por `false` y volver— que sí rompe esa caché.
|
|
35
|
+
*
|
|
36
|
+
* POR QUE ESTE ARCHIVO ES NUEVO
|
|
37
|
+
*
|
|
38
|
+
* El vigilante existía y funcionaba, pero vivía DENTRO del prompt clásico
|
|
39
|
+
* (ui/prompt.ts), que se abría y se cerraba en cada vuelta. Al mudarse a Ink y
|
|
40
|
+
* después al árbol único, el prompt dejó de abrirse y cerrarse... y el vigilante
|
|
41
|
+
* se quedó atrás. El fallo volvió, y volvió idéntico.
|
|
42
|
+
*
|
|
43
|
+
* Aquí vive mientras viva el árbol, que es toda la sesión.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* @param esperando ¿está la caja abierta esperando una orden?
|
|
47
|
+
*
|
|
48
|
+
* Se usa para reafirmar en el momento exacto en que se abre. Ese instante es el
|
|
49
|
+
* que sigue a un comando de barra, a un menú o a un proceso hijo: justo cuando
|
|
50
|
+
* la consola puede acabar de volver en cocido sin que Node se entere.
|
|
51
|
+
*/
|
|
52
|
+
export function useTecladoSiempreNuestro(esperando) {
|
|
53
|
+
// Al abrir la caja se reafirma SIEMPRE, sin preguntar. `repararTeclado` solo
|
|
54
|
+
// actúa cuando SABE que se perdió el modo crudo, y este es justo el caso en el
|
|
55
|
+
// que no lo sabe.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!esperando)
|
|
58
|
+
return;
|
|
59
|
+
if (terminalPrestado())
|
|
60
|
+
return;
|
|
61
|
+
reafirmarModoCrudo(process.stdin);
|
|
62
|
+
avisar(`reafirmado al abrir la caja (isRaw decia ${process.stdin.isRaw})`);
|
|
63
|
+
}, [esperando]);
|
|
64
|
+
// Y cuatro veces por segundo, mientras el árbol viva, se comprueba que sigue
|
|
65
|
+
// siendo nuestro. No se reafirma en cada vuelta a propósito: pasar por `false`
|
|
66
|
+
// y volver cuatro veces por segundo es pedirle problemas a la consola por un
|
|
67
|
+
// caso raro. Aquí solo se repara lo que se ve roto.
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const vigilante = setInterval(() => {
|
|
70
|
+
// Mientras un diálogo tiene el terminal, el modo crudo es SUYO: tocarlo
|
|
71
|
+
// por detrás le quitaría las teclas justo cuando espera una respuesta.
|
|
72
|
+
if (terminalPrestado())
|
|
73
|
+
return;
|
|
74
|
+
const reparado = repararTeclado(process.stdin);
|
|
75
|
+
if (reparado.length > 0)
|
|
76
|
+
avisar(`recuperado: ${reparado.join(" y ")}`);
|
|
77
|
+
}, MS_ENTRE_COMPROBACIONES);
|
|
78
|
+
// Un temporizador de vigilancia no es motivo para que el proceso siga vivo.
|
|
79
|
+
vigilante.unref?.();
|
|
80
|
+
return () => clearInterval(vigilante);
|
|
81
|
+
}, []);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Se calla salvo que se pida verlo.
|
|
85
|
+
*
|
|
86
|
+
* Si esto llega a decir algo es que hay un fallo de verdad en quien soltó el
|
|
87
|
+
* teclado, y saber cuál de las veinte salidas fue vale más que el mensaje.
|
|
88
|
+
*/
|
|
89
|
+
function avisar(texto) {
|
|
90
|
+
if (process.env.CHOCOLATITO_TECLADO !== "debug")
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
process.stderr.write(`\n[teclado] ${texto}\n`);
|
|
94
|
+
}
|
|
95
|
+
catch { }
|
|
96
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
export interface LineaPermanente {
|
|
48
|
+
/** Estable y única. Es la `key` que <Static> necesita. */
|
|
49
|
+
id: number;
|
|
50
|
+
texto: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Entra texto del agente. Acepta trozos a media línea, que es lo normal.
|
|
54
|
+
*
|
|
55
|
+
* El `\r` suelto se descarta con su línea: en un flujo de markdown no es un
|
|
56
|
+
* retorno de carro de verdad, viene de un `\r\n` partido por la mitad entre dos
|
|
57
|
+
* trozos, y dejarlo pasar mete una línea vacía por cada salto.
|
|
58
|
+
*/
|
|
59
|
+
export declare function anotar(texto: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Las líneas que <Static> todavía no ha escrito.
|
|
62
|
+
*
|
|
63
|
+
* DEVUELVE UN ARRAY NUEVO CADA VEZ QUE HAY ALGO NUEVO, Y NO ES UN CAPRICHO
|
|
64
|
+
*
|
|
65
|
+
* Este fue el fallo que dejó la conversación entera sin escribirse, y costó
|
|
66
|
+
* encontrarlo porque no parecía un fallo: la primera versión devolvía el array
|
|
67
|
+
* interno tal cual, al que `anotar` le hace `push`.
|
|
68
|
+
*
|
|
69
|
+
* <Static> memoriza lo que va a emitir con `useMemo(() => items.slice(index),
|
|
70
|
+
* [items, index])` (Static.js:12-14). Con la MISMA referencia de array y el
|
|
71
|
+
* mismo índice, React da por buena la memoria y devuelve lo de antes: el `[]`
|
|
72
|
+
* del primer dibujado. El array crecía, la pantalla no. Nada de emitir de más
|
|
73
|
+
* ni de menos: sencillamente no se emitía NUNCA.
|
|
74
|
+
*
|
|
75
|
+
* Así que la referencia tiene que cambiar cuando cambia el contenido. Se
|
|
76
|
+
* reconstruye solo cuando ha crecido de verdad —una vez por línea terminada, no
|
|
77
|
+
* una por dibujado—, que a treinta fotogramas por segundo es la diferencia entre
|
|
78
|
+
* reconstruirlo treinta veces por segundo o ninguna mientras nadie escribe.
|
|
79
|
+
*/
|
|
80
|
+
export declare function pendientes(): LineaPermanente[];
|
|
81
|
+
/** El trozo que cuelga, el que pinta la zona de abajo. */
|
|
82
|
+
export declare function colgando(): string;
|
|
83
|
+
/**
|
|
84
|
+
* "Lo de hasta aquí ya está en el terminal."
|
|
85
|
+
*
|
|
86
|
+
* Se llama SOLO justo antes de desmontar el árbol. Llamarlo con el árbol vivo
|
|
87
|
+
* encogería la lista que ve <Static>, que es la regla 1 de arriba.
|
|
88
|
+
*/
|
|
89
|
+
export declare function darPorPintado(): void;
|
|
90
|
+
export declare function alCambiar(cb: () => void): () => void;
|
|
91
|
+
/** Para las pruebas. En producción la conversación no se tira nunca. */
|
|
92
|
+
export declare function vaciarTranscripcion(): void;
|
|
93
|
+
/** Cuántas líneas terminadas van. Para las pruebas y para medir. */
|
|
94
|
+
export declare function cuantasLineas(): number;
|
|
95
|
+
/**
|
|
96
|
+
* Recorta el trozo que cuelga para que quepa en las filas que le tocan.
|
|
97
|
+
*
|
|
98
|
+
* NO ES COSMÉTICO, ES LA RED DE SEGURIDAD DEL RENDIMIENTO
|
|
99
|
+
*
|
|
100
|
+
* En Windows, si el bloque de abajo llega a medir lo que la ventana, Ink deja de
|
|
101
|
+
* repintar por diferencias y pasa a `clearTerminal` + reimprimir la conversación
|
|
102
|
+
* ENTERA en cada fotograma (ink.js:100-102 y 769). Con el spinner a 30 por
|
|
103
|
+
* segundo eso es parpadeo, historial perdido y coste creciente con la sesión.
|
|
104
|
+
*
|
|
105
|
+
* El caso que lo dispara: un párrafo largo sin un solo salto de línea. Mientras
|
|
106
|
+
* no lo termine, ese párrafo entero es "lo que cuelga".
|
|
107
|
+
*
|
|
108
|
+
* Cuando hay que recortar se quitan también los colores: cortar una cadena por
|
|
109
|
+
* el medio parte los códigos de escape y lo que sale es basura de colores
|
|
110
|
+
* enganchada al resto de la sesión. Es un trozo que en cuanto llegue su salto de
|
|
111
|
+
* línea se reemplaza por la línea entera y bien pintada, así que el recorte no
|
|
112
|
+
* dura ni un parpadeo.
|
|
113
|
+
*/
|
|
114
|
+
export declare function recortarCola(texto: string, cols: number, filas: number): string[];
|