chocolatito-code 1.3.4 → 1.4.3
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/config/license.js +54 -2
- package/dist/ui/banner.js +3 -2
- package/dist/ui/entrada.d.ts +41 -0
- package/dist/ui/entrada.js +143 -0
- package/dist/ui/ink/Prompt.d.ts +42 -0
- package/dist/ui/ink/Prompt.js +188 -0
- package/dist/ui/ink/montarPrompt.d.ts +10 -0
- package/dist/ui/ink/montarPrompt.js +129 -0
- package/dist/ui/marco.js +17 -3
- package/dist/ui/prompt.js +53 -0
- package/dist/ui/selector.js +10 -0
- package/extension/manifest.json +1 -1
- package/package.json +5 -1
package/dist/config/license.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import readline from "node:readline";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { getGlobalConfigPath } from "./env.js";
|
|
7
|
+
import { CONFIG_FILE_NAME } from "./constants.js";
|
|
7
8
|
/**
|
|
8
9
|
* LICENCIAS
|
|
9
10
|
*
|
|
@@ -365,9 +366,10 @@ export async function ensureLicense() {
|
|
|
365
366
|
if (res.kind === "ok")
|
|
366
367
|
return true;
|
|
367
368
|
if (res.kind === "rejected") {
|
|
368
|
-
// Respuesta clara de Lemon Squeezy:
|
|
369
|
+
// Respuesta clara de Lemon Squeezy: esta clave ya no vale. Pero eso NO es
|
|
370
|
+
// motivo para cerrarle la puerta al que acaba de comprar otra.
|
|
369
371
|
console.log(chalk.red(`\n✖ ${res.reason}\n`));
|
|
370
|
-
return
|
|
372
|
+
return pedirClaveDeRecambio();
|
|
371
373
|
}
|
|
372
374
|
// No se pudo preguntar. Aqui es donde importa no castigar al cliente por una
|
|
373
375
|
// caida que no es suya.
|
|
@@ -382,6 +384,56 @@ export async function ensureLicense() {
|
|
|
382
384
|
chalk.gray("\n Conéctate a internet una vez para reanudarla.\n"));
|
|
383
385
|
return false;
|
|
384
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* LA PUERTA QUE SE CERRABA POR DENTRO
|
|
389
|
+
*
|
|
390
|
+
* Cuando la clave guardada dejaba de valer, esto imprimía el motivo y devolvía
|
|
391
|
+
* `false`. El programa se cerraba y no volvía a preguntar NUNCA: al arrancar de
|
|
392
|
+
* nuevo encontraba la misma clave guardada, la volvía a rechazar y se volvía a
|
|
393
|
+
* cerrar. La única salida era borrar a mano ~/.chocolatitorc, un archivo oculto
|
|
394
|
+
* que ningún cliente sabe que existe.
|
|
395
|
+
*
|
|
396
|
+
* Le pasó al dueño en su propio portátil, que es la peor forma de enterarse: lo
|
|
397
|
+
* tenía instalado con una licencia de MODO PRUEBA de Lemon Squeezy, compró la
|
|
398
|
+
* suscripción de verdad, y no hubo manera de meter la clave nueva. Desde fuera
|
|
399
|
+
* se ve como "compré y no funciona", que es exactamente el momento en que un
|
|
400
|
+
* cliente pide la devolución.
|
|
401
|
+
*
|
|
402
|
+
* Y no hace falta que sea una prueba: pasa igual con una suscripción que caducó
|
|
403
|
+
* y se renovó, con un equipo liberado desde el panel, o con un reembolso
|
|
404
|
+
* seguido de una compra nueva.
|
|
405
|
+
*
|
|
406
|
+
* Ahora se ofrece meter otra clave ahí mismo. Sin terminal delante —un script,
|
|
407
|
+
* una tubería— al menos se dice DÓNDE está el archivo, que es lo único
|
|
408
|
+
* accionable en ese caso.
|
|
409
|
+
*/
|
|
410
|
+
async function pedirClaveDeRecambio() {
|
|
411
|
+
const donde = path.join(os.homedir(), CONFIG_FILE_NAME);
|
|
412
|
+
if (!process.stdin.isTTY) {
|
|
413
|
+
console.log(chalk.gray(` La clave guardada está en ${donde}.`) +
|
|
414
|
+
chalk.gray("\n Bórrala y ejecuta 'chocolatito' en una terminal para introducir la nueva.\n"));
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
console.log(chalk.gray(" Si has comprado una suscripción nueva, pega aquí su clave y se cambia.") +
|
|
418
|
+
chalk.gray(`\n (Enter en blanco para salir. La anterior está guardada en ${donde}.)\n`));
|
|
419
|
+
const clave = await pedirClave();
|
|
420
|
+
if (!clave) {
|
|
421
|
+
console.log(chalk.gray("\n No se introdujo ninguna clave.\n"));
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
const nueva = await activate(clave);
|
|
425
|
+
if (nueva.kind === "ok") {
|
|
426
|
+
console.log(chalk.green(`\n✔ Licencia activada${nueva.license.customerEmail ? ` para ${nueva.license.customerEmail}` : ""}.\n`));
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
// Un solo intento. Encadenar preguntas hasta acertar convierte un fallo de
|
|
430
|
+
// conexión en un bucle del que no se sale con Ctrl+C limpio.
|
|
431
|
+
const motivo = nueva.kind === "rejected" || nueva.kind === "unpersisted"
|
|
432
|
+
? nueva.reason
|
|
433
|
+
: `No se pudo activar (${nueva.reason}).`;
|
|
434
|
+
console.log(chalk.red(`\n✖ ${motivo}\n`));
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
385
437
|
/** Texto del comando /licencia. */
|
|
386
438
|
export function licenseStatus() {
|
|
387
439
|
const l = getStoredLicense();
|
package/dist/ui/banner.js
CHANGED
|
@@ -123,7 +123,8 @@ export const FILAS_MINIMAS_PARA_ANCLAR = 16;
|
|
|
123
123
|
* Filas que se imprimen DESPUES del banner y hay que dejarles sitio.
|
|
124
124
|
*
|
|
125
125
|
* Son el aviso del atajo (su texto ya trae un salto dentro, mas el que anade
|
|
126
|
-
* console.log: 2 filas) y el marco del prompt
|
|
126
|
+
* console.log: 2 filas) y el marco del prompt, que son cinco: una de aire y
|
|
127
|
+
* despues barra, entrada, barra y estado.
|
|
127
128
|
*
|
|
128
129
|
* AQUI ESTABA EL FALLO DEL ZORRO CORTADO
|
|
129
130
|
*
|
|
@@ -132,7 +133,7 @@ export const FILAS_MINIMAS_PARA_ANCLAR = 16;
|
|
|
132
133
|
* arriba la cabeza del zorro. El usuario lo vio en cuanto abrio: "hay mucho
|
|
133
134
|
* espacio y se baja y se corta el zorrito al iniciar".
|
|
134
135
|
*/
|
|
135
|
-
const FILAS_DESPUES_DEL_BANNER =
|
|
136
|
+
const FILAS_DESPUES_DEL_BANNER = 7;
|
|
136
137
|
/**
|
|
137
138
|
* Filas en blanco que hay que soltar ANTES de `contenido` para que el prompt
|
|
138
139
|
* acabe pegado al fondo de la ventana. Cero si no cabe o no hace falta.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { DropdownItem } from "./marco.js";
|
|
2
|
+
/**
|
|
3
|
+
* LA LÓGICA DEL PROMPT, SIN TERMINAL DELANTE
|
|
4
|
+
*
|
|
5
|
+
* Todo esto vivía dentro del closure de `askInteractivePrompt`, mezclado con el
|
|
6
|
+
* dibujo, el modo crudo y el oyente de teclas. Mientras hubo una sola forma de
|
|
7
|
+
* pintar el prompt eso daba igual; con dos —la de siempre y la de Ink— habría
|
|
8
|
+
* significado escribir dos veces el autocompletado, el historial y la detección
|
|
9
|
+
* de menciones, y que se separaran al primer arreglo.
|
|
10
|
+
*
|
|
11
|
+
* Aquí no hay nada que sepa de terminales: entra texto y una posición de
|
|
12
|
+
* cursor, sale qué ofrecer. Por eso se puede comprobar sin abrir nada.
|
|
13
|
+
*/
|
|
14
|
+
/** Una arroba a medio escribir justo antes del cursor. */
|
|
15
|
+
export declare function mencionEnCurso(texto: string, cursor: number): {
|
|
16
|
+
activa: boolean;
|
|
17
|
+
consulta: string;
|
|
18
|
+
desde: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Archivos del proyecto para el autocompletado de "@".
|
|
22
|
+
*
|
|
23
|
+
* Tope de resultados y de profundidad a propósito: esto corre en cada
|
|
24
|
+
* pulsación, y recorrer un monorepo entero por cada letra se nota.
|
|
25
|
+
*/
|
|
26
|
+
export declare function archivosDelProyecto(dir: string, maximo?: number): string[];
|
|
27
|
+
/** Lo que se ofrece debajo de la entrada, o nada si no toca ofrecer. */
|
|
28
|
+
export declare function opcionesDelDesplegable(texto: string, cursor: number, archivos: string[]): DropdownItem[];
|
|
29
|
+
/**
|
|
30
|
+
* Aplica la opción elegida. Devuelve el texto y el cursor nuevos, o `null` si
|
|
31
|
+
* no había nada que aplicar.
|
|
32
|
+
*
|
|
33
|
+
* Devuelve valores en vez de mutar: así el componente de Ink puede meterlo en
|
|
34
|
+
* su estado sin que nada cambie a sus espaldas entre dos dibujados.
|
|
35
|
+
*/
|
|
36
|
+
export declare function aplicarOpcion(texto: string, cursor: number, opcion: DropdownItem | undefined): {
|
|
37
|
+
texto: string;
|
|
38
|
+
cursor: number;
|
|
39
|
+
} | null;
|
|
40
|
+
export declare function leerHistorial(): string[];
|
|
41
|
+
export declare function apuntarEnHistorial(orden: string): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { SLASH_COMMANDS } from "./comandos.js";
|
|
5
|
+
/**
|
|
6
|
+
* LA LÓGICA DEL PROMPT, SIN TERMINAL DELANTE
|
|
7
|
+
*
|
|
8
|
+
* Todo esto vivía dentro del closure de `askInteractivePrompt`, mezclado con el
|
|
9
|
+
* dibujo, el modo crudo y el oyente de teclas. Mientras hubo una sola forma de
|
|
10
|
+
* pintar el prompt eso daba igual; con dos —la de siempre y la de Ink— habría
|
|
11
|
+
* significado escribir dos veces el autocompletado, el historial y la detección
|
|
12
|
+
* de menciones, y que se separaran al primer arreglo.
|
|
13
|
+
*
|
|
14
|
+
* Aquí no hay nada que sepa de terminales: entra texto y una posición de
|
|
15
|
+
* cursor, sale qué ofrecer. Por eso se puede comprobar sin abrir nada.
|
|
16
|
+
*/
|
|
17
|
+
/** Una arroba a medio escribir justo antes del cursor. */
|
|
18
|
+
export function mencionEnCurso(texto, cursor) {
|
|
19
|
+
const antes = texto.slice(0, cursor);
|
|
20
|
+
const m = antes.match(/@([^\s]*)$/);
|
|
21
|
+
if (m && m.index !== undefined) {
|
|
22
|
+
return { activa: true, consulta: m[1].toLowerCase(), desde: m.index };
|
|
23
|
+
}
|
|
24
|
+
return { activa: false, consulta: "", desde: -1 };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Archivos del proyecto para el autocompletado de "@".
|
|
28
|
+
*
|
|
29
|
+
* Tope de resultados y de profundidad a propósito: esto corre en cada
|
|
30
|
+
* pulsación, y recorrer un monorepo entero por cada letra se nota.
|
|
31
|
+
*/
|
|
32
|
+
export function archivosDelProyecto(dir, maximo = 40) {
|
|
33
|
+
const encontrados = [];
|
|
34
|
+
const ignorar = new Set([
|
|
35
|
+
"node_modules",
|
|
36
|
+
".git",
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
".next",
|
|
40
|
+
"out",
|
|
41
|
+
"venv",
|
|
42
|
+
"__pycache__",
|
|
43
|
+
".turbo",
|
|
44
|
+
]);
|
|
45
|
+
function recorrer(actual, hondo = 0) {
|
|
46
|
+
if (hondo > 6 || encontrados.length >= maximo)
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
for (const ent of fs.readdirSync(actual, { withFileTypes: true })) {
|
|
50
|
+
if (ent.name.startsWith(".") && ent.name !== ".env")
|
|
51
|
+
continue;
|
|
52
|
+
if (ignorar.has(ent.name))
|
|
53
|
+
continue;
|
|
54
|
+
const completo = path.join(actual, ent.name);
|
|
55
|
+
if (ent.isDirectory()) {
|
|
56
|
+
recorrer(completo, hondo + 1);
|
|
57
|
+
}
|
|
58
|
+
else if (ent.isFile()) {
|
|
59
|
+
encontrados.push(path.relative(dir, completo).replace(/\\/g, "/"));
|
|
60
|
+
if (encontrados.length >= maximo)
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch { }
|
|
66
|
+
}
|
|
67
|
+
recorrer(dir);
|
|
68
|
+
return encontrados;
|
|
69
|
+
}
|
|
70
|
+
/** Lo que se ofrece debajo de la entrada, o nada si no toca ofrecer. */
|
|
71
|
+
export function opcionesDelDesplegable(texto, cursor, archivos) {
|
|
72
|
+
if (texto.startsWith("/")) {
|
|
73
|
+
const consulta = texto.toLowerCase();
|
|
74
|
+
return SLASH_COMMANDS.filter((c) => c.name.toLowerCase().startsWith(consulta) || consulta === "/").map((c) => ({
|
|
75
|
+
type: "command",
|
|
76
|
+
label: `${c.name}${c.args ? " " + c.args : ""}`,
|
|
77
|
+
secondary: c.description,
|
|
78
|
+
insertText: c.name,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
const arroba = mencionEnCurso(texto, cursor);
|
|
82
|
+
if (arroba.activa) {
|
|
83
|
+
return archivos
|
|
84
|
+
.filter((f) => f.toLowerCase().includes(arroba.consulta))
|
|
85
|
+
.slice(0, 8)
|
|
86
|
+
.map((f) => ({
|
|
87
|
+
type: "file",
|
|
88
|
+
label: f,
|
|
89
|
+
secondary: "Archivo del proyecto",
|
|
90
|
+
insertText: `@${f} `,
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Aplica la opción elegida. Devuelve el texto y el cursor nuevos, o `null` si
|
|
97
|
+
* no había nada que aplicar.
|
|
98
|
+
*
|
|
99
|
+
* Devuelve valores en vez de mutar: así el componente de Ink puede meterlo en
|
|
100
|
+
* su estado sin que nada cambie a sus espaldas entre dos dibujados.
|
|
101
|
+
*/
|
|
102
|
+
export function aplicarOpcion(texto, cursor, opcion) {
|
|
103
|
+
if (!opcion)
|
|
104
|
+
return null;
|
|
105
|
+
if (opcion.type === "command") {
|
|
106
|
+
return { texto: opcion.insertText, cursor: opcion.insertText.length };
|
|
107
|
+
}
|
|
108
|
+
const arroba = mencionEnCurso(texto, cursor);
|
|
109
|
+
if (!arroba.activa)
|
|
110
|
+
return null;
|
|
111
|
+
const nuevo = texto.slice(0, arroba.desde) + opcion.insertText + texto.slice(cursor);
|
|
112
|
+
return { texto: nuevo, cursor: arroba.desde + opcion.insertText.length };
|
|
113
|
+
}
|
|
114
|
+
// ------------------------------------------------------------------ historial
|
|
115
|
+
function archivoDeHistorial() {
|
|
116
|
+
const dir = path.join(os.homedir(), ".chocolatito");
|
|
117
|
+
if (!fs.existsSync(dir))
|
|
118
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
119
|
+
return path.join(dir, "history");
|
|
120
|
+
}
|
|
121
|
+
export function leerHistorial() {
|
|
122
|
+
try {
|
|
123
|
+
const f = archivoDeHistorial();
|
|
124
|
+
if (!fs.existsSync(f))
|
|
125
|
+
return [];
|
|
126
|
+
return fs
|
|
127
|
+
.readFileSync(f, "utf-8")
|
|
128
|
+
.split("\n")
|
|
129
|
+
.filter((l) => l.trim().length > 0);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Un historial ilegible no puede impedir escribir: se sigue sin él.
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export function apuntarEnHistorial(orden) {
|
|
137
|
+
if (!orden.trim())
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
fs.appendFileSync(archivoDeHistorial(), `${orden.trim()}\n`, "utf-8");
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* EL PROMPT, DIBUJADO POR INK
|
|
4
|
+
*
|
|
5
|
+
* POR QUE SE CAMBIÓ
|
|
6
|
+
*
|
|
7
|
+
* La versión de siempre lo hacía todo a mano: pedía el modo crudo, escuchaba
|
|
8
|
+
* `keypress` de readline, contaba filas físicas, subía el cursor con `\x1b[nA` y
|
|
9
|
+
* repintaba. Funcionaba, pero cada arreglo destapaba otro agujero —el marco que
|
|
10
|
+
* bajaba una fila por pulsación, el cursor detrás del acento, el teclado que se
|
|
11
|
+
* quedaba en modo cocido después de un hijo—, y todos eran del MISMO tipo:
|
|
12
|
+
* llevar a mano una cuenta que el terminal también lleva.
|
|
13
|
+
*
|
|
14
|
+
* Ink se queda con esa parte. El modo crudo lo pide y lo devuelve él, con cuenta
|
|
15
|
+
* de referencias, y al desmontar lo restaura pase lo que pase. El repintado lo
|
|
16
|
+
* hace por diferencias, así que no hay filas que contar.
|
|
17
|
+
*
|
|
18
|
+
* LO QUE NO CAMBIA, Y ES DELIBERADO
|
|
19
|
+
*
|
|
20
|
+
* El aspecto sale del MISMO `buildPromptFrame` de antes. No se ha reescrito el
|
|
21
|
+
* marco en cajas de Ink: se pinta la lista de líneas que ya devolvía. Así el
|
|
22
|
+
* cambio no puede alterar ni una columna de lo que se ve, y las pruebas del
|
|
23
|
+
* marco —anchos, recortes, el modo a la izquierda, el motor a la derecha— siguen
|
|
24
|
+
* valiendo como red mientras se cambia lo de debajo.
|
|
25
|
+
*
|
|
26
|
+
* EL CURSOR
|
|
27
|
+
*
|
|
28
|
+
* Ink no mueve el cursor del terminal: lo esconde. Por eso el cursor se pinta
|
|
29
|
+
* como un carácter más dentro del texto, igual que ya se hacía en la barra
|
|
30
|
+
* mientras el agente trabaja.
|
|
31
|
+
*/
|
|
32
|
+
export interface PromptProps {
|
|
33
|
+
/** Se llama UNA vez, con la orden ya expandida. */
|
|
34
|
+
alEnviar: (orden: string) => void;
|
|
35
|
+
/** Ctrl+C: cerrar la sesión. */
|
|
36
|
+
alCancelar: () => void;
|
|
37
|
+
cwd: string;
|
|
38
|
+
promptSymbol: string;
|
|
39
|
+
/** Mensajes que llegaron tarde y hay que dejar por encima del marco. */
|
|
40
|
+
avisos: string[];
|
|
41
|
+
}
|
|
42
|
+
export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, }: PromptProps): React.ReactElement;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useMemo, useRef } from "react";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
|
+
import { buildPromptFrame } from "../marco.js";
|
|
5
|
+
import { cycleMode } from "../modes.js";
|
|
6
|
+
import { aplicarOpcion, archivosDelProyecto, leerHistorial, mencionEnCurso, opcionesDelDesplegable, } from "../entrada.js";
|
|
7
|
+
import { esGrande, guardarPegado, marcaQueTerminaEn } from "../pegados.js";
|
|
8
|
+
/** Marca del cursor. Ink esconde el de verdad, así que este se dibuja. */
|
|
9
|
+
const CARET = "█";
|
|
10
|
+
export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, }) {
|
|
11
|
+
const { stdout } = useStdout();
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
const [texto, setTexto] = useState("");
|
|
14
|
+
const [cursor, setCursor] = useState(0);
|
|
15
|
+
const [elegido, setElegido] = useState(0);
|
|
16
|
+
/** Contador para forzar un dibujado cuando cambia algo de fuera de React. */
|
|
17
|
+
const [, setRedibujar] = useState(0);
|
|
18
|
+
const historial = useRef(leerHistorial());
|
|
19
|
+
const [posHistorial, setPosHistorial] = useState(historial.current.length);
|
|
20
|
+
// Se escanea UNA vez por prompt, no en cada tecla: recorrer el proyecto entero
|
|
21
|
+
// por cada letra se nota en cuanto el proyecto pasa de mediano.
|
|
22
|
+
const archivos = useMemo(() => {
|
|
23
|
+
try {
|
|
24
|
+
return archivosDelProyecto(cwd);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}, [cwd]);
|
|
30
|
+
const opciones = opcionesDelDesplegable(texto, cursor, archivos);
|
|
31
|
+
const indice = opciones.length > 0 ? ((elegido % opciones.length) + opciones.length) % opciones.length : 0;
|
|
32
|
+
function enviar() {
|
|
33
|
+
const orden = texto.trim();
|
|
34
|
+
if (!orden)
|
|
35
|
+
return;
|
|
36
|
+
alEnviar(orden);
|
|
37
|
+
exit();
|
|
38
|
+
}
|
|
39
|
+
useInput((entrada, tecla) => {
|
|
40
|
+
// ------------------------------------------------------------- salir
|
|
41
|
+
if (tecla.ctrl && entrada === "c") {
|
|
42
|
+
alCancelar();
|
|
43
|
+
exit();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// ------------------------------------------- cambiar el modo de permisos
|
|
47
|
+
if (tecla.tab && tecla.shift) {
|
|
48
|
+
cycleMode();
|
|
49
|
+
// El modo NO vive en el estado de React: lo guarda modes.ts, que lo
|
|
50
|
+
// comparten el prompt, el vigilante de teclas y el dialogo de permisos.
|
|
51
|
+
// Sin este empujon, React no tiene motivo para volver a dibujar y la barra
|
|
52
|
+
// se queda anunciando el modo anterior mientras el de verdad ya cambio.
|
|
53
|
+
setRedibujar((n) => n + 1);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// ------------------------------------------------------------- enviar
|
|
57
|
+
if (tecla.return) {
|
|
58
|
+
// La continuación con barra invertida mete un salto de verdad.
|
|
59
|
+
if (texto.endsWith("\\")) {
|
|
60
|
+
setTexto(texto.slice(0, -1) + "\n");
|
|
61
|
+
setCursor(texto.length);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// El desplegable manda sobre lo tecleado: si en pantalla está señalado
|
|
65
|
+
// "/effort" y has escrito "/e", se envía lo que se ve. Ver la nota larga
|
|
66
|
+
// en la versión de siempre; esto es el mismo criterio.
|
|
67
|
+
const arroba = mencionEnCurso(texto, cursor).activa;
|
|
68
|
+
const escribiendoComando = texto.startsWith("/") && !texto.includes(" ");
|
|
69
|
+
const soloLaBarra = texto === "/";
|
|
70
|
+
if (opciones.length > 0 && (escribiendoComando || arroba)) {
|
|
71
|
+
const opcion = opciones[indice];
|
|
72
|
+
const yaEntero = Boolean(opcion) && texto === opcion.insertText;
|
|
73
|
+
if (!yaEntero) {
|
|
74
|
+
const aplicado = aplicarOpcion(texto, cursor, opcion);
|
|
75
|
+
if (aplicado) {
|
|
76
|
+
setTexto(aplicado.texto);
|
|
77
|
+
setCursor(aplicado.cursor);
|
|
78
|
+
setElegido(0);
|
|
79
|
+
// Una mención lleva más texto detrás; con solo "/" no has elegido
|
|
80
|
+
// nada todavía. En los dos casos se completa y se espera.
|
|
81
|
+
if (arroba || soloLaBarra)
|
|
82
|
+
return;
|
|
83
|
+
alEnviar(aplicado.texto.trim());
|
|
84
|
+
exit();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
enviar();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
// ------------------------------------------------------- autocompletar
|
|
93
|
+
if (tecla.tab && opciones.length > 0) {
|
|
94
|
+
const aplicado = aplicarOpcion(texto, cursor, opciones[indice]);
|
|
95
|
+
if (aplicado) {
|
|
96
|
+
setTexto(aplicado.texto);
|
|
97
|
+
setCursor(aplicado.cursor);
|
|
98
|
+
setElegido(0);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// ------------------------------------------------ moverse por la lista
|
|
103
|
+
if (tecla.upArrow) {
|
|
104
|
+
if (opciones.length > 0) {
|
|
105
|
+
setElegido(indice - 1);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (historial.current.length > 0 && posHistorial > 0) {
|
|
109
|
+
const nueva = posHistorial - 1;
|
|
110
|
+
setPosHistorial(nueva);
|
|
111
|
+
const linea = historial.current[nueva] || "";
|
|
112
|
+
setTexto(linea);
|
|
113
|
+
setCursor(linea.length);
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (tecla.downArrow) {
|
|
118
|
+
if (opciones.length > 0) {
|
|
119
|
+
setElegido(indice + 1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (posHistorial < historial.current.length - 1) {
|
|
123
|
+
const nueva = posHistorial + 1;
|
|
124
|
+
setPosHistorial(nueva);
|
|
125
|
+
const linea = historial.current[nueva] || "";
|
|
126
|
+
setTexto(linea);
|
|
127
|
+
setCursor(linea.length);
|
|
128
|
+
}
|
|
129
|
+
else if (posHistorial === historial.current.length - 1) {
|
|
130
|
+
// Bajar desde la última deja la línea en blanco, que es de donde se
|
|
131
|
+
// había salido al empezar a navegar.
|
|
132
|
+
setPosHistorial(historial.current.length);
|
|
133
|
+
setTexto("");
|
|
134
|
+
setCursor(0);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// ------------------------------------------------- moverse por el texto
|
|
139
|
+
if (tecla.leftArrow) {
|
|
140
|
+
setCursor(Math.max(0, cursor - 1));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (tecla.rightArrow) {
|
|
144
|
+
setCursor(Math.min(texto.length, cursor + 1));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// --------------------------------------------------------------- borrar
|
|
148
|
+
if (tecla.backspace || tecla.delete) {
|
|
149
|
+
if (cursor === 0)
|
|
150
|
+
return;
|
|
151
|
+
// Una marca de pegado se borra ENTERA. Dejar "[pegado #1 · 347 line" a
|
|
152
|
+
// medias no significa nada y al enviar se expandiría a la nada.
|
|
153
|
+
const marca = marcaQueTerminaEn(texto, cursor);
|
|
154
|
+
const cuantos = marca ? cursor - marca.inicio : 1;
|
|
155
|
+
setTexto(texto.slice(0, cursor - cuantos) + texto.slice(cursor));
|
|
156
|
+
setCursor(cursor - cuantos);
|
|
157
|
+
setElegido(0);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// ---------------------------------------------------------------- texto
|
|
161
|
+
//
|
|
162
|
+
// Ink entrega el pegado como UN solo trozo, así que aquí no hace falta nada
|
|
163
|
+
// del baile de antes con el modo de pegado entre corchetes ni con esperar
|
|
164
|
+
// al siguiente tick para saber si el Enter era un salto del pegado: si
|
|
165
|
+
// llegan varias líneas juntas, es un pegado y punto.
|
|
166
|
+
if (entrada && !tecla.ctrl && !tecla.meta) {
|
|
167
|
+
const limpio = entrada.replace(/\r\n?/g, "\n");
|
|
168
|
+
const trozo = esGrande(limpio) ? guardarPegado(limpio) : limpio;
|
|
169
|
+
setTexto(texto.slice(0, cursor) + trozo + texto.slice(cursor));
|
|
170
|
+
setCursor(cursor + trozo.length);
|
|
171
|
+
setElegido(0);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
const cols = Math.max(20, stdout?.columns || 80);
|
|
175
|
+
const rows = stdout?.rows || 24;
|
|
176
|
+
// El cursor se dibuja: Ink esconde el del terminal.
|
|
177
|
+
const conCaret = texto.slice(0, cursor) + CARET + texto.slice(cursor);
|
|
178
|
+
const marco = buildPromptFrame({
|
|
179
|
+
cols,
|
|
180
|
+
rows,
|
|
181
|
+
promptSymbol,
|
|
182
|
+
input: conCaret,
|
|
183
|
+
cursorIndex: cursor,
|
|
184
|
+
items: opciones,
|
|
185
|
+
selectedIndex: indice,
|
|
186
|
+
});
|
|
187
|
+
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)))] }));
|
|
188
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pide una orden al usuario. Misma firma que la versión de siempre.
|
|
3
|
+
*
|
|
4
|
+
* Devuelve el texto ya expandido: las marcas de pegado vuelven a ser el texto
|
|
5
|
+
* entero antes de que nadie lo vea, porque lo que el modelo tiene que recibir es
|
|
6
|
+
* lo que el usuario pegó, no "[pegado #1 · 347 lineas]".
|
|
7
|
+
*/
|
|
8
|
+
export declare function preguntarConInk(promptSymbol?: string, cwd?: string): Promise<string>;
|
|
9
|
+
export declare function hayPromptDeInk(): boolean;
|
|
10
|
+
export declare function avisarSobreElPromptDeInk(texto: string): void;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
import { Prompt } from "./Prompt.js";
|
|
6
|
+
import { expandirPegados, limpiarPegados } from "../pegados.js";
|
|
7
|
+
import { apuntarEnHistorial } from "../entrada.js";
|
|
8
|
+
import { soltarPie } from "../pieFijo.js";
|
|
9
|
+
import { clearTyped } from "../typeAhead.js";
|
|
10
|
+
/**
|
|
11
|
+
* EL PUENTE ENTRE INK Y EL BUCLE DE SIEMPRE
|
|
12
|
+
*
|
|
13
|
+
* El programa es un `while` que espera una orden: `const orden = await
|
|
14
|
+
* askInteractivePrompt()`. Ink, en cambio, es un árbol que se monta y vive.
|
|
15
|
+
*
|
|
16
|
+
* Aquí se casan las dos formas: se monta Ink, se espera a que el componente
|
|
17
|
+
* diga que hay orden, se desmonta y se resuelve la promesa. Para el bucle no ha
|
|
18
|
+
* cambiado nada —sigue habiendo una función que devuelve un texto— y por eso
|
|
19
|
+
* este cambio no toca index.ts ni una línea.
|
|
20
|
+
*
|
|
21
|
+
* POR QUE SE MONTA Y SE DESMONTA EN CADA VUELTA
|
|
22
|
+
*
|
|
23
|
+
* Podría quedarse montado toda la sesión, pero entonces Ink tendría que ser
|
|
24
|
+
* dueño TAMBIÉN de todo lo que el agente imprime mientras trabaja: las
|
|
25
|
+
* herramientas, el markdown, el pie. Eso es el resto de la mudanza, no ésta.
|
|
26
|
+
* Montando por vuelta, Ink manda solo mientras se escribe —que es donde estaban
|
|
27
|
+
* los fallos— y suelta el terminal en cuanto empieza el turno.
|
|
28
|
+
*
|
|
29
|
+
* El desmontaje devuelve el modo del teclado con su propia cuenta de
|
|
30
|
+
* referencias. Es justamente lo que aquí se llevaba a mano y fallaba.
|
|
31
|
+
*/
|
|
32
|
+
/** Lo que se teclea mientras el agente trabaja; aquí ya no vale. */
|
|
33
|
+
function limpiarRestos() {
|
|
34
|
+
clearTyped();
|
|
35
|
+
limpiarPegados();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Envuelve al prompt para que los mensajes que llegan tarde sean estado de
|
|
39
|
+
* React y no escrituras sueltas a stdout.
|
|
40
|
+
*
|
|
41
|
+
* Sin esto, un servidor MCP que termina de conectarse a destiempo escribiría
|
|
42
|
+
* justo encima del marco: Ink no se entera de lo que no pasa por su árbol, y en
|
|
43
|
+
* el siguiente dibujado la cuenta de filas ya no cuadra.
|
|
44
|
+
*/
|
|
45
|
+
function Anfitrion({ cwd, promptSymbol, resolver, cancelar, registrarAviso, }) {
|
|
46
|
+
const [avisos, setAvisos] = useState([]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
registrarAviso((texto) => {
|
|
49
|
+
setAvisos((previos) => [...previos, texto.replace(/\n$/, "")]);
|
|
50
|
+
});
|
|
51
|
+
}, [registrarAviso]);
|
|
52
|
+
return (_jsx(Prompt, { alEnviar: resolver, alCancelar: cancelar, cwd: cwd, promptSymbol: promptSymbol, avisos: avisos }));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pide una orden al usuario. Misma firma que la versión de siempre.
|
|
56
|
+
*
|
|
57
|
+
* Devuelve el texto ya expandido: las marcas de pegado vuelven a ser el texto
|
|
58
|
+
* entero antes de que nadie lo vea, porque lo que el modelo tiene que recibir es
|
|
59
|
+
* lo que el usuario pegó, no "[pegado #1 · 347 lineas]".
|
|
60
|
+
*/
|
|
61
|
+
export async function preguntarConInk(promptSymbol = "❯", cwd = process.cwd()) {
|
|
62
|
+
// El pie tenía pintado el marco mientras el agente trabajaba. Se borra antes
|
|
63
|
+
// de que Ink empiece a contar: dos dueños del mismo trozo de pantalla se
|
|
64
|
+
// pisan, y el que pierde deja filas huérfanas.
|
|
65
|
+
soltarPie();
|
|
66
|
+
limpiarRestos();
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
let entregado = false;
|
|
69
|
+
let empujarAviso = null;
|
|
70
|
+
// Los avisos tardíos entran por aquí mientras el prompt esté montado.
|
|
71
|
+
escribirTardio = (texto) => {
|
|
72
|
+
if (empujarAviso)
|
|
73
|
+
empujarAviso(texto);
|
|
74
|
+
};
|
|
75
|
+
const terminar = (orden) => {
|
|
76
|
+
if (entregado)
|
|
77
|
+
return;
|
|
78
|
+
entregado = true;
|
|
79
|
+
resolve(orden);
|
|
80
|
+
};
|
|
81
|
+
const instancia = render(_jsx(Anfitrion, { cwd: cwd, promptSymbol: promptSymbol, resolver: (orden) => {
|
|
82
|
+
const entero = expandirPegados(orden);
|
|
83
|
+
apuntarEnHistorial(entero);
|
|
84
|
+
terminar(entero);
|
|
85
|
+
}, cancelar: () => {
|
|
86
|
+
terminar("/exit");
|
|
87
|
+
}, registrarAviso: (empujar) => {
|
|
88
|
+
empujarAviso = empujar;
|
|
89
|
+
} }));
|
|
90
|
+
instancia.waitUntilExit().then(() => {
|
|
91
|
+
escribirTardio = null;
|
|
92
|
+
limpiarPegados();
|
|
93
|
+
// EL PROGRAMA SE CERRABA SOLO AL ELEGIR /effort
|
|
94
|
+
//
|
|
95
|
+
// Ink desreferencia stdin al desmontarse (stdin.unref) para no dejar el
|
|
96
|
+
// proceso vivo por una entrada que ya nadie escucha. Muy razonable... pero
|
|
97
|
+
// lo que viene DESPUES del prompt tambien lee el teclado: el menu de
|
|
98
|
+
// /effort, el dialogo de permisos. Con stdin sin referencia, esos se
|
|
99
|
+
// quedan esperando una tecla mientras Node ve el bucle de eventos vacio y
|
|
100
|
+
// se cierra.
|
|
101
|
+
//
|
|
102
|
+
// Visto por el usuario: escribia "/efor", pulsaba Enter sobre la
|
|
103
|
+
// recomendacion, y Chocolatito desaparecia dejandolo en la ruta de la
|
|
104
|
+
// consola. Ni error ni despedida: como si nunca hubiera estado.
|
|
105
|
+
//
|
|
106
|
+
// Se vuelve a referenciar aqui, que es el punto por el que se pasa SIEMPRE
|
|
107
|
+
// al cerrar el prompt, venga la orden de donde venga.
|
|
108
|
+
process.stdin.ref?.();
|
|
109
|
+
// Si el árbol muere sin haber entregado nada -una excepción, un Ctrl+C
|
|
110
|
+
// del sistema-, la promesa no puede quedarse colgada: el bucle se
|
|
111
|
+
// detendría para siempre sin decir por qué.
|
|
112
|
+
terminar("");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Por dónde entran los mensajes que llegan mientras el prompt está abierto.
|
|
118
|
+
*
|
|
119
|
+
* `null` cuando no hay prompt montado: entonces quien avise que escriba por
|
|
120
|
+
* donde escriba siempre, que no hay nada que romper.
|
|
121
|
+
*/
|
|
122
|
+
let escribirTardio = null;
|
|
123
|
+
export function hayPromptDeInk() {
|
|
124
|
+
return escribirTardio !== null;
|
|
125
|
+
}
|
|
126
|
+
export function avisarSobreElPromptDeInk(texto) {
|
|
127
|
+
if (escribirTardio)
|
|
128
|
+
escribirTardio(texto);
|
|
129
|
+
}
|
package/dist/ui/marco.js
CHANGED
|
@@ -91,7 +91,10 @@ export function buildPromptFrame(opts) {
|
|
|
91
91
|
// 2. Desplegable. Un marco mas alto que la ventana obliga al terminal a
|
|
92
92
|
// desplazar el contenido, y a partir de ahi el repintado ya no sabe donde
|
|
93
93
|
// esta su primera fila: se muestra solo una ventana alrededor del elegido.
|
|
94
|
-
|
|
94
|
+
// El 1 de delante es la fila de aire; despues barra, entrada, barra y pie.
|
|
95
|
+
// Sin contarla, el desplegable pide una fila mas de las que caben y el marco
|
|
96
|
+
// se sale por arriba de la ventana.
|
|
97
|
+
const room = Math.max(1, viewportRows - 1 - (1 + 1 + inputRows + 1 + 1));
|
|
95
98
|
const shown = Math.min(items.length, room);
|
|
96
99
|
const dropdownOffset = shown > 0 && items.length > shown
|
|
97
100
|
? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
|
|
@@ -184,13 +187,24 @@ export function buildPromptFrame(opts) {
|
|
|
184
187
|
else {
|
|
185
188
|
footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, util));
|
|
186
189
|
}
|
|
187
|
-
|
|
190
|
+
// UNA FILA DE AIRE ENCIMA DE LA CAJA
|
|
191
|
+
//
|
|
192
|
+
// Al anclar el prompt al fondo, el marco quedo pegado a lo de arriba: la
|
|
193
|
+
// ultima linea de la respuesta y la barra superior de la caja, seguidas. Se
|
|
194
|
+
// lee apelotonado y cuesta ver donde acaba una cosa y empieza la otra. El
|
|
195
|
+
// usuario lo pidio asi: "ponle un poquito de espacio pero no tanto".
|
|
196
|
+
//
|
|
197
|
+
// Una fila, no dos: dos ya son un hueco, y en una ventana corta cada fila que
|
|
198
|
+
// se gasta en aire es una que no se ve de conversacion.
|
|
199
|
+
const lines = ["", bar, inputLine, ...dropdownLines, bar, footerLine];
|
|
188
200
|
// 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
|
|
189
201
|
// total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
|
|
190
202
|
// cuanto la entrada se partia el acento se iba a escribir a otra linea.
|
|
191
203
|
const typed = input.length === 0 ? "" : input.slice(0, cursorIndex);
|
|
192
204
|
const segments = typed.split("\n");
|
|
193
|
-
|
|
205
|
+
// La fila de aire de arriba cuenta: sin sumarla, el cursor se dibuja una fila
|
|
206
|
+
// por encima de donde se escribe.
|
|
207
|
+
let cursorRow = 1 + physicalRows(bar, cols);
|
|
194
208
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
195
209
|
cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
|
|
196
210
|
}
|
package/dist/ui/prompt.js
CHANGED
|
@@ -19,6 +19,7 @@ import { buildPromptFrame } from "./marco.js";
|
|
|
19
19
|
export { buildPromptFrame } from "./marco.js";
|
|
20
20
|
export { SLASH_COMMANDS } from "./comandos.js";
|
|
21
21
|
import { escribirEncimaDelPie, soltarPie } from "./pieFijo.js";
|
|
22
|
+
import { avisarSobreElPromptDeInk, hayPromptDeInk, preguntarConInk } from "./ink/montarPrompt.js";
|
|
22
23
|
import { esGrande, guardarPegado, expandirPegados, marcaQueTerminaEn, limpiarPegados } from "./pegados.js";
|
|
23
24
|
/**
|
|
24
25
|
* Escribir algo mientras el prompt esta dibujado.
|
|
@@ -46,6 +47,12 @@ let escribirEncimaDelPrompt = null;
|
|
|
46
47
|
* pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
|
|
47
48
|
*/
|
|
48
49
|
export function imprimirSobreElPrompt(texto) {
|
|
50
|
+
// Con Ink montado, escribir a stdout por fuera de su arbol le descuadra la
|
|
51
|
+
// cuenta de filas en el siguiente dibujado. Se le entrega como estado.
|
|
52
|
+
if (hayPromptDeInk()) {
|
|
53
|
+
avisarSobreElPromptDeInk(texto);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
49
56
|
if (escribirEncimaDelPrompt) {
|
|
50
57
|
escribirEncimaDelPrompt(texto);
|
|
51
58
|
return;
|
|
@@ -140,7 +147,24 @@ function scanProjectFiles(dir, maxResults = 40) {
|
|
|
140
147
|
walk(dir);
|
|
141
148
|
return results;
|
|
142
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* ¿Se usa el prompt de Ink? Sí, salvo que se pida lo contrario.
|
|
152
|
+
*
|
|
153
|
+
* La salida de emergencia existe porque esto cambia la parte más frágil del
|
|
154
|
+
* programa —el teclado— justo cuando hay clientes pagando. Si Ink se porta mal
|
|
155
|
+
* en una consola concreta, `CHOCOLATITO_PROMPT=clasico` devuelve el de siempre
|
|
156
|
+
* sin reinstalar ni bajar de versión.
|
|
157
|
+
*/
|
|
158
|
+
function usarInk() {
|
|
159
|
+
return process.env.CHOCOLATITO_PROMPT !== "clasico";
|
|
160
|
+
}
|
|
143
161
|
export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.cwd()) {
|
|
162
|
+
if (usarInk())
|
|
163
|
+
return preguntarConInk(promptSymbol, cwd);
|
|
164
|
+
return promptClasico(promptSymbol, cwd);
|
|
165
|
+
}
|
|
166
|
+
/** La implementación de siempre: readline en crudo y las filas contadas a mano. */
|
|
167
|
+
async function promptClasico(promptSymbol = "❯", cwd = process.cwd()) {
|
|
144
168
|
return new Promise((resolve) => {
|
|
145
169
|
// El relevo de la barra.
|
|
146
170
|
//
|
|
@@ -200,9 +224,38 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
200
224
|
// demasiados los sitios que apagan el modo crudo con razon, y basta que uno
|
|
201
225
|
// llegue tarde para que el usuario teclee y su texto salga en la linea de
|
|
202
226
|
// ayuda en vez de en el recuadro.
|
|
227
|
+
/**
|
|
228
|
+
* La primera vuelta REAFIRMA aunque Node crea que todo esta bien.
|
|
229
|
+
*
|
|
230
|
+
* EL AGUJERO QUE TAPA
|
|
231
|
+
*
|
|
232
|
+
* `repararTeclado` solo actua cuando SABE que se perdio el modo crudo: mira
|
|
233
|
+
* `process.stdin.isRaw`. Pero hay una forma de perderlo que Node no ve. Un
|
|
234
|
+
* proceso hijo que hereda la consola -git al refrescar el contexto, un hook,
|
|
235
|
+
* un comando- puede devolverla en cocido al terminar, y `isRaw` se queda
|
|
236
|
+
* diciendo `true`. Node cree que manda y no manda.
|
|
237
|
+
*
|
|
238
|
+
* Si eso pasa DESPUES de abrir el prompt, la reafirmacion de arriba ya paso
|
|
239
|
+
* y el vigilante no lo ve: el usuario teclea, no sale nada, y solo se
|
|
240
|
+
* desatasca al pulsar Enter, que es cuando la consola en cocido suelta la
|
|
241
|
+
* linea. Palabras del usuario, dos veces: "al cambiar de potencia se queda
|
|
242
|
+
* pegado y no deja escribir, tengo que presionar enter".
|
|
243
|
+
*
|
|
244
|
+
* Reafirmar una vez mas, poco despues de abrir, cubre esa ventana. No se
|
|
245
|
+
* hace en cada vuelta a proposito: pasar por false y volver a true ocho
|
|
246
|
+
* veces por segundo es pedirle problemas a la consola por un caso raro.
|
|
247
|
+
*/
|
|
248
|
+
let primeraVigilancia = true;
|
|
203
249
|
const vigilanteTeclado = setInterval(() => {
|
|
204
250
|
if (isFinished)
|
|
205
251
|
return;
|
|
252
|
+
if (primeraVigilancia) {
|
|
253
|
+
primeraVigilancia = false;
|
|
254
|
+
reafirmarModoCrudo(process.stdin);
|
|
255
|
+
if (process.env.CHOCOLATITO_TECLADO === "debug") {
|
|
256
|
+
process.stderr.write(`\n[teclado] reafirmado al abrir (isRaw decia ${process.stdin.isRaw})\n`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
206
259
|
const reparado = repararTeclado(process.stdin);
|
|
207
260
|
if (reparado.length === 0)
|
|
208
261
|
return;
|
package/dist/ui/selector.js
CHANGED
|
@@ -24,6 +24,16 @@ export async function elegirDeLista(titulo, opciones, indiceInicial = 0) {
|
|
|
24
24
|
if (process.stdin.setRawMode)
|
|
25
25
|
process.stdin.setRawMode(true);
|
|
26
26
|
process.stdin.resume();
|
|
27
|
+
// Y referenciado, no solo reanudado. Son cosas distintas: `resume` hace que
|
|
28
|
+
// lleguen los datos, `ref` hace que Node considere que hay trabajo
|
|
29
|
+
// pendiente. Sin lo segundo, este menu se queda esperando una tecla
|
|
30
|
+
// mientras el proceso ve el bucle de eventos vacio y se cierra solo.
|
|
31
|
+
//
|
|
32
|
+
// No es hipotetico: paso justo aqui. Ink desreferencia stdin al desmontar el
|
|
33
|
+
// prompt, y al abrir este menu Chocolatito desaparecia dejando al usuario en
|
|
34
|
+
// la ruta de la consola. Se arreglo tambien alli, pero este menu no tiene
|
|
35
|
+
// por que depender de quien haya corrido antes.
|
|
36
|
+
process.stdin.ref?.();
|
|
27
37
|
/** Filas del ultimo dibujo. Se repinta encima, sin dejar restos. */
|
|
28
38
|
let filas = 0;
|
|
29
39
|
function pintar() {
|
package/extension/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"minimum_chrome_version": "102",
|
|
4
4
|
"name": "Chocolatito Code",
|
|
5
|
-
"version": "1.3
|
|
5
|
+
"version": "1.4.3",
|
|
6
6
|
"description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
|
|
7
7
|
"permissions": [
|
|
8
8
|
"tabs",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chocolatito-code",
|
|
3
|
-
"version": "1.3
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Agente autónomo de programación para la terminal, con control real del ordenador y del navegador. Desarrollado por Chocolatito.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,17 +51,21 @@
|
|
|
51
51
|
"dotenv": "^16.4.7",
|
|
52
52
|
"execa": "^9.5.2",
|
|
53
53
|
"fast-glob": "^3.3.3",
|
|
54
|
+
"ink": "^7.1.1",
|
|
54
55
|
"marked": "^15.0.12",
|
|
55
56
|
"marked-terminal": "^7.3.0",
|
|
56
57
|
"openai": "^4.85.4",
|
|
57
58
|
"ora": "^8.2.0",
|
|
58
59
|
"puppeteer-core": "^25.9.0",
|
|
60
|
+
"react": "^19.2.8",
|
|
59
61
|
"ws": "^8.21.3"
|
|
60
62
|
},
|
|
61
63
|
"devDependencies": {
|
|
62
64
|
"@types/diff": "^7.0.0",
|
|
63
65
|
"@types/node": "^22.13.5",
|
|
66
|
+
"@types/react": "^19.2.18",
|
|
64
67
|
"@types/ws": "^8.18.1",
|
|
68
|
+
"ink-testing-library": "^4.0.0",
|
|
65
69
|
"typescript": "^5.7.3"
|
|
66
70
|
}
|
|
67
71
|
}
|