chocolatito-code 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/agent/huecos.d.ts +58 -0
  2. package/dist/agent/huecos.js +81 -0
  3. package/dist/agent/loop.js +144 -24
  4. package/dist/agent/salidaDelAgente.js +10 -24
  5. package/dist/agent/toolGate.js +6 -1
  6. package/dist/config/engine.d.ts +21 -1
  7. package/dist/config/engine.js +22 -1
  8. package/dist/config/quota.d.ts +11 -0
  9. package/dist/config/quota.js +18 -0
  10. package/dist/index.js +35 -26
  11. package/dist/prompts/systemPrompt.js +186 -150
  12. package/dist/tools/browserExtension.js +118 -3
  13. package/dist/tools/computerUse.d.ts +1 -1
  14. package/dist/tools/computerUse.js +183 -2
  15. package/dist/tools/toolDefsComputer.js +6 -2
  16. package/dist/tools/visionBridge.js +60 -3
  17. package/dist/tools/win/hostScript.js +154 -0
  18. package/dist/ui/banner.d.ts +31 -0
  19. package/dist/ui/banner.js +45 -0
  20. package/dist/ui/comandos.d.ts +33 -0
  21. package/dist/ui/comandos.js +49 -0
  22. package/dist/ui/informeDeUso.d.ts +17 -0
  23. package/dist/ui/informeDeUso.js +81 -0
  24. package/dist/ui/interrupt.d.ts +10 -0
  25. package/dist/ui/interrupt.js +50 -2
  26. package/dist/ui/marco.d.ts +35 -0
  27. package/dist/ui/marco.js +161 -0
  28. package/dist/ui/pieFijo.d.ts +56 -0
  29. package/dist/ui/pieFijo.js +309 -0
  30. package/dist/ui/prompt.d.ts +4 -41
  31. package/dist/ui/prompt.js +24 -157
  32. package/dist/ui/reasoningStream.js +22 -12
  33. package/dist/ui/renderer.d.ts +13 -0
  34. package/dist/ui/renderer.js +41 -12
  35. package/dist/ui/spinner.d.ts +43 -8
  36. package/dist/ui/spinner.js +118 -43
  37. package/extension/background.js +9 -3
  38. package/extension/iconos/128.png +0 -0
  39. package/extension/iconos/16.png +0 -0
  40. package/extension/iconos/32.png +0 -0
  41. package/extension/iconos/48.png +0 -0
  42. package/extension/iconos/ORIGEN.md +48 -0
  43. package/extension/manifest.json +60 -60
  44. package/package.json +67 -67
