chocolatito-code 1.3.1 → 1.3.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/index.js CHANGED
@@ -13,6 +13,9 @@ import { buildSystemPrompt } from "./prompts/systemPrompt.js";
13
13
  import { AgentLoop } from "./agent/loop.js";
14
14
  import { codigoDeSalida, EXIT_OK, EXIT_FALLO, EXIT_USO, EXIT_LICENCIA } from "./agent/result.js";
15
15
  import { showBanner, animateStartupBanner, showHelp, showEffortMenu } from "./ui/banner.js";
16
+ import { fijarMotor } from "./ui/motorActual.js";
17
+ import { elegirDeLista } from "./ui/selector.js";
18
+ import { reafirmarModoCrudo } from "./ui/keyboardGuard.js";
16
19
  import { askInteractivePrompt, SLASH_COMMANDS } from "./ui/prompt.js";
17
20
  import { changeDir } from "./tools/changeDir.js";
18
21
  import { gitDiff, gitCommit } from "./tools/gitTools.js";
@@ -233,6 +236,9 @@ ${nodeViejo}
233
236
  const sessionManager = new SessionManager();
234
237
  const sessionId = `session_${Date.now()}`;
235
238
  let currentModel = DEFAULT_ESPECTRO;
239
+ // La barra de estado lo lee de aqui. Fijarlo al arrancar y no solo al
240
+ // cambiarlo: si no, hasta el primer /effort la barra no dice nada.
241
+ fijarMotor(currentModel.name, currentModel.effortLevel);
236
242
  let isPlanMode = false;
237
243
  let systemPrompt = buildSystemPrompt(context, currentModel, skillManager, memoryManager, isPlanMode);
238
244
  const agent = new AgentLoop(apiKey, getEngineBaseUrl(), currentModel, systemPrompt);
@@ -429,6 +435,7 @@ ${sessionData}
429
435
  const restauracion = prepararRestauracion(loaded, { cwd: agent.currentCwd, modelId: currentModel.id });
430
436
  agent.currentCwd = restauracion.cwd;
431
437
  currentModel = ESPECTRO_MODELS[restauracion.modelId] || currentModel;
438
+ fijarMotor(currentModel.name, currentModel.effortLevel);
432
439
  const refreshedContext = await getProjectContext(agent.currentCwd);
433
440
  systemPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
434
441
  agent.messages = [{ role: "system", content: systemPrompt }, ...restauracion.conversacion];
@@ -580,20 +587,57 @@ Directivas de ejecución:
580
587
  }
