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/agent/loop.js +49 -13
- package/dist/config/quota.d.ts +11 -0
- package/dist/config/quota.js +18 -0
- package/dist/index.js +35 -26
- package/dist/prompts/systemPrompt.js +186 -150
- package/dist/ui/banner.d.ts +31 -0
- package/dist/ui/banner.js +45 -0
- package/dist/ui/comandos.d.ts +33 -0
- package/dist/ui/comandos.js +49 -0
- package/dist/ui/informeDeUso.d.ts +17 -0
- package/dist/ui/informeDeUso.js +81 -0
- package/dist/ui/marco.d.ts +35 -0
- package/dist/ui/marco.js +161 -0
- package/dist/ui/prompt.d.ts +4 -41
- package/dist/ui/prompt.js +21 -162
- package/dist/ui/renderer.d.ts +13 -0
- package/dist/ui/renderer.js +41 -12
- package/dist/ui/spinner.d.ts +33 -5
- package/dist/ui/spinner.js +65 -28
- package/extension/manifest.json +60 -60
- package/package.json +67 -67
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
|
|
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
|
-
|
|
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
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
|
|
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;
|
package/dist/ui/renderer.d.ts
CHANGED
|
@@ -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;
|
package/dist/ui/renderer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
+
salida.linea(indented);
|
|
44
59
|
if (remainingCount > 0) {
|
|
45
|
-
|
|
60
|
+
salida.linea(chalk.gray(` … +${remainingCount} líneas más`));
|
|
46
61
|
}
|
|
47
62
|
}
|
|
48
63
|
export function renderToolError(name, errorSummary) {
|
|
49
|
-
|
|
64
|
+
salida.linea(` ${chalk.red("⎿")} ${chalk.red(errorSummary)}`);
|
|
50
65
|
}
|
|
51
66
|
export function renderDiff(filePath, diffText) {
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
salida.linea(`\n${parsed}`);
|
|
72
87
|
}
|
|
73
88
|
catch {
|
|
74
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
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
|
package/dist/ui/spinner.d.ts
CHANGED
|
@@ -19,13 +19,41 @@ export declare class DynamicSpinner {
|
|
|
19
19
|
*/
|
|
20
20
|
private lineaDelSpinner;
|
|
21
21
|
/**
|
|
22
|
-
* La
|
|
22
|
+
* LA BARRA. La MISMA que se ve al escribir una orden, no una parecida.
|
|
23
23
|
*
|
|
24
|
-
* Antes
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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>;
|
package/dist/ui/spinner.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
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
|
|
121
|
-
|
|
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
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
}
|
package/extension/manifest.json
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"manifest_version": 3,
|
|
3
|
-
"minimum_chrome_version": "102",
|
|
4
|
-
"name": "Chocolatito Code",
|
|
5
|
-
"version": "1.3.
|
|
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
|
+
}
|