@@ -0,0 +1,49 @@
1
+ /**
2
+ * LOS COMANDOS DE BARRA, EN UN SOLO SITIO
3
+ *
4
+ * Vivian dentro de prompt.ts, que es quien los autocompleta. Ahora tambien los
5
+ * necesita el prompt de sistema, y prompt.ts arrastra readline, el historial en
6
+ * disco y el escaneo del proyecto: no es algo que deba cargar quien solo quiere
7
+ * la lista.
8
+ *
9
+ * POR QUE LE HACEN FALTA AL MODELO
10
+ *
11
+ * Sesion real. El usuario pregunta como ver su consumo y el agente contesta
12
+ * hablando de "la barra lateral", "la app de escritorio" y "empezar un hilo
13
+ * nuevo". Ninguna de esas cosas existe aqui: esto es una terminal. El modelo
14
+ * sabia que se llamaba Chocolatito Code -se lo dice el prompt- pero no sabia
15
+ * NADA de su propio producto, asi que contestaba con lo que sabe de los chats
16
+ * web. Y la respuesta correcta era una palabra: /cost.
17
+ *
18
+ * Con la lista en un modulo aparte, la que ve el usuario al escribir "/" y la
19
+ * que se le cuenta al modelo son la misma. Duplicarla habria durado hasta el
20
+ * primer comando nuevo.
21
+ *
22
+ * OJO: esta lista SE MODIFICA EN CALIENTE. index.ts anade /actualizar y
23
+ * /licencia al arrancar, segun como este instalado el programa. Por eso el
24
+ * prompt de sistema la lee en cada turno y no una vez al importar.
25
+ */
26
+ export const SLASH_COMMANDS = [
27
+ { name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
28
+ { name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
29
+ { name: "/export", description: "Exportar bitácora de la sesión a archivo Markdown", args: "[nombre.md]" },
30
+ { name: "/plan", description: "Alternar Modo Plan (solo lectura e investigación previa)" },
31
+ { name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" },
32
+ { name: "/skills", description: "Ver skills disponibles y cómo usarlos" },
33
+ { name: "/memory", description: "Consultar la memoria persistente transversal (~/.chocolatito/memory/)" },
34
+ { name: "/effort", description: "Regular potencia del motor (1.0, 1.5, 2.0, 2.5)", args: "[nivel]" },
35
+ { name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
36
+ { name: "/compact", description: "Comprimir contexto y optimizar memoria de la sesión" },
37
+ { name: "/diff", description: "Ver diferencias de código (Git diff) del proyecto" },
38
+ { name: "/commit", description: "Crear un commit en Git con mensaje semántico", args: "<mensaje>" },
39
+ { name: "/cd", description: "Cambiar el directorio de trabajo activo", args: "<ruta>" },
40
+ { name: "/uso", description: "Ver cuánto llevas consumido de tu cuenta" },
41
+ { name: "/clear", description: "Limpiar historial y reiniciar contexto" },
42
+ { name: "/init", description: "Crear archivo de directivas CHOCOLATITO.md" },
43
+ { name: "/help", description: "Mostrar guía completa de comandos y ayuda" },
44
+ { name: "/exit", description: "Cerrar sesión de Chocolatito Code" },
45
+ ];
46
+ /** La lista tal y como se le cuenta al modelo. Una linea por comando. */
47
+ export function listaParaElModelo() {
48
+ return SLASH_COMMANDS.map((c) => `- ${c.name}${c.args ? ` ${c.args}` : ""}: ${c.description}`).join("\n");
49
+ }
@@ -0,0 +1,17 @@
1
+ import { QuotaStatus } from "../config/quota.js";
2
+ export interface DatosDeSesion {
3
+ motor: string;
4
+ tokensEntrada: number;
5
+ tokensSalida: number;
6
+ tokensEnCache: number;
7
+ /** Coste de la sesion, ya formateado. Solo se usa con clave propia. */
8
+ costeFormateado: string;
9
+ }
10
+ /**
11
+ * Arma el informe. Puro y exportado para poder comprobarlo: lo que importa aqui
12
+ * es lo que NO sale, y eso a ojo en un terminal no se revisa.
13
+ *
14
+ * `vaciaEn` es cuando la ventana corta vuelve a cero (quota.ventanaVaciaEn).
15
+ * `null` cuando no hay nada dentro de ella.
16
+ */
17
+ export declare function construirInformeDeUso(estado: QuotaStatus, sesion: DatosDeSesion, vaciaEn: Date | null, ahora?: number, gestionado?: boolean): string;
@@ -0,0 +1,81 @@
1
+ import chalk from "chalk";
2
+ import { faltaPara, isManagedMode } from "../config/quota.js";
3
+ /**
4
+ * EL INFORME DE /cost
5
+ *
6
+ * QUE ENSENABA ANTES
7
+ *
8
+ * • Tokens entrada: 11,553 ($0.00508 USD)
9
+ * • Tokens salida: 332 ($0.00044 USD)
10
+ * • Inversion total: $0.0055 USD
11
+ *
12
+ * Tarifas de API en la cara de alguien que paga una suscripcion plana. Dos
13
+ * problemas, y el segundo es el importante.
14
+ *
15
+ * **No le sirve.** Su factura es fija; que un turno costara medio centimo no le
16
+ * dice nada que pueda usar. Lo que necesita saber es cuanto le queda antes de
17
+ * que el programa le pare, y eso son las ventanas de uso.
18
+ *
19
+ * **Y ensena el margen.** El coste de API por cuenta es informacion del negocio
20
+ * de quien vende esto, no del cliente. Puesto en pantalla, cualquiera saca la
21
+ * cuenta de lo que se gana con el.
22
+ *
23
+ * CON CLAVE PROPIA ES AL REVES
24
+ *
25
+ * Ahi el dinero es del usuario, se lo cobra su proveedor y no hay ventana que
26
+ * ensenar porque no se le limita nada (ver la nota de quota.ts). Ocultarle el
27
+ * gasto seria esconderle SU factura. Asi que ese caso mantiene los dolares.
28
+ */
29
+ const ANCHO_BARRA = 20;
30
+ /** Barra de progreso en texto. Se ve el nivel de un vistazo, sin leer el numero. */
31
+ function barra(porcentaje) {
32
+ const llenas = Math.max(0, Math.min(ANCHO_BARRA, Math.round((porcentaje / 100) * ANCHO_BARRA)));
33
+ // Tres colores y no un degradado: lo unico que hay que distinguir es "vas
34
+ // bien", "ojo" y "estas a punto de quedarte sin".
35
+ const color = porcentaje >= 90 ? "#EF4444" : porcentaje >= 70 ? "#FFA500" : "#22C55E";
36
+ return chalk.hex(color)("█".repeat(llenas)) + chalk.gray("░".repeat(ANCHO_BARRA - llenas));
37
+ }
38
+ function porcentaje(gastado, tope) {
39
+ if (!(tope > 0))
40
+ return 0;
41
+ return Math.min(100, Math.round((gastado / tope) * 100));
42
+ }
43
+ /**
44
+ * Arma el informe. Puro y exportado para poder comprobarlo: lo que importa aqui
45
+ * es lo que NO sale, y eso a ojo en un terminal no se revisa.
46
+ *
47
+ * `vaciaEn` es cuando la ventana corta vuelve a cero (quota.ventanaVaciaEn).
48
+ * `null` cuando no hay nada dentro de ella.
49
+ */
50
+ export function construirInformeDeUso(estado, sesion, vaciaEn, ahora = Date.now(), gestionado = isManagedMode()) {
51
+ const num = (n) => n.toLocaleString("es-ES");
52
+ const lineas = [];
53
+ lineas.push(chalk.bold.hex("#A855F7")("\n📊 CONSUMO DE TU CUENTA"));
54
+ lineas.push("");
55
+ if (gestionado) {
56
+ const pctVentana = porcentaje(estado.spentWindowUsd, estado.config.windowBudgetUsd);
57
+ const pctSemana = porcentaje(estado.spentWeekUsd, estado.config.weeklyBudgetUsd);
58
+ // La de 5 h va primera: es la que corta antes y la que sorprende.
59
+ const cuando = vaciaEn
60
+ ? chalk.gray(` vuelve a cero en ${faltaPara(vaciaEn, ahora)}`)
61
+ : chalk.gray(" sin uso reciente");
62
+ lineas.push(` ${chalk.gray(`Ventana de ${estado.config.windowHours} h`.padEnd(16))}${barra(pctVentana)} ` +
63
+ `${chalk.bold.white(`${pctVentana}%`.padStart(4))}${cuando}`);
64
+ lineas.push(` ${chalk.gray("Últimos 7 días".padEnd(16))}${barra(pctSemana)} ` +
65
+ `${chalk.bold.white(`${pctSemana}%`.padStart(4))}`);
66
+ }
67
+ else {
68
+ // Clave propia: no hay cuenta que gestionar, hay una factura que es suya.
69
+ lineas.push(` ${chalk.gray("Gasto de API".padEnd(16))}${chalk.bold.green(sesion.costeFormateado)}`);
70
+ lineas.push(` ${chalk.gray("".padEnd(16))}${chalk.gray("con tu propia clave: esto te lo cobra tu proveedor")}`);
71
+ }
72
+ lineas.push("");
73
+ lineas.push(` ${chalk.gray("Esta sesión".padEnd(16))}${chalk.white(num(sesion.tokensEntrada))} de entrada · ` +
74
+ `${chalk.white(num(sesion.tokensSalida))} de salida`);
75
+ if (sesion.tokensEnCache > 0) {
76
+ lineas.push(` ${chalk.gray("".padEnd(16))}${chalk.gray(`${num(sesion.tokensEnCache)} reaprovechados de la caché`)}`);
77
+ }
78
+ lineas.push(` ${chalk.gray("Motor".padEnd(16))}${chalk.white(sesion.motor)}`);
79
+ lineas.push("");
80
+ return lineas.join("\n");
81
+ }
@@ -3,6 +3,14 @@ export declare function tomarTecladoParaPrompt(): void;
3
3
  /** Lo llama el prompt al cerrarse, para que el siguiente turno pueda restaurar. */
4
4
  export declare function devolverTecladoDelPrompt(): void;
5
5
  export declare function releaseKeyboard(): void;
6
+ /**
7
+ * Devuelve el teclado a quien lo tenia antes del dialogo.
8
+ *
9
+ * NO se llama a start(): eso crearia un AbortController nuevo, y la peticion en
10
+ * curso se quedaria escuchando al viejo. Esc dejaria de cancelar justo lo que el
11
+ * usuario quiere cancelar.
12
+ */
13
+ export declare function retomarTeclado(): void;
6
14
  export declare class InterruptWatcher {
7
15
  private controller;
8
16
  private wasRaw;
@@ -13,6 +21,8 @@ export declare class InterruptWatcher {
13
21
  get interrupted(): boolean;
14
22
  get signal(): AbortSignal | undefined;
15
23
  start(): AbortSignal | undefined;
24
+ /** Vuelve a escuchar despues de un dialogo, sin tocar la senal de aborto. */
25
+ reanudar(): void;
16
26
  private abort;
17
27
  stop(): void;
18
28
  }
@@ -3,6 +3,7 @@ import process from "node:process";
3
3
  import chalk from "chalk";
4
4
  import { cycleMode, modeAnnouncement } from "./modes.js";
5
5
  import { pushTyped, backspaceTyped, commitTyped } from "./typeAhead.js";
6
+ import { reafirmarModoCrudo } from "./keyboardGuard.js";
6
7
  import { dibujar } from "./salida.js";
7
8
  /**
8
9
  * Interrupcion con Esc mientras el agente trabaja.
@@ -69,11 +70,39 @@ export function tomarTecladoParaPrompt() {
69
70
  export function devolverTecladoDelPrompt() {
70
71
  promptConElTeclado = false;
71
72
  }
73
+ /**
74
+ * Vigilantes que se soltaron para ensenar un dialogo, y que hay que devolver.
75
+ *
76
+ * El dialogo de permisos suelta el teclado antes de dibujarse -si no, el
77
+ * vigilante se come las teclas y la caja no responde-, pero hasta ahora nadie lo
78
+ * devolvia despues. El resto del turno se quedaba SIN vigilante: en modo cocido
79
+ * el terminal hacia el eco por su cuenta, Esc dejaba de cancelar, y lo que el
80
+ * usuario escribiera no se encolaba.
81
+ *
82
+ * Se veia asi, y el usuario dio con la pista sin saberlo: "a veces se queda
83
+ * trabado y no se puede escribir, pero con darle Enter vuelve a la normalidad".
84
+ * Claro: en modo cocido el terminal guarda la linea y Enter la suelta de golpe.
85
+ */
86
+ let soltados = [];
72
87
  export function releaseKeyboard() {
73
- for (const w of Array.from(activos)) {
88
+ soltados = Array.from(activos);
89
+ for (const w of soltados) {
74
90
  w.stop();
75
91
  }
76
92
  }
93
+ /**
94
+ * Devuelve el teclado a quien lo tenia antes del dialogo.
95
+ *
96
+ * NO se llama a start(): eso crearia un AbortController nuevo, y la peticion en
97
+ * curso se quedaria escuchando al viejo. Esc dejaria de cancelar justo lo que el
98
+ * usuario quiere cancelar.
99
+ */
100
+ export function retomarTeclado() {
101
+ const devolver = soltados;
102
+ soltados = [];
103
+ for (const w of devolver)
104
+ w.reanudar();
105
+ }
77
106
  export class InterruptWatcher {
78
107
  controller = null;
79
108
  wasRaw = false;
@@ -164,6 +193,23 @@ export class InterruptWatcher {
164
193
  activos.add(this);
165
194
  return this.controller.signal;
166
195
  }
196
+ /** Vuelve a escuchar despues de un dialogo, sin tocar la senal de aborto. */
197
+ reanudar() {
198
+ if (this.listening || !this.onKey)
199
+ return;
200
+ if (!process.stdin.isTTY || !process.stdin.setRawMode)
201
+ return;
202
+ readline.emitKeypressEvents(process.stdin);
203
+ if (activos.size === 0)
204
+ modoCrudoOriginal = process.stdin.isRaw || false;
205
+ // Reafirmar y no solo poner: el readline del dialogo deja la consola en modo
206
+ // cocido, y un setRawMode(true) a secas puede ser un no-op.
207
+ reafirmarModoCrudo(process.stdin);
208
+ process.stdin.resume();
209
+ process.stdin.on("keypress", this.onKey);
210
+ this.listening = true;
211
+ activos.add(this);
212
+ }
167
213
  abort() {
168
214
  if (this.aborted)
169
215
  return;
@@ -176,7 +222,9 @@ export class InterruptWatcher {
176
222
  stop() {
177
223
  if (this.onKey) {
178
224
  process.stdin.removeListener("keypress", this.onKey);
179
- this.onKey = null;
225
+ // El manejador NO se tira: reanudar() lo vuelve a enganchar tal cual, con
226
+ // su AbortController intacto. Rehacerlo desde start() romperia el Esc de
227
+ // la peticion que ya esta en vuelo.
180
228
  }
181
229
  const escuchaba = this.listening;
182
230
  this.listening = false;
@@ -0,0 +1,35 @@
1
+ export interface DropdownItem {
2
+ type: "command" | "file";
3
+ label: string;
4
+ secondary?: string;
5
+ insertText: string;
6
+ }
7
+ export interface PromptFrameOptions {
8
+ cols: number;
9
+ /** Alto de la ventana; sirve para no dibujar un marco que no quepa. */
10
+ rows?: number;
11
+ promptSymbol?: string;
12
+ input?: string;
13
+ cursorIndex?: number;
14
+ items?: DropdownItem[];
15
+ selectedIndex?: number;
16
+ }
17
+ export interface PromptFrame {
18
+ /** Lineas LOGICAS. Cualquiera puede ocupar mas de una fila fisica. */
19
+ lines: string[];
20
+ /** Filas de pantalla que ocupara el marco entero con este ancho. */
21
+ totalRows: number;
22
+ /** Fila fisica del cursor, con 0 en la primera fila del marco. */
23
+ cursorRow: number;
24
+ cursorCol: number;
25
+ /** Primer elemento del desplegable que se muestra (ventana deslizante). */
26
+ dropdownOffset: number;
27
+ }
28
+ /**
29
+ * Arma el marco del prompt y dice exactamente donde queda el cursor.
30
+ *
31
+ * Va aparte de render() por dos razones: se puede probar sin terminal, y la
32
+ * aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
33
+ * lugar de estimarse por otro lado y descuadrarse.
34
+ */
35
+ export declare function buildPromptFrame(opts: PromptFrameOptions): PromptFrame;
@@ -0,0 +1,161 @@
1
+ import chalk from "chalk";
2
+ import { describeMode, getMode } from "./modes.js";
3
+ /**
4
+ * EL MARCO DE LA BARRA, EN UN SOLO SITIO
5
+ *
6
+ * Vivia dentro de prompt.ts. Ahora lo necesitan dos: el prompt cuando espera una
7
+ * orden, y el spinner mientras el agente trabaja.
8
+ *
9
+ * POR QUE COMPARTIDO Y NO COPIADO
10
+ *
11
+ * Habia DOS barras distintas turnandose. Al esperar salia el marco entero
12
+ * -barra, entrada, barra, linea de ayuda-; al trabajar, una version recortada
13
+ * con otro texto. Se parecian lo justo para que el cambio de una a otra se viera
14
+ * como un salto, y el usuario lo dijo asi: "aun no es fija... debe ser fija y
15
+ * por debajo deben pasar los otros mensajes".
16
+ *
17
+ * Con el constructor compartido no pueden divergir: si alguien toca el marco, lo
18
+ * toca en los dos estados a la vez. Copiarlo habria durado hasta el primer
19
+ * retoque.
20
+ *
21
+ * Va aparte tambien por peso: prompt.ts arrastra readline, el historial en disco
22
+ * y el escaneo del proyecto, y el spinner no tiene por que cargar nada de eso.
23
+ */
24
+ function stripAnsi(str) {
25
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
26
+ }
27
+ const PLACEHOLDER = "Escribe una instrucción, '@' para archivos o '/' para comandos...";
28
+ /**
29
+ * Filas FISICAS que ocupa una linea del marco.
30
+ *
31
+ * Aqui estaba el fallo de verdad: el repintado contaba elementos del array del
32
+ * marco mientras el terminal cuenta filas de pantalla. Cualquier linea que no
33
+ * cupiera en el ancho se partia en dos filas, el cursor subia una fila de menos
34
+ * y el marco entero se iba bajando, dejando una barra huerfana por pulsacion.
35
+ * La entrada ademas puede llevar saltos de linea dentro (continuacion con "\").
36
+ */
37
+ function physicalRows(line, cols) {
38
+ return stripAnsi(line)
39
+ .split("\n")
40
+ .reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
41
+ }
42
+ /** Recorta a `width` columnas visibles. Texto sin pintar: aqui no hay que cerrar colores. */
43
+ function truncatePlain(text, width) {
44
+ if (width <= 0)
45
+ return "";
46
+ if (text.length <= width)
47
+ return text;
48
+ return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
49
+ }
50
+ /**
51
+ * Arma el marco del prompt y dice exactamente donde queda el cursor.
52
+ *
53
+ * Va aparte de render() por dos razones: se puede probar sin terminal, y la
54
+ * aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
55
+ * lugar de estimarse por otro lado y descuadrarse.
56
+ */
57
+ export function buildPromptFrame(opts) {
58
+ const cols = Math.max(20, opts.cols || 80);
59
+ const viewportRows = Math.max(6, opts.rows || 24);
60
+ const promptSymbol = opts.promptSymbol ?? "❯";
61
+ const input = opts.input ?? "";
62
+ const cursorIndex = Math.max(0, Math.min(opts.cursorIndex ?? input.length, input.length));
63
+ const items = opts.items ?? [];
64
+ const selectedIndex = items.length > 0 ? (((opts.selectedIndex ?? 0) % items.length) + items.length) % items.length : 0;
65
+ const isPlan = promptSymbol.includes("plan");
66
+ const symbolStr = isPlan
67
+ ? `${chalk.cyan.bold("plan ❯")} `
68
+ : `${chalk.bold.hex("#D97757")("❯")} `;
69
+ const symbolWidth = isPlan ? 7 : 2;
70
+ const bar = chalk.gray("─".repeat(cols));
71
+ // 1. Linea de entrada. Lo que el usuario escribe NO se recorta nunca: se deja
72
+ // que el terminal la parta y despues se cuentan las filas que ocupa.
73
+ let formattedInput;
74
+ if (input.length === 0) {
75
+ // La pista si se recorta: en una ventana estrecha ocupaba una segunda fila
76
+ // para no decir nada que el usuario no sepa ya.
77
+ formattedInput = chalk.gray(truncatePlain(PLACEHOLDER, cols - symbolWidth));
78
+ }
79
+ else if (input.startsWith("/")) {
80
+ const parts = input.split(" ");
81
+ const cmdPart = chalk.bold.hex("#A855F7")(parts[0]);
82
+ const restPart = parts.slice(1).join(" ");
83
+ formattedInput = restPart ? `${cmdPart} ${chalk.white(restPart)}` : cmdPart;
84
+ }
85
+ else {
86
+ formattedInput = input.replace(/(@[^\s]+)/g, (m) => chalk.bold.hex("#D97757")(m));
87
+ }
88
+ const inputLine = symbolStr + formattedInput;
89
+ const inputRows = physicalRows(inputLine, cols);
90
+ // 2. Desplegable. Un marco mas alto que la ventana obliga al terminal a
91
+ // desplazar el contenido, y a partir de ahi el repintado ya no sabe donde
92
+ // esta su primera fila: se muestra solo una ventana alrededor del elegido.
93
+ const room = Math.max(1, viewportRows - 1 - (1 + inputRows + 1 + 1));
94
+ const shown = Math.min(items.length, room);
95
+ const dropdownOffset = shown > 0 && items.length > shown
96
+ ? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
97
+ : 0;
98
+ const dropdownLines = [];
99
+ for (let i = dropdownOffset; i < dropdownOffset + shown; i++) {
100
+ const item = items[i];
101
+ const isSelected = i === selectedIndex;
102
+ const colorHex = item.type === "file" ? "#D97757" : "#A855F7";
103
+ const pointer = isSelected ? chalk.bold.hex(colorHex)(" ❯ ") : chalk.gray(" ");
104
+ // El puntero se lleva 3 columnas fijas. De lo que queda manda la etiqueta:
105
+ // la descripcion es lo primero que se cae, porque una ruta a medias no se
106
+ // puede ni leer ni elegir.
107
+ const free = cols - 3;
108
+ const label = truncatePlain(item.label, free);
109
+ const roomForDesc = free - label.length - 2;
110
+ const secondary = item.secondary && roomForDesc >= 8 ? truncatePlain(item.secondary, roomForDesc) : "";
111
+ const paintedLabel = isSelected ? chalk.bold.white(label) : chalk.hex(colorHex)(label);
112
+ const paintedDesc = secondary
113
+ ? isSelected
114
+ ? chalk.whiteBright(` ${secondary}`)
115
+ : chalk.gray(` ${secondary}`)
116
+ : "";
117
+ dropdownLines.push(`${pointer}${paintedLabel}${paintedDesc}`);
118
+ }
119
+ // 3. Pie de estado. El relleno se calculaba con Math.max(1, ...): cuando no
120
+ // cabia, en vez de recortarse se quedaba con un espacio y la linea salia MAS
121
+ // ancha que el terminal. En una consola de 80 columnas el pie media 87 y se
122
+ // partia en dos filas desde el primer dibujado.
123
+ const modeLook = describeMode(getMode());
124
+ const hintPlain = "? /help para atajos · shift+tab modo · esc cancelar";
125
+ const modePlain = `⇥ modo ${modeLook.label}`;
126
+ const enginePlain = "· Espectro 1.0 (Ágil)";
127
+ const hint = chalk.gray(hintPlain);
128
+ const modeTag = chalk.hex(modeLook.color)(modePlain);
129
+ const engine = chalk.gray(enginePlain);
130
+ let footerLine;
131
+ if (hintPlain.length + 1 + modePlain.length + 1 + enginePlain.length <= cols) {
132
+ const padLen = cols - hintPlain.length - modePlain.length - 1 - enginePlain.length;
133
+ footerLine = `${hint}${" ".repeat(padLen)}${modeTag} ${engine}`;
134
+ }
135
+ else if (modePlain.length + 1 + enginePlain.length <= cols) {
136
+ // Los atajos son opcionales; el modo activo no, que es lo que decide si te
137
+ // van a preguntar antes de tocar tus archivos.
138
+ footerLine = `${" ".repeat(cols - modePlain.length - 1 - enginePlain.length)}${modeTag} ${engine}`;
139
+ }
140
+ else if (modePlain.length <= cols) {
141
+ footerLine = `${" ".repeat(cols - modePlain.length)}${modeTag}`;
142
+ }
143
+ else {
144
+ footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, cols));
145
+ }
146
+ const lines = [bar, inputLine, ...dropdownLines, bar, footerLine];
147
+ // 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
148
+ // total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
149
+ // cuanto la entrada se partia el acento se iba a escribir a otra linea.
150
+ const typed = input.length === 0 ? "" : input.slice(0, cursorIndex);
151
+ const segments = typed.split("\n");
152
+ let cursorRow = physicalRows(bar, cols);
153
+ for (let i = 0; i < segments.length - 1; i++) {
154
+ cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
155
+ }
156
+ const lastSegWidth = (segments.length === 1 ? symbolWidth : 0) + segments[segments.length - 1].length;
157
+ cursorRow += Math.floor(lastSegWidth / cols);
158
+ const cursorCol = lastSegWidth % cols;
159
+ const totalRows = lines.reduce((n, l) => n + physicalRows(l, cols), 0);
160
+ return { lines, totalRows, cursorRow, cursorCol, dropdownOffset };
161
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Las secciones, de arriba abajo. El orden es fijo a proposito: si cada quien
3
+ * pintara donde le tocara, dos productores independientes -el razonamiento y el
4
+ * spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
5
+ */
6
+ declare const ORDEN: readonly ["razonamiento", "spinner", "entrada"];
7
+ export type SeccionDelPie = (typeof ORDEN)[number];
8
+ /**
9
+ * Enciende o apaga el pie a mano. Con `null` vuelve a mandar el terminal.
10
+ *
11
+ * Las pruebas lo encienden porque en `node --test` no hay TTY y sin esto el pie
12
+ * seria un no-op silencioso: pasarian todas sin comprobar nada.
13
+ */
14
+ export declare function habilitarPie(valor: boolean | null): void;
15
+ /** ¿Hay algo pintado abajo que haya que respetar? */
16
+ export declare function hayPie(): boolean;
17
+ /**
18
+ * Pone (o reemplaza) una seccion del pie y repinta.
19
+ *
20
+ * Con la lista vacia la seccion desaparece, que es lo mismo que quitarla. Si el
21
+ * contenido es identico al que ya habia, no se repinta: el spinner llama ocho
22
+ * veces por segundo y la barra de escribir casi nunca cambia entre dos.
23
+ */
24
+ export declare function fijarSeccion(nombre: SeccionDelPie, lineas: string[]): void;
25
+ /**
26
+ * Varias secciones de una vez, con UN solo repintado.
27
+ *
28
+ * El spinner cambia su linea y la barra de escribir en el mismo tick. Puestas
29
+ * por separado se pintaba el bloque dos veces cada 120 ms, y la segunda pasada
30
+ * borraba y reescribia lo que la primera acababa de dejar: el doble de bytes
31
+ * para el mismo resultado, y un parpadeo que se nota en un terminal lento.
32
+ */
33
+ export declare function fijarSecciones(entradas: Array<[SeccionDelPie, string[]]>): void;
34
+ export declare function quitarSeccion(nombre: SeccionDelPie): void;
35
+ /**
36
+ * Escribe texto permanente sin que el pie se lo lleve por delante.
37
+ *
38
+ * Sin pie es un write normal y no cuesta nada; por eso puede sustituir a
39
+ * `console.log` en el camino del agente sin pensarlo dos veces.
40
+ */
41
+ export declare function escribirEncimaDelPie(texto: string): void;
42
+ /**
43
+ * Quita el pie entero y deja el terminal como si nunca hubiera estado.
44
+ *
45
+ * Lo que quede a medias de linea se conserva en pantalla: es texto del agente,
46
+ * no adorno, y borrarlo seria perder salida.
47
+ */
48
+ export declare function soltarPie(): void;
49
+ /**
50
+ * Olvida el estado sin tocar la pantalla.
51
+ *
52
+ * Para las pruebas y para la salida del proceso: ahi no queda terminal al que
53
+ * repintar, pero el cursor escondido si sobrevive y hay que devolverlo.
54
+ */
55
+ export declare function reiniciarPie(): void;
56
+ export {};