581
588
  if (trimmed.startsWith("/effort") || trimmed.startsWith("/espectro") || trimmed === "/model") {
582
589
  const parts = trimmed.split(/\s+/);
583
- const targetId = parts[1]?.trim();
590
+ let targetId = parts[1]?.trim();
591
+ // Sin argumento se ABRE UN MENU, no se imprime una lista.
592
+ //
593
+ // Antes se volcaban las cuatro opciones con console.log y una frase al
594
+ // final que decia "selecciona con /effort 2.0". En pantalla se veia una
595
+ // lista, la gente pulsaba las flechas -que es lo unico razonable- y no
596
+ // pasaba nada.
584
597
  if (!targetId || !ESPECTRO_MODELS[targetId]) {
585
- showEffortMenu(currentModel.id);
586
- }
587
- else {
588
- currentModel = ESPECTRO_MODELS[targetId];
589
- const refreshedContext = await getProjectContext(agent.currentCwd);
590
- const updatedPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
591
- agent.setEspectroModel(currentModel, updatedPrompt);
592
- console.log(chalk.green(`\n✔ Nivel actualizado a `) +
593
- chalk.bold.hex("#A855F7")(currentModel.name) +
594
- chalk.gray(` (${currentModel.effortLevel})`) +
595
- `\n ${chalk.italic.gray(currentModel.description)}\n`);
598
+ const opciones = Object.values(ESPECTRO_MODELS).map((m) => ({
599
+ id: m.id,
600
+ titulo: `${m.name} ${m.id === currentModel.id ? "· activo" : ""}`.trimEnd(),
601
+ detalle: `Esfuerzo ${m.effortLevel} — ${m.description}`,
602
+ }));
603
+ const indiceActual = Object.values(ESPECTRO_MODELS).findIndex((m) => m.id === currentModel.id);
604
+ const elegido = await elegirDeLista("POTENCIA DEL MOTOR — CHOCOLATITO CODE", opciones, Math.max(0, indiceActual));
605
+ // Sin terminal el menu no puede abrirse: ahi se cae a la lista de
606
+ // siempre, que al menos dice como elegir escribiendolo.
607
+ if (elegido === null) {
608
+ if (!process.stdin.isTTY)
609
+ showEffortMenu(currentModel.id);
610
+ continue;
611
+ }
612
+ targetId = elegido;
596
613
  }
614
+ currentModel = ESPECTRO_MODELS[targetId];
615
+ // Sin esto la barra de abajo sigue anunciando el motor anterior.
616
+ fijarMotor(currentModel.name, currentModel.effortLevel);
617
+ const refreshedContext = await getProjectContext(agent.currentCwd);
618
+ const updatedPrompt = buildSystemPrompt(refreshedContext, currentModel, skillManager, memoryManager, isPlanMode);
619
+ agent.setEspectroModel(currentModel, updatedPrompt);
620
+ console.log(chalk.green(`
621
+ ✔ Nivel actualizado a `) +
622
+ chalk.bold.hex("#A855F7")(currentModel.name) +
623
+ chalk.gray(` (${currentModel.effortLevel})`) +
624
+ `
625
+ ${chalk.italic.gray(currentModel.description)}
626
+ `);
627
+ // EL TECLADO SE QUEDABA TRABADO AL CAMBIAR DE MOTOR
628
+ //
629
+ // "una vez cambiado de effort se queda trabado y con un enter se va la
630
+ // traba". Ese "con un enter se arregla" es la firma del modo COCIDO: la
631
+ // consola retiene lo tecleado hasta el salto de linea, asi que el prompt
632
+ // no recibe nada hasta que pulsas Enter.
633
+ //
634
+ // Aqui en medio se refresca el contexto del proyecto, que lanza git. Un
635
+ // hijo que hereda la consola puede devolverla en cocido al terminar, y
636
+ // Node no se entera: `process.stdin.isRaw` sigue diciendo true. Por eso el
637
+ // vigilante del prompt no lo arregla —solo actua cuando SABE que se
638
+ // perdio— y hace falta reafirmar, que pasa por false antes de poner true
639
+ // y por tanto no se lo come la cache de libuv.
640
+ reafirmarModoCrudo(process.stdin);
597
641
  continue;
598
642
  }
