chocolatito-code 1.3.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.
package/dist/ui/prompt.js CHANGED
@@ -4,12 +4,21 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import os from "node:os";
6
6
  import chalk from "chalk";
7
- import { cycleMode, describeMode, getMode } from "./modes.js";
7
+ import { cycleMode } from "./modes.js";
8
8
  import { releaseKeyboard, tomarTecladoParaPrompt, devolverTecladoDelPrompt } from "./interrupt.js";
9
9
  import { repararTeclado, reafirmarModoCrudo, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
10
10
  import { clearTyped } from "./typeAhead.js";
11
11
  import { dibujar } from "./salida.js";
12
- import { escribirEncimaDelPie, reiniciarPie } from "./pieFijo.js";
12
+ // La lista vive en su propio modulo porque tambien la necesita el prompt de
13
+ // sistema, y este archivo arrastra readline, el historial y el escaneo del
14
+ // proyecto. Se re-exporta para no romper a quien ya la importaba de aqui.
15
+ import { SLASH_COMMANDS } from "./comandos.js";
16
+ // El marco lo comparten el prompt y el spinner: son la MISMA barra, no dos
17
+ // que se parecen. Ver la nota de marco.ts.
18
+ import { buildPromptFrame } from "./marco.js";
19
+ export { buildPromptFrame } from "./marco.js";
20
+ export { SLASH_COMMANDS } from "./comandos.js";
21
+ import { escribirEncimaDelPie, soltarPie } from "./pieFijo.js";
13
22
  import { esGrande, guardarPegado, expandirPegados, marcaQueTerminaEn, limpiarPegados } from "./pegados.js";
14
23
  /**
15
24
  * Escribir algo mientras el prompt esta dibujado.
@@ -45,26 +54,6 @@ export function imprimirSobreElPrompt(texto) {
45
54
  // viven ahi. Un aviso que llegue tarde tiene que pasar por encima igual.
46
55
  escribirEncimaDelPie(texto.endsWith("\n") ? texto : `${texto}\n`);
47
56
  }
48
- export const SLASH_COMMANDS = [
49
- { name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
50
- { name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
51
- { name: "/export", description: "Exportar bitácora de la sesión a archivo Markdown", args: "[nombre.md]" },
52
- { name: "/plan", description: "Alternar Modo Plan (solo lectura e investigación previa)" },
53
- { name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" },
54
- { name: "/skills", description: "Ver skills disponibles y cómo usarlos" },
55
- { name: "/memory", description: "Consultar la memoria persistente transversal (~/.chocolatito/memory/)" },
56
- { name: "/effort", description: "Regular potencia del motor (1.0, 1.5, 2.0, 2.5)", args: "[nivel]" },
57
- { name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
58
- { name: "/compact", description: "Comprimir contexto y optimizar memoria de la sesión" },
59
- { name: "/diff", description: "Ver diferencias de código (Git diff) del proyecto" },
60
- { name: "/commit", description: "Crear un commit en Git con mensaje semántico", args: "<mensaje>" },
61
- { name: "/cd", description: "Cambiar el directorio de trabajo activo", args: "<ruta>" },
62
- { name: "/cost", description: "Ver consumo detallado de tokens e inversión en USD" },
63
- { name: "/clear", description: "Limpiar historial y reiniciar contexto" },
64
- { name: "/init", description: "Crear archivo de directivas CHOCOLATITO.md" },
65
- { name: "/help", description: "Mostrar guía completa de comandos y ayuda" },
66
- { name: "/exit", description: "Cerrar sesión de Chocolatito Code" },
67
- ];
68
57
  let isKeypressInitialized = false;
69
58
  /**
70
59
  * PEGADO DE VARIAS LINEAS
@@ -151,148 +140,18 @@ function scanProjectFiles(dir, maxResults = 40) {
151
140
  walk(dir);
152
141
  return results;
153
142
  }
154
- const PLACEHOLDER = "Escribe una instrucción, '@' para archivos o '/' para comandos...";
155
- /**
156
- * Filas FISICAS que ocupa una linea del marco.
157
- *
158
- * Aqui estaba el fallo de verdad: el repintado contaba elementos del array del
159
- * marco mientras el terminal cuenta filas de pantalla. Cualquier linea que no
160
- * cupiera en el ancho se partia en dos filas, el cursor subia una fila de menos
161
- * y el marco entero se iba bajando, dejando una barra huerfana por pulsacion.
162
- * La entrada ademas puede llevar saltos de linea dentro (continuacion con "\").
163
- */
164
- function physicalRows(line, cols) {
165
- return stripAnsi(line)
166
- .split("\n")
167
- .reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
168
- }
169
- /** Recorta a `width` columnas visibles. Texto sin pintar: aqui no hay que cerrar colores. */
170
- function truncatePlain(text, width) {
171
- if (width <= 0)
172
- return "";
173
- if (text.length <= width)
174
- return text;
175
- return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
176
- }
177
- /**
178
- * Arma el marco del prompt y dice exactamente donde queda el cursor.
179
- *
180
- * Va aparte de render() por dos razones: se puede probar sin terminal, y la
181
- * aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
182
- * lugar de estimarse por otro lado y descuadrarse.
183
- */
184
- export function buildPromptFrame(opts) {
185
- const cols = Math.max(20, opts.cols || 80);
186
- const viewportRows = Math.max(6, opts.rows || 24);
187
- const promptSymbol = opts.promptSymbol ?? "❯";
188
- const input = opts.input ?? "";
189
- const cursorIndex = Math.max(0, Math.min(opts.cursorIndex ?? input.length, input.length));
190
- const items = opts.items ?? [];
191
- const selectedIndex = items.length > 0 ? (((opts.selectedIndex ?? 0) % items.length) + items.length) % items.length : 0;
192
- const isPlan = promptSymbol.includes("plan");
193
- const symbolStr = isPlan
194
- ? `${chalk.cyan.bold("plan ❯")} `
195
- : `${chalk.bold.hex("#D97757")("❯")} `;
196
- const symbolWidth = isPlan ? 7 : 2;
197
- const bar = chalk.gray("─".repeat(cols));
198
- // 1. Linea de entrada. Lo que el usuario escribe NO se recorta nunca: se deja
199
- // que el terminal la parta y despues se cuentan las filas que ocupa.
200
- let formattedInput;
201
- if (input.length === 0) {
202
- // La pista si se recorta: en una ventana estrecha ocupaba una segunda fila
203
- // para no decir nada que el usuario no sepa ya.
204
- formattedInput = chalk.gray(truncatePlain(PLACEHOLDER, cols - symbolWidth));
205
- }
206
- else if (input.startsWith("/")) {
207
- const parts = input.split(" ");
208
- const cmdPart = chalk.bold.hex("#A855F7")(parts[0]);
209
- const restPart = parts.slice(1).join(" ");
210
- formattedInput = restPart ? `${cmdPart} ${chalk.white(restPart)}` : cmdPart;
211
- }
212
- else {
213
- formattedInput = input.replace(/(@[^\s]+)/g, (m) => chalk.bold.hex("#D97757")(m));
214
- }
215
- const inputLine = symbolStr + formattedInput;
216
- const inputRows = physicalRows(inputLine, cols);
217
- // 2. Desplegable. Un marco mas alto que la ventana obliga al terminal a
218
- // desplazar el contenido, y a partir de ahi el repintado ya no sabe donde
219
- // esta su primera fila: se muestra solo una ventana alrededor del elegido.
220
- const room = Math.max(1, viewportRows - 1 - (1 + inputRows + 1 + 1));
221
- const shown = Math.min(items.length, room);
222
- const dropdownOffset = shown > 0 && items.length > shown
223
- ? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
224
- : 0;
225
- const dropdownLines = [];
226
- for (let i = dropdownOffset; i < dropdownOffset + shown; i++) {
227
- const item = items[i];
228
- const isSelected = i === selectedIndex;
229
- const colorHex = item.type === "file" ? "#D97757" : "#A855F7";
230
- const pointer = isSelected ? chalk.bold.hex(colorHex)(" ❯ ") : chalk.gray(" ");
231
- // El puntero se lleva 3 columnas fijas. De lo que queda manda la etiqueta:
232
- // la descripcion es lo primero que se cae, porque una ruta a medias no se
233
- // puede ni leer ni elegir.
234
- const free = cols - 3;
235
- const label = truncatePlain(item.label, free);
236
- const roomForDesc = free - label.length - 2;
237
- const secondary = item.secondary && roomForDesc >= 8 ? truncatePlain(item.secondary, roomForDesc) : "";
238
- const paintedLabel = isSelected ? chalk.bold.white(label) : chalk.hex(colorHex)(label);
239
- const paintedDesc = secondary
240
- ? isSelected
241
- ? chalk.whiteBright(` ${secondary}`)
242
- : chalk.gray(` ${secondary}`)
243
- : "";
244
- dropdownLines.push(`${pointer}${paintedLabel}${paintedDesc}`);
245
- }
246
- // 3. Pie de estado. El relleno se calculaba con Math.max(1, ...): cuando no
247
- // cabia, en vez de recortarse se quedaba con un espacio y la linea salia MAS
248
- // ancha que el terminal. En una consola de 80 columnas el pie media 87 y se
249
- // partia en dos filas desde el primer dibujado.
250
- const modeLook = describeMode(getMode());
251
- const hintPlain = "? /help para atajos · shift+tab modo · esc cancelar";
252
- const modePlain = `⇥ modo ${modeLook.label}`;
253
- const enginePlain = "· Espectro 1.0 (Ágil)";
254
- const hint = chalk.gray(hintPlain);
255
- const modeTag = chalk.hex(modeLook.color)(modePlain);
256
- const engine = chalk.gray(enginePlain);
257
- let footerLine;
258
- if (hintPlain.length + 1 + modePlain.length + 1 + enginePlain.length <= cols) {
259
- const padLen = cols - hintPlain.length - modePlain.length - 1 - enginePlain.length;
260
- footerLine = `${hint}${" ".repeat(padLen)}${modeTag} ${engine}`;
261
- }
262
- else if (modePlain.length + 1 + enginePlain.length <= cols) {
263
- // Los atajos son opcionales; el modo activo no, que es lo que decide si te
264
- // van a preguntar antes de tocar tus archivos.
265
- footerLine = `${" ".repeat(cols - modePlain.length - 1 - enginePlain.length)}${modeTag} ${engine}`;
266
- }
267
- else if (modePlain.length <= cols) {
268
- footerLine = `${" ".repeat(cols - modePlain.length)}${modeTag}`;
269
- }
270
- else {
271
- footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, cols));
272
- }
273
- const lines = [bar, inputLine, ...dropdownLines, bar, footerLine];
274
- // 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
275
- // total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
276
- // cuanto la entrada se partia el acento se iba a escribir a otra linea.
277
- const typed = input.length === 0 ? "" : input.slice(0, cursorIndex);
278
- const segments = typed.split("\n");
279
- let cursorRow = physicalRows(bar, cols);
280
- for (let i = 0; i < segments.length - 1; i++) {
281
- cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
282
- }
283
- const lastSegWidth = (segments.length === 1 ? symbolWidth : 0) + segments[segments.length - 1].length;
284
- cursorRow += Math.floor(lastSegWidth / cols);
285
- const cursorCol = lastSegWidth % cols;
286
- const totalRows = lines.reduce((n, l) => n + physicalRows(l, cols), 0);
287
- return { lines, totalRows, cursorRow, cursorCol, dropdownOffset };
288
- }
289
143
  export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.cwd()) {
290
144
  return new Promise((resolve) => {
291
- // A partir de aqui el terminal es del prompt, que lleva su propia cuenta de
292
- // filas. El pie fijo tiene que olvidar la suya SIN borrar nada: lo que dejo
293
- // en pantalla es respuesta del agente, no adorno. Y devuelve el cursor, que
294
- // lo escondia mientras se repintaba.
295
- reiniciarPie();
145
+ // El relevo de la barra.
146
+ //
147
+ // Mientras el agente trabajaba, el pie tenia pintado ESTE MISMO marco. Se
148
+ // borra -dejando el cursor justo donde empezaba- y acto seguido se dibuja
149
+ // aqui, en el mismo sitio y con el mismo contenido. Desde fuera la barra no
150
+ // se ha movido: no hay parpadeo ni salto, que era la queja.
151
+ //
152
+ // `soltarPie` y no `reiniciarPie`: hay que BORRAR lo que el pie tenia
153
+ // pintado. Olvidarlo sin borrar dejaria dos marcos, uno encima del otro.
154
+ soltarPie();
296
155
  let inputBuffer = "";
297
156
  let cursorIndex = 0;
298
157
  let selectedIndex = 0;
@@ -5,6 +5,19 @@ export declare function renderToolError(name: string, errorSummary: string): voi
5
5
  export declare function renderDiff(filePath: string, diffText: string): void;
6
6
  export declare function renderReasoning(reasoningText: string): void;
7
7
  export declare function renderAssistantResponse(markdownText: string): void;
8
+ /**
9
+ * La linea de cada turno.
10
+ *
11
+ * `⚡ 3.0s · 64 tokens salida (10.8k en caché) · Espectro 1.0 (Ágil) · 99%
12
+ * contexto libre · 28% de la cuota semanal`
13
+ *
14
+ * Con cuenta gestionada NO lleva importe. Se quito primero de /cost y esta se
15
+ * quedo atras; el usuario lo vio enseguida -"aun sigue saliendo el precio que se
16
+ * gasta"- y con razon: /cost se mira cuando uno se acuerda, esta linea sale en
17
+ * TODOS los turnos. De los dos sitios, era el que mas importaba.
18
+ *
19
+ * Con clave propia si sale: ahi el dinero es del usuario y es su factura.
20
+ */
8
21
  export declare function renderFooter(costStr: string, outTokens: number, cachedTokens: number, modelName: string, durationMs?: number, contextPercent?: number, quotaStr?: string): void;
9
22
  export declare function renderError(message: string): void;
10
23
  export declare function formatToolName(name: string): string;
@@ -1,6 +1,21 @@
1
+ /**
2
+ * POR QUE ESTO NO USA `console.log`
3
+ *
4
+ * Mientras el agente trabaja hay un bloque pintado abajo del todo -el spinner y
5
+ * la barra de escribir- que se borra y se repinta alrededor de cada escritura.
6
+ * Un `console.log` no pasa por ahi: escribe donde este el cursor, encima del
7
+ * bloque, y el repintado siguiente ya no sabe donde empieza el suyo. En pantalla
8
+ * eso se ve como un spinner que se queda atras y filas huerfanas por todos lados.
9
+ *
10
+ * Ademas era el ultimo sitio que ataba el agente a una terminal. `salida` existe
11
+ * justamente para que el bucle no sepa donde acaba lo que dice, y el
12
+ * renderizador -que es quien mas imprime- seguia escribiendo directo.
13
+ */
1
14
  import chalk from "chalk";
2
15
  import { Marked } from "marked";
3
16
  import { markedTerminal } from "marked-terminal";
17
+ import { salida } from "../agent/salidaDelAgente.js";
18
+ import { isManagedMode } from "../config/quota.js";
4
19
  const marked = new Marked(markedTerminal({
5
20
  heading: chalk.bold.hex("#D97757"),
6
21
  firstHeading: chalk.bold.hex("#D97757").underline,
@@ -22,14 +37,14 @@ const marked = new Marked(markedTerminal({
22
37
  export function renderUserPrompt(text) {
23
38
  const symbol = chalk.bold.hex("#D97757")("❯");
24
39
  const shaded = chalk.bgHex("#242730").hex("#F4F4F5").bold(` ${text.trim()} `);
25
- console.log(`\n${symbol} ${shaded}\n`);
40
+ salida.linea(`\n${symbol} ${shaded}\n`);
26
41
  }
27
42
  // Tool call rendering as specified in §B5
28
43
  export function renderToolStart(name, summary) {
29
44
  const bullet = chalk.hex("#FFA500")("●"); // Amber while starting
30
45
  const toolName = chalk.bold.white(formatToolName(name));
31
46
  const argPart = summary ? chalk.gray(`(${summary.length > 60 ? summary.slice(0, 57) + '...' : summary})`) : "";
32
- console.log(`\n${bullet} ${toolName}${argPart}`);
47
+ salida.linea(`\n${bullet} ${toolName}${argPart}`);
33
48
  }
34
49
  export function renderToolSuccess(name, resultSummary) {
35
50
  const rawLines = resultSummary.trim().split("\n");
@@ -40,23 +55,23 @@ export function renderToolSuccess(name, resultSummary) {
40
55
  const prefix = idx === 0 ? ` ${chalk.gray("⎿")} ` : " ";
41
56
  return `${prefix}${colorizeText(line)}`;
42
57
  }).join("\n");
43
- console.log(indented);
58
+ salida.linea(indented);
44
59
  if (remainingCount > 0) {
45
- console.log(chalk.gray(` … +${remainingCount} líneas más`));
60
+ salida.linea(chalk.gray(` … +${remainingCount} líneas más`));
46
61
  }
47
62
  }
48
63
  export function renderToolError(name, errorSummary) {
49
- console.log(` ${chalk.red("⎿")} ${chalk.red(errorSummary)}`);
64
+ salida.linea(` ${chalk.red("⎿")} ${chalk.red(errorSummary)}`);
50
65
  }
51
66
  export function renderDiff(filePath, diffText) {
52
- console.log(`\n${diffText}\n`);
67
+ salida.linea(`\n${diffText}\n`);
53
68
  }
54
69
  export function renderReasoning(reasoningText) {
55
70
  if (!reasoningText.trim())
56
71
  return;
57
72
  const lines = reasoningText.trim().split("\n");
58
73
  const formatted = lines.map((l) => ` ${chalk.hex("#8B5CF6")("│")} ${chalk.gray(l)}`).join("\n");
59
- console.log(`\n ${chalk.bold.hex("#8B5CF6")("💭 Razonamiento:")}\n${formatted}\n`);
74
+ salida.linea(`\n ${chalk.bold.hex("#8B5CF6")("💭 Razonamiento:")}\n${formatted}\n`);
60
75
  }
61
76
  export function renderAssistantResponse(markdownText) {
62
77
  try {
@@ -68,23 +83,37 @@ export function renderAssistantResponse(markdownText) {
68
83
  let parsed = String(marked.parse(cleaned));
69
84
  // Convert raw asterisks bullets to terracotta bullets
70
85
  parsed = parsed.replace(/^(\s*)\*\s+/gm, `$1${chalk.hex("#D97757")("•")} `);
71
- console.log(`\n${parsed}`);
86
+ salida.linea(`\n${parsed}`);
72
87
  }
73
88
  catch {
74
- console.log(`\n${markdownText.trim()}\n`);
89
+ salida.linea(`\n${markdownText.trim()}\n`);
75
90
  }
76
91
  }
92
+ /**
93
+ * La linea de cada turno.
94
+ *
95
+ * `⚡ 3.0s · 64 tokens salida (10.8k en caché) · Espectro 1.0 (Ágil) · 99%
96
+ * contexto libre · 28% de la cuota semanal`
97
+ *
98
+ * Con cuenta gestionada NO lleva importe. Se quito primero de /cost y esta se
99
+ * quedo atras; el usuario lo vio enseguida -"aun sigue saliendo el precio que se
100
+ * gasta"- y con razon: /cost se mira cuando uno se acuerda, esta linea sale en
101
+ * TODOS los turnos. De los dos sitios, era el que mas importaba.
102
+ *
103
+ * Con clave propia si sale: ahi el dinero es del usuario y es su factura.
104
+ */
77
105
  export function renderFooter(costStr, outTokens, cachedTokens, modelName, durationMs, contextPercent, quotaStr) {
78
106
  const durationPart = durationMs ? `${(durationMs / 1000).toFixed(1)}s · ` : "";
79
107
  const outPart = `${outTokens.toLocaleString()} tokens salida`;
80
108
  const cachePart = cachedTokens > 0 ? ` (${(cachedTokens / 1000).toFixed(1)}k en caché)` : "";
81
109
  const contextPart = contextPercent !== undefined ? ` · ${contextPercent}% contexto libre` : "";
82
110
  const quotaPart = quotaStr ? ` · ${quotaStr}` : "";
83
- const line = chalk.gray(` ${chalk.hex("#D97757")("")} ${durationPart}${outPart}${cachePart} · ${chalk.green(costStr)} · ${modelName}${contextPart}${quotaPart}`);
84
- console.log(`${line}\n`);
111
+ const costPart = isManagedMode() ? "" : ` · ${chalk.green(costStr)}`;
112
+ const line = chalk.gray(` ${chalk.hex("#D97757")("⚡")} ${durationPart}${outPart}${cachePart}${costPart} · ${modelName}${contextPart}${quotaPart}`);
113
+ salida.linea(`${line}\n`);
85
114
  }
86
115
  export function renderError(message) {
87
- console.log(`\n ${chalk.bgRed.white.bold(" ERROR ")} ${chalk.red(message)}\n`);
116
+ salida.linea(`\n ${chalk.bgRed.white.bold(" ERROR ")} ${chalk.red(message)}\n`);
88
117
  }
89
118
  function colorizeText(text) {
90
119
  // Highlight file paths (C:\..., /path/...) in cyan
@@ -19,13 +19,41 @@ export declare class DynamicSpinner {
19
19
  */
20
20
  private lineaDelSpinner;
21
21
  /**
22
- * La barra de escribir, debajo del spinner.
22
+ * LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
23
23
  *
24
- * Antes solo aparecia si ya habias escrito algo, asi que mientras el agente
25
- * trabajaba no habia ninguna senal de que se pudiera escribir: el teclado es
26
- * del vigilante y lo tecleado se acumulaba a ciegas. Ahora la barra esta
27
- * siempre, y es la misma que se ve al escribir el prompt.
24
+ * Antes habia dos: al esperar salia el marco entero -barra, entrada, barra,
25
+ * linea de ayuda-; al trabajar, una version recortada con otro texto. Se
26
+ * parecian lo justo para que el cambio de una a otra se viera como un salto.
27
+ * Palabras del usuario: "aun no es fija... debe ser fija y por debajo deben
28
+ * pasar los otros mensajes".
29
+ *
30
+ * Ahora las dos salen de `buildPromptFrame`, asi que no pueden divergir: quien
31
+ * toque el marco lo toca en los dos estados a la vez.
32
+ *
33
+ * Sin desplegable: el autocompletado de "@" y "/" es del prompt de verdad, que
34
+ * es quien tiene el teclado. Aqui lo tecleado solo se encola.
28
35
  */
29
36
  private lineasDeEntrada;
30
37
  stop(): void;
31
38
  }
39
+ /**
40
+ * DEJA EL PIE PUESTO MIENTRAS CORRE ALGO
41
+ *
42
+ * El spinner solo vivia mientras hablaba el modelo. En cuanto empezaba una
43
+ * herramienta se paraba, y ahi se acababa lo unico fijo de la pantalla: durante
44
+ * un `run_command` de veinte segundos, o una lectura de un archivo enorme, no
45
+ * habia ni cronometro ni forma de saber que seguia vivo. Justo el rato en que
46
+ * uno se pregunta si se ha colgado.
47
+ *
48
+ * EL RETRASO NO ES UN CAPRICHO
49
+ *
50
+ * La mayoria de las herramientas tardan milisegundos. Pintar el pie y borrarlo
51
+ * en el mismo suspiro es un parpadeo en cada llamada, y encima el pie mueve
52
+ * lineas: veinte herramientas seguidas serian veinte sacudidas. Con 150 ms de
53
+ * espera, lo rapido no pinta nada y lo lento sale igual.
54
+ *
55
+ * El `finally` es obligatorio. Un spinner que se queda corriendo repinta el pie
56
+ * ocho veces por segundo encima de lo que venga despues -incluido el prompt-, y
57
+ * de ahi no se sale sin cerrar el programa.
58
+ */
59
+ export declare function conSpinner<T>(etiqueta: string, tarea: () => Promise<T>, retrasoMs?: number): Promise<T>;
@@ -2,7 +2,8 @@ import chalk from "chalk";
2
2
  import process from "node:process";
3
3
  import { typedText, queuedCount } from "./typeAhead.js";
4
4
  import { modeBadge } from "./modes.js";
5
- import { fijarSecciones } from "./pieFijo.js";
5
+ import { fijarSecciones, quitarSeccion } from "./pieFijo.js";
6
+ import { buildPromptFrame } from "./marco.js";
6
7
  /**
7
8
  * LA LINEA QUE DICE QUE ESTO SIGUE VIVO
8
9
  *
@@ -31,18 +32,9 @@ const VERBS = [
31
32
  "Tejiendo",
32
33
  "Cocinando",
33
34
  ];
34
- const PISTA_VACIA = "escribe para encolar un mensaje…";
35
35
  function sinColores(texto) {
36
36
  return texto.replace(/\x1b\[[0-9;]*m/g, "");
37
37
  }
38
- /** Deja las ULTIMAS `ancho` columnas: al escribir, lo que importa es el final. */
39
- function colaVisible(texto, ancho) {
40
- if (ancho <= 0)
41
- return "";
42
- if (texto.length <= ancho)
43
- return texto;
44
- return `…${texto.slice(-(ancho - 1))}`;
45
- }
46
38
  export class DynamicSpinner {
47
39
  timer = null;
48
40
  glyphIdx = 0;
@@ -109,22 +101,30 @@ export class DynamicSpinner {
109
101
  return hueco >= 2 ? `${izquierda}${" ".repeat(hueco)}${badge}` : izquierda;
110
102
  }
111
103
  /**
112
- * La barra de escribir, debajo del spinner.
104
+ * LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
105
+ *
106
+ * Antes habia dos: al esperar salia el marco entero -barra, entrada, barra,
107
+ * linea de ayuda-; al trabajar, una version recortada con otro texto. Se
108
+ * parecian lo justo para que el cambio de una a otra se viera como un salto.
109
+ * Palabras del usuario: "aun no es fija... debe ser fija y por debajo deben
110
+ * pasar los otros mensajes".
113
111
  *
114
- * Antes solo aparecia si ya habias escrito algo, asi que mientras el agente
115
- * trabajaba no habia ninguna senal de que se pudiera escribir: el teclado es
116
- * del vigilante y lo tecleado se acumulaba a ciegas. Ahora la barra esta
117
- * siempre, y es la misma que se ve al escribir el prompt.
112
+ * Ahora las dos salen de `buildPromptFrame`, asi que no pueden divergir: quien
113
+ * toque el marco lo toca en los dos estados a la vez.
114
+ *
115
+ * Sin desplegable: el autocompletado de "@" y "/" es del prompt de verdad, que
116
+ * es quien tiene el teclado. Aqui lo tecleado solo se encola.
118
117
  */
119
118
  lineasDeEntrada() {
120
- const cols = Math.max(20, process.stdout.columns || 80);
121
- const typing = typedText();
119
+ const marco = buildPromptFrame({
120
+ cols: Math.max(20, process.stdout.columns || 80),
121
+ rows: process.stdout.rows || 24,
122
+ input: typedText(),
123
+ cursorIndex: typedText().length,
124
+ items: [],
125
+ });
126
+ const filas = [...marco.lines];
122
127
  const pendientes = queuedCount();
123
- const simbolo = chalk.bold.hex("#D97757")("❯");
124
- const cuerpo = typing.length > 0
125
- ? `${chalk.white(colaVisible(typing, cols - 3))}${chalk.hex("#D97757")("▌")}`
126
- : chalk.gray(colaVisible(PISTA_VACIA, cols - 2));
127
- const filas = [chalk.gray("─".repeat(cols)), `${simbolo} ${cuerpo}`];
128
128
  if (pendientes > 0) {
129
129
  const plural = pendientes === 1 ? "mensaje en cola" : "mensajes en cola";
130
130
  filas.push(chalk.gray(` ${pendientes} ${plural}; se enviará al terminar este turno`));
@@ -139,11 +139,48 @@ export class DynamicSpinner {
139
139
  clearInterval(this.timer);
140
140
  this.timer = null;
141
141
  }
142
- // Las dos de golpe: quitarlas por separado deja un fotograma con la barra
143
- // de escribir suelta y sin spinner encima.
144
- fijarSecciones([
145
- ["spinner", []],
146
- ["entrada", []],
147
- ]);
142
+ // Solo se va la linea del spinner. La BARRA se queda.
143
+ //
144
+ // Es lo que la hace fija de verdad: entre una herramienta y la siguiente el
145
+ // spinner se para y vuelve a arrancar, y si la barra se fuera con el, se
146
+ // veria desaparecer y reaparecer en cada llamada. Quien la retira es el
147
+ // prompt al abrirse (soltarPie), y ahi el relevo no se nota porque lo que
148
+ // dibuja justo despues es exactamente el mismo marco.
149
+ quitarSeccion("spinner");
150
+ }
151
+ }
152
+ /**
153
+ * DEJA EL PIE PUESTO MIENTRAS CORRE ALGO
154
+ *
155
+ * El spinner solo vivia mientras hablaba el modelo. En cuanto empezaba una
156
+ * herramienta se paraba, y ahi se acababa lo unico fijo de la pantalla: durante
157
+ * un `run_command` de veinte segundos, o una lectura de un archivo enorme, no
158
+ * habia ni cronometro ni forma de saber que seguia vivo. Justo el rato en que
159
+ * uno se pregunta si se ha colgado.
160
+ *
161
+ * EL RETRASO NO ES UN CAPRICHO
162
+ *
163
+ * La mayoria de las herramientas tardan milisegundos. Pintar el pie y borrarlo
164
+ * en el mismo suspiro es un parpadeo en cada llamada, y encima el pie mueve
165
+ * lineas: veinte herramientas seguidas serian veinte sacudidas. Con 150 ms de
166
+ * espera, lo rapido no pinta nada y lo lento sale igual.
167
+ *
168
+ * El `finally` es obligatorio. Un spinner que se queda corriendo repinta el pie
169
+ * ocho veces por segundo encima de lo que venga despues -incluido el prompt-, y
170
+ * de ahi no se sale sin cerrar el programa.
171
+ */
172
+ export async function conSpinner(etiqueta, tarea, retrasoMs = 150) {
173
+ let girando = null;
174
+ const aviso = setTimeout(() => {
175
+ girando = new DynamicSpinner(etiqueta);
176
+ girando.start();
177
+ }, retrasoMs);
178
+ try {
179
+ return await tarea();
180
+ }
181
+ finally {
182
+ clearTimeout(aviso);
183
+ if (girando)
184
+ girando.stop();
148
185
  }
149
186
  }
@@ -1,60 +1,60 @@
1
- {
2
- "manifest_version": 3,
3
- "minimum_chrome_version": "102",
4
- "name": "Chocolatito Code",
5
- "version": "1.3.0",
6
- "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
- "permissions": [
8
- "tabs",
9
- "tabGroups",
10
- "scripting",
11
- "debugger",
12
- "webNavigation",
13
- "storage",
14
- "downloads"
15
- ],
16
- "host_permissions": [
17
- "<all_urls>"
18
- ],
19
- "icons": {
20
- "16": "iconos/16.png",
21
- "32": "iconos/32.png",
22
- "48": "iconos/48.png",
23
- "128": "iconos/128.png"
24
- },
25
- "background": {
26
- "service_worker": "background.js"
27
- },
28
- "content_scripts": [
29
- {
30
- "matches": [
31
- "<all_urls>"
32
- ],
33
- "js": [
34
- "content.js"
35
- ],
36
- "run_at": "document_idle",
37
- "all_frames": true
38
- }
39
- ],
40
- "action": {
41
- "default_title": "Chocolatito Code",
42
- "default_popup": "popup.html",
43
- "default_icon": {
44
- "16": "iconos/16.png",
45
- "32": "iconos/32.png",
46
- "48": "iconos/48.png"
47
- }
48
- },
49
- "web_accessible_resources": [
50
- {
51
- "resources": [
52
- "iconos/*",
53
- "fuentes/*"
54
- ],
55
- "matches": [
56
- "<all_urls>"
57
- ]
58
- }
59
- ]
60
- }
1
+ {
2
+ "manifest_version": 3,
3
+ "minimum_chrome_version": "102",
4
+ "name": "Chocolatito Code",
5
+ "version": "1.3.1",
6
+ "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
+ "permissions": [
8
+ "tabs",
9
+ "tabGroups",
10
+ "scripting",
11
+ "debugger",
12
+ "webNavigation",
13
+ "storage",
14
+ "downloads"
15
+ ],
16
+ "host_permissions": [
17
+ "<all_urls>"
18
+ ],
19
+ "icons": {
20
+ "16": "iconos/16.png",
21
+ "32": "iconos/32.png",
22
+ "48": "iconos/48.png",
23
+ "128": "iconos/128.png"
24
+ },
25
+ "background": {
26
+ "service_worker": "background.js"
27
+ },
28
+ "content_scripts": [
29
+ {
30
+ "matches": [
31
+ "<all_urls>"
32
+ ],
33
+ "js": [
34
+ "content.js"
35
+ ],
36
+ "run_at": "document_idle",
37
+ "all_frames": true
38
+ }
39
+ ],
40
+ "action": {
41
+ "default_title": "Chocolatito Code",
42
+ "default_popup": "popup.html",
43
+ "default_icon": {
44
+ "16": "iconos/16.png",
45
+ "32": "iconos/32.png",
46
+ "48": "iconos/48.png"
47
+ }
48
+ },
49
+ "web_accessible_resources": [
50
+ {
51
+ "resources": [
52
+ "iconos/*",
53
+ "fuentes/*"
54
+ ],
55
+ "matches": [
56
+ "<all_urls>"
57
+ ]
58
+ }
59
+ ]
60
+ }