599
643
  if (trimmed === "/clear") {
package/dist/ui/marco.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
- import { describeMode, getMode } from "./modes.js";
2
+ import { describeMode, getMode, TRIANGULOS } from "./modes.js";
3
+ import { ESFUERZO_MAXIMO, esfuerzoActual, marcaDeMotor, motorActual } from "./motorActual.js";
3
4
  /**
4
5
  * EL MARCO DE LA BARRA, EN UN SOLO SITIO
5
6
  *
@@ -121,27 +122,67 @@ export function buildPromptFrame(opts) {
121
122
  // ancha que el terminal. En una consola de 80 columnas el pie media 87 y se
122
123
  // partia en dos filas desde el primer dibujado.
123
124
  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);
125
+ // 3. La linea de estado, con POSICIONES FIJAS.
126
+ //
127
+ // Antes iban los atajos a la izquierda y el modo y el motor apelotonados a la
128
+ // derecha, y encima la linea se reordenaba sola en cuatro variantes segun el
129
+ // ancho: al estrecharse la ventana, el modo saltaba de sitio. El usuario lo
130
+ // pidio asi: "el boton del modo debe ir a la izquierda... y el modelo a la
131
+ // derecha, pero deben ser fijos siempre y nunca cambiar".
132
+ //
133
+ // Ahora el modo esta SIEMPRE pegado a la izquierda y el motor SIEMPRE pegado a
134
+ // la derecha. Lo unico que aparece y desaparece es la pista del medio, que es
135
+ // lo prescindible: quien ya se sabe los atajos no la necesita, y quien no, la
136
+ // tiene en /help.
137
+ const modePlain = `${TRIANGULOS} modo ${modeLook.label}`;
138
+ // Estaba escrito a mano y por eso no cambiaba nunca. Vacio si nadie lo ha
139
+ // fijado: mejor un hueco que un nombre que no es el que se esta usando.
140
+ //
141
+ // El nivel de esfuerzo va PEGADO al motor y no en una linea aparte: es un dato
142
+ // del mismo hecho -con que potencia trabajas-, y una fila mas en el pie es una
143
+ // fila menos de conversacion en pantalla.
144
+ const nivel = esfuerzoActual();
145
+ const marca = marcaDeMotor(nivel);
146
+ const motor = motorActual();
147
+ const enginePlain = motor ? `${marca ? `${marca} ` : ""}${motor}${nivel ? ` · ${nivel}` : ""}` : "";
148
+ const hintPlain = "shift+tab cambia · esc cancela · ? /help";
128
149
  const modeTag = chalk.hex(modeLook.color)(modePlain);
129
- const engine = chalk.gray(enginePlain);
150
+ // En el maximo se pinta encendido, no en gris: es el unico nivel que conviene
151
+ // que se note, porque es el que se come la cuota.
152
+ const engine = nivel === ESFUERZO_MAXIMO ? chalk.hex("#FFA500")(enginePlain) : chalk.gray(enginePlain);
153
+ // Dos columnas de respiro. Los triangulos son un glifo de anchura ambigua y
154
+ // hay terminales que lo pintan doble: si la linea llegara justa al ancho, el
155
+ // terminal la partiria en dos filas y el repintado del marco se descuadraria
156
+ // entero. Ese fallo ya costo una barra huerfana por pulsacion.
157
+ const util = Math.max(0, cols - 2);
130
158
  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}`;
159
+ const huecoConPista = util - modePlain.length - 1 - hintPlain.length - enginePlain.length;
160
+ const hueco = util - modePlain.length - enginePlain.length;
161
+ if (!enginePlain) {
162
+ // Todavia nadie ha dicho que motor hay. Se enseña el modo y ya, pero SIN
163
+ // saltarse el recorte: "⏵⏵ modo acepta ediciones" son 24 columnas y en una
164
+ // consola de 20 se partiria en dos filas, que es lo que descuadra el marco.
165
+ const conPista = util - modePlain.length - 1 - hintPlain.length;
166
+ if (modePlain.length > util) {
167
+ footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, util));
168
+ }
169
+ else {
170
+ footerLine = conPista >= 0 ? `${modeTag} ${chalk.gray(hintPlain)}` : modeTag;
171
+ }
134
172
  }
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}`;
173
+ else if (huecoConPista >= 1) {
174
+ footerLine = `${modeTag} ${chalk.gray(hintPlain)}${" ".repeat(huecoConPista)}${engine}`;
175
+ }
176
+ else if (hueco >= 1) {
177
+ footerLine = `${modeTag}${" ".repeat(hueco)}${engine}`;
139
178
  }
140
- else if (modePlain.length <= cols) {
141
- footerLine = `${" ".repeat(cols - modePlain.length)}${modeTag}`;
179
+ else if (modePlain.length <= util) {
180
+ // Si hay que sacrificar uno, se va el motor: el modo es lo que decide si te
181
+ // van a preguntar antes de tocar tus archivos.
182
+ footerLine = modeTag;
142
183
  }
143
184
  else {
144
- footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, cols));
185
+ footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, util));
145
186
  }
146
187
  const lines = [bar, inputLine, ...dropdownLines, bar, footerLine];
147
188
  // 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
@@ -22,7 +22,15 @@ export interface ModeLook {
22
22
  color: string;
23
23
  }
24
24
  export declare function describeMode(mode?: InteractiveMode): ModeLook;
25
- /** Etiqueta de una linea para el pie del prompt y para el spinner. */
25
+ /**
26
+ * El simbolo del modo.
27
+ *
28
+ * Dos triangulos, no la flecha de tabulacion. El "⇥" de antes es el simbolo del
29
+ * tabulador: dice COMO se cambia de modo, no QUE modo hay puesto. Y como ademas
30
+ * el atajo es shift+tab, se leia como si el modo fuera un atajo mas.
31
+ */
32
+ export declare const TRIANGULOS = "\u23F5\u23F5";
33
+ /** Etiqueta de una linea del modo activo. */
26
34
  export declare function modeBadge(mode?: InteractiveMode): string;
27
35
  /** Aviso completo, para cuando el usuario acaba de cambiar de modo. */
28
36
  export declare function modeAnnouncement(mode?: InteractiveMode): string;
package/dist/ui/modes.js CHANGED
@@ -43,15 +43,23 @@ const LOOKS = {
43
43
  export function describeMode(mode = current) {
44
44
  return LOOKS[mode];
45
45
  }
46
- /** Etiqueta de una linea para el pie del prompt y para el spinner. */
46
+ /**
47
+ * El simbolo del modo.
48
+ *
49
+ * Dos triangulos, no la flecha de tabulacion. El "⇥" de antes es el simbolo del
50
+ * tabulador: dice COMO se cambia de modo, no QUE modo hay puesto. Y como ademas
51
+ * el atajo es shift+tab, se leia como si el modo fuera un atajo mas.
52
+ */
53
+ export const TRIANGULOS = "⏵⏵";
54
+ /** Etiqueta de una linea del modo activo. */
47
55
  export function modeBadge(mode = current) {
48
56
  if (mode === "default")
49
57
  return "";
50
58
  const look = LOOKS[mode];
51
- return chalk.hex(look.color)(`⇥ modo ${look.label}`);
59
+ return chalk.hex(look.color)(`${TRIANGULOS} modo ${look.label}`);
52
60
  }
53
61
  /** Aviso completo, para cuando el usuario acaba de cambiar de modo. */
54
62
  export function modeAnnouncement(mode = current) {
55
63
  const look = LOOKS[mode];
56
- return `${chalk.bold.hex(look.color)(`⇥ modo ${look.label}`)} ${chalk.gray(`· ${look.hint}`)} ${chalk.gray("· shift+tab para cambiar")}`;
64
+ return `${chalk.bold.hex(look.color)(`${TRIANGULOS} modo ${look.label}`)} ${chalk.gray(`· ${look.hint}`)} ${chalk.gray("· shift+tab para cambiar")}`;
57
65
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * QUE MOTOR ESTA PUESTO
3
+ *
4
+ * La linea de estado del marco lo llevaba escrito a mano:
5
+ *
6
+ * const enginePlain = "Espectro 1.0 (Ágil)";
7
+ *
8
+ * Asi que decia siempre lo mismo pasara lo que pasara. Cambiabas de motor con
9
+ * /effort, el agente empezaba a usar otro de verdad, y la barra seguia
10
+ * anunciando el primero. Lo vio el usuario: "al poner el modelo, la parte de
11
+ * abajo que es fija no se actualiza con el modelo que se pone".
12
+ *
13
+ * Un indicador que miente es peor que no tener indicador: te hace tomar
14
+ * decisiones -esperar mas, gastar mas- creyendo que estas en otro sitio.
15
+ *
16
+ * Vive aparte y no en el marco por lo mismo que el modo (ver modes.ts): lo
17
+ * escribe quien cambia de motor y lo lee quien pinta, y ninguno de los dos tiene
18
+ * por que conocer al otro.
19
+ */
20
+ /** Lo pone index.ts al arrancar y cada vez que se cambia de motor. */
21
+ export declare function fijarMotor(nuevo: string, nivel?: string): void;
22
+ /** El nivel de esfuerzo activo ("Normal", "Alto", "Extendido", "Máximo"). */
23
+ export declare function esfuerzoActual(): string;
24
+ /** El nivel mas alto. Es el unico que se anuncia distinto. */
25
+ export declare const ESFUERZO_MAXIMO = "M\u00E1ximo";
26
+ /**
27
+ * La marca que precede al nombre del motor.
28
+ *
29
+ * Recibe el reloj en vez de leerlo: asi se puede comprobar fotograma a
30
+ * fotograma, que dentro de un terminal no se revisa a ojo.
31
+ *
32
+ * Cambia cada 180 ms, no cada 120 como el spinner. Dos cosas latiendo al mismo
33
+ * ritmo se leen como una sola parpadeando; a ritmos distintos se distinguen.
34
+ */
35
+ export declare function marcaDeMotor(nivel?: string, ahora?: number): string;
36
+ /**
37
+ * El nombre del motor activo.
38
+ *
39
+ * Vacio hasta que alguien lo fije. Quien pinta decide que hacer con eso: mejor
40
+ * no enseñar nada que enseñar un nombre inventado.
41
+ */
42
+ export declare function motorActual(): string;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * QUE MOTOR ESTA PUESTO
3
+ *
4
+ * La linea de estado del marco lo llevaba escrito a mano:
5
+ *
6
+ * const enginePlain = "Espectro 1.0 (Ágil)";
7
+ *
8
+ * Asi que decia siempre lo mismo pasara lo que pasara. Cambiabas de motor con
9
+ * /effort, el agente empezaba a usar otro de verdad, y la barra seguia
10
+ * anunciando el primero. Lo vio el usuario: "al poner el modelo, la parte de
11
+ * abajo que es fija no se actualiza con el modelo que se pone".
12
+ *
13
+ * Un indicador que miente es peor que no tener indicador: te hace tomar
14
+ * decisiones -esperar mas, gastar mas- creyendo que estas en otro sitio.
15
+ *
16
+ * Vive aparte y no en el marco por lo mismo que el modo (ver modes.ts): lo
17
+ * escribe quien cambia de motor y lo lee quien pinta, y ninguno de los dos tiene
18
+ * por que conocer al otro.
19
+ */
20
+ let nombre = "";
21
+ let esfuerzo = "";
22
+ /** Lo pone index.ts al arrancar y cada vez que se cambia de motor. */
23
+ export function fijarMotor(nuevo, nivel = "") {
24
+ if (nuevo && nuevo.trim())
25
+ nombre = nuevo.trim();
26
+ if (nivel && nivel.trim())
27
+ esfuerzo = nivel.trim();
28
+ }
29
+ /** El nivel de esfuerzo activo ("Normal", "Alto", "Extendido", "Máximo"). */
30
+ export function esfuerzoActual() {
31
+ return esfuerzo;
32
+ }
33
+ /** El nivel mas alto. Es el unico que se anuncia distinto. */
34
+ export const ESFUERZO_MAXIMO = "Máximo";
35
+ /**
36
+ * Los cuatro fotogramas del latido del maximo.
37
+ *
38
+ * Solo se anima el nivel mas alto, y a proposito: si parpadeara siempre seria
39
+ * ruido de fondo que se deja de ver a los dos minutos. Animando solo el maximo,
40
+ * el movimiento SIGNIFICA algo -"estas gastando el motor caro"- en vez de ser
41
+ * decoracion.
42
+ */
43
+ const LATIDO = ["◇", "◈", "◆", "◈"];
44
+ /**
45
+ * La marca que precede al nombre del motor.
46
+ *
47
+ * Recibe el reloj en vez de leerlo: asi se puede comprobar fotograma a
48
+ * fotograma, que dentro de un terminal no se revisa a ojo.
49
+ *
50
+ * Cambia cada 180 ms, no cada 120 como el spinner. Dos cosas latiendo al mismo
51
+ * ritmo se leen como una sola parpadeando; a ritmos distintos se distinguen.
52
+ */
53
+ export function marcaDeMotor(nivel = esfuerzo, ahora = Date.now()) {
54
+ if (nivel !== ESFUERZO_MAXIMO)
55
+ return "";
56
+ return LATIDO[Math.floor(ahora / 180) % LATIDO.length];
57
+ }
58
+ /**
59
+ * El nombre del motor activo.
60
+ *
61
+ * Vacio hasta que alguien lo fije. Quien pinta decide que hacer con eso: mejor
62
+ * no enseñar nada que enseñar un nombre inventado.
63
+ */
64
+ export function motorActual() {
65
+ return nombre;
66
+ }
package/dist/ui/prompt.js CHANGED
@@ -462,10 +462,28 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
462
462
  render();
463
463
  return;
464
464
  }
465
- if (items.length > 0 && (inputBuffer === "/" || getAtMention().active)) {
466
- if (applySelectedDropdownItem()) {
465
+ // EL DESPLEGABLE MANDA SOBRE LO TECLEADO
466
+ //
467
+ // Antes esto solo entraba con el buffer EXACTAMENTE en "/". Si habias
468
+ // escrito "/e" y el desplegable te estaba señalando "/effort", el Enter
469
+ // enviaba "/e" tal cual: un comando que no existe, mientras en pantalla
470
+ // ponia otra cosa. Lo conto el usuario asi: "me recomienda effort pero
471
+ // al dar enter se manda como /e".
472
+ const mencionActiva = getAtMention().active;
473
+ const escribiendoComando = inputBuffer.startsWith("/") && !inputBuffer.includes(" ");
474
+ const soloLaBarra = inputBuffer === "/";
475
+ if (items.length > 0 && (escribiendoComando || mencionActiva)) {
476
+ const elegido = items[selectedIndex];
477
+ const yaEstaEntero = Boolean(elegido) && inputBuffer === elegido.insertText;
478
+ if (!yaEstaEntero && applySelectedDropdownItem()) {
467
479
  render();
468
- return;
480
+ // Se completa Y SE ENVIA, que es lo que uno espera al ver el comando
481
+ // señalado y pulsar Enter. Dos excepciones que solo completan:
482
+ // - una mencion de archivo casi siempre lleva mas texto detras;
483
+ // - con solo "/" escrito no has elegido nada todavia, y enviar ahi
484
+ // ejecutaria el primero de la lista sin querer.
485
+ if (mencionActiva || soloLaBarra)
486
+ return;
469
487
  }
470
488
  }
471
489
  scheduleSubmit();
@@ -0,0 +1,37 @@
1
+ /**
2
+ * UNA LISTA POR LA QUE SE PUEDE SUBIR Y BAJAR
3
+ *
4
+ * `/effort` no era un menu: era un `console.log` con las cuatro opciones y una
5
+ * frase al final que decia "selecciona con /effort 2.0". El usuario lo vio en
6
+ * pantalla como una lista, hizo lo unico razonable -pulsar las flechas- y no
7
+ * paso nada. Sus palabras: "me salen en pantalla los modelos pero no puedo
8
+ * subir y bajar entre ellos con las flechitas".
9
+ *
10
+ * Una lista que parece elegible y no lo es engaña dos veces: primero no
11
+ * funciona, y despues hay que leerse la letra pequeña para descubrir que habia
12
+ * que escribir el comando otra vez con un argumento.
13
+ *
14
+ * POR QUE NO SE REUTILIZO EL DIALOGO DE PERMISOS
15
+ *
16
+ * Hace lo mismo -flechas y Enter- pero tiene dentro toda la logica de aprobar,
17
+ * rechazar, recordar patrones y pedir explicacion por escrito. Sacar de ahi la
18
+ * parte de elegir habria sido tocar el camino por el que pasa CADA accion con
19
+ * consecuencias del agente, y por un menu que se abre de higos a brevas. Esto
20
+ * son sesenta lineas que no tocan nada de eso.
21
+ */
22
+ export interface OpcionDeLista {
23
+ /** Lo que se devuelve al elegirla. */
24
+ id: string;
25
+ /** Primera linea, la que se lee. */
26
+ titulo: string;
27
+ /** Segunda linea en gris. Opcional. */
28
+ detalle?: string;
29
+ }
30
+ /**
31
+ * Enseña la lista y devuelve el id elegido. `null` si se sale con Esc.
32
+ *
33
+ * Sin terminal no hay nada que elegir: devuelve null en el acto en vez de
34
+ * quedarse esperando una tecla que no va a llegar nunca. Es lo que pasaria al
35
+ * correrlo desde un script o dentro de las pruebas.
36
+ */
37
+ export declare function elegirDeLista(titulo: string, opciones: OpcionDeLista[], indiceInicial?: number): Promise<string | null>;
@@ -0,0 +1,81 @@
1
+ import chalk from "chalk";
2
+ import process from "node:process";
3
+ import readline from "node:readline";
4
+ import { dibujar } from "./salida.js";
5
+ /**
6
+ * Enseña la lista y devuelve el id elegido. `null` si se sale con Esc.
7
+ *
8
+ * Sin terminal no hay nada que elegir: devuelve null en el acto en vez de
9
+ * quedarse esperando una tecla que no va a llegar nunca. Es lo que pasaria al
10
+ * correrlo desde un script o dentro de las pruebas.
11
+ */
12
+ export async function elegirDeLista(titulo, opciones, indiceInicial = 0) {
13
+ if (opciones.length === 0)
14
+ return null;
15
+ if (!process.stdin.isTTY)
16
+ return null;
17
+ return new Promise((resolve) => {
18
+ let elegido = Math.max(0, Math.min(indiceInicial, opciones.length - 1));
19
+ if (!readline.__choco_keypress) {
20
+ readline.emitKeypressEvents(process.stdin);
21
+ readline.__choco_keypress = true;
22
+ }
23
+ const eraCrudo = process.stdin.isRaw;
24
+ if (process.stdin.setRawMode)
25
+ process.stdin.setRawMode(true);
26
+ process.stdin.resume();
27
+ /** Filas del ultimo dibujo. Se repinta encima, sin dejar restos. */
28
+ let filas = 0;
29
+ function pintar() {
30
+ const lineas = ["", chalk.bold.hex("#E4CDEC")(titulo), ""];
31
+ opciones.forEach((o, i) => {
32
+ const puesto = i === elegido;
33
+ const punta = puesto ? chalk.bold.hex("#D97757")(" ❯ ") : " ";
34
+ const nombre = puesto ? chalk.bold.white(o.titulo) : chalk.hex("#A855F7")(o.titulo);
35
+ lineas.push(`${punta}${nombre}`);
36
+ if (o.detalle)
37
+ lineas.push(` ${chalk.gray(o.detalle)}`);
38
+ });
39
+ lineas.push("", chalk.gray(" ↑↓ para moverte · enter para elegir · esc para salir"), "");
40
+ // Se sube justo lo que se escribio la vez anterior: ni una fila mas, ni
41
+ // una menos. Contar de mas se come la linea de encima.
42
+ const salida = (filas > 0 ? `\x1b[${filas}A` : "") + lineas.map((l) => `\r\x1b[2K${l}`).join("\n") + "\n";
43
+ dibujar(salida);
44
+ filas = lineas.length;
45
+ }
46
+ function terminar(valor) {
47
+ process.stdin.removeListener("keypress", alPulsar);
48
+ // Se devuelve el modo que habia, no se fuerza uno: si el prompt tenia el
49
+ // teclado en crudo y aqui se dejara en cocido, la sesion se queda sin
50
+ // poder escribir y no se sabe por que.
51
+ if (process.stdin.setRawMode)
52
+ process.stdin.setRawMode(eraCrudo || false);
53
+ resolve(valor);
54
+ }
55
+ function alPulsar(_str, key) {
56
+ if (!key)
57
+ return;
58
+ if (key.name === "up" || (key.name === "k" && !key.ctrl)) {
59
+ elegido = (elegido - 1 + opciones.length) % opciones.length;
60
+ pintar();
61
+ }
62
+ else if (key.name === "down" || (key.name === "j" && !key.ctrl)) {
63
+ elegido = (elegido + 1) % opciones.length;
64
+ pintar();
65
+ }
66
+ else if (key.name === "return" || key.name === "enter") {
67
+ terminar(opciones[elegido].id);
68
+ }
69
+ else if (key.name === "escape") {
70
+ terminar(null);
71
+ }
72
+ else if (key.ctrl && key.name === "c") {
73
+ // Ctrl+C aqui es "sal del menu", no "cierra el programa": estas
74
+ // eligiendo, no ejecutando nada.
75
+ terminar(null);
76
+ }
77
+ }
78
+ pintar();
79
+ process.stdin.on("keypress", alPulsar);
80
+ });
81
+ }
@@ -1,7 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import process from "node:process";
3
3
  import { typedText, queuedCount } from "./typeAhead.js";
4
- import { modeBadge } from "./modes.js";
5
4
  import { fijarSecciones, quitarSeccion } from "./pieFijo.js";
6
5
  import { buildPromptFrame } from "./marco.js";
7
6
  /**
@@ -32,9 +31,6 @@ const VERBS = [
32
31
  "Tejiendo",
33
32
  "Cocinando",
34
33
  ];
35
- function sinColores(texto) {
36
- return texto.replace(/\x1b\[[0-9;]*m/g, "");
37
- }
38
34
  export class DynamicSpinner {
39
35
  timer = null;
40
36
  glyphIdx = 0;
@@ -81,24 +77,17 @@ export class DynamicSpinner {
81
77
  * salian todas como "Rumiando…", indistinguibles de una peticion normal.
82
78
  */
83
79
  lineaDelSpinner(elapsedSec) {
84
- const cols = Math.max(20, process.stdout.columns || 80);
85
80
  const glyph = chalk.hex("#D97757")(GLYPHS[this.glyphIdx]);
86
81
  const verbo = this.prefix || VERBS[this.verbIdx];
87
82
  const verb = chalk.italic.hex("#D97757")(`${verbo}…`);
88
83
  // El contador va hacia abajo: son los tokens que ESTAN LLEGANDO del modelo.
89
84
  const tokenStr = this.tokenCount > 0 ? ` · ↓ ${(this.tokenCount / 1000).toFixed(1)}k tokens` : "";
90
85
  const cola = chalk.gray(`(${elapsedSec}s${tokenStr} · esc para pausar)`);
91
- const izquierda = `${glyph} ${verb} ${cola}`;
92
- const anchoIzq = 1 + 1 + verbo.length + 1 + 1 + `(${elapsedSec}s${tokenStr} · esc para pausar)`.length;
93
- // La chapa del modo se pega a la derecha, y solo si sobra sitio: antes se
94
- // concatenaba siempre y en una consola estrecha empujaba la linea a una
95
- // segunda fila, que es justo lo que descuadra el repintado.
96
- const badge = modeBadge();
97
- if (!badge)
98
- return izquierda;
99
- const anchoBadge = sinColores(badge).length;
100
- const hueco = cols - anchoIzq - anchoBadge;
101
- return hueco >= 2 ? `${izquierda}${" ".repeat(hueco)}${badge}` : izquierda;
86
+ // El modo YA NO va aqui. Vive en la linea de estado de la barra, que ahora
87
+ // es la misma en los dos estados: repetirlo era decir dos veces lo mismo en
88
+ // dos sitios que no siempre coincidian, y ademas obligaba a cuadrar anchos a
89
+ // mano justo en la linea mas propensa a partirse en dos filas.
90
+ return `${glyph} ${verb} ${cola}`;
102
91
  }
103
92
  /**
104
93
  * LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "minimum_chrome_version": "102",
4
4
  "name": "Chocolatito Code",
5
- "version": "1.3.1",
5
+ "version": "1.3.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.1",
3
+ "version": "1.3.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": {