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.
- package/dist/agent/huecos.d.ts +58 -0
- package/dist/agent/huecos.js +81 -0
- package/dist/agent/loop.js +144 -24
- package/dist/agent/salidaDelAgente.js +10 -24
- package/dist/agent/toolGate.js +6 -1
- package/dist/config/engine.d.ts +21 -1
- package/dist/config/engine.js +22 -1
- 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/tools/browserExtension.js +118 -3
- package/dist/tools/computerUse.d.ts +1 -1
- package/dist/tools/computerUse.js +183 -2
- package/dist/tools/toolDefsComputer.js +6 -2
- package/dist/tools/visionBridge.js +60 -3
- package/dist/tools/win/hostScript.js +154 -0
- 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/interrupt.d.ts +10 -0
- package/dist/ui/interrupt.js +50 -2
- package/dist/ui/marco.d.ts +35 -0
- package/dist/ui/marco.js +161 -0
- package/dist/ui/pieFijo.d.ts +56 -0
- package/dist/ui/pieFijo.js +309 -0
- package/dist/ui/prompt.d.ts +4 -41
- package/dist/ui/prompt.js +24 -157
- package/dist/ui/reasoningStream.js +22 -12
- package/dist/ui/renderer.d.ts +13 -0
- package/dist/ui/renderer.js +41 -12
- package/dist/ui/spinner.d.ts +43 -8
- package/dist/ui/spinner.js +118 -43
- package/extension/background.js +9 -3
- package/extension/iconos/128.png +0 -0
- package/extension/iconos/16.png +0 -0
- package/extension/iconos/32.png +0 -0
- package/extension/iconos/48.png +0 -0
- package/extension/iconos/ORIGEN.md +48 -0
- package/extension/manifest.json +60 -60
- package/package.json +67 -67
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { dibujar } from "./salida.js";
|
|
3
|
+
/**
|
|
4
|
+
* EL PIE QUE NO SE VA CON EL SCROLL
|
|
5
|
+
*
|
|
6
|
+
* EL PROBLEMA
|
|
7
|
+
*
|
|
8
|
+
* El spinner y la linea de razonamiento se pintaban donde estuviera el cursor:
|
|
9
|
+
*
|
|
10
|
+
* process.stdout.write(`\r\x1b[2K${glyph} ${verb} (${segundos}s)`);
|
|
11
|
+
*
|
|
12
|
+
* Eso escribe en la fila actual y ahi se queda. En cuanto llega la respuesta,
|
|
13
|
+
* cada linea nueva empuja el contenido hacia arriba y se lleva el spinner con
|
|
14
|
+
* ella. El usuario lo describio mejor que ninguna documentacion: "la nubecita y
|
|
15
|
+
* el rayito suben con lo que me respondes". Y lo mismo la barra de escribir.
|
|
16
|
+
*
|
|
17
|
+
* El resultado es que mientras el modelo habla no hay NADA fijo en pantalla: ni
|
|
18
|
+
* cuanto lleva, ni cuantos tokens, ni donde escribir para encolar algo. Todo
|
|
19
|
+
* queda enterrado unas pantallas mas arriba.
|
|
20
|
+
*
|
|
21
|
+
* LA IDEA
|
|
22
|
+
*
|
|
23
|
+
* Un terminal no tiene "zonas". Se puede pedir una con la region de scroll
|
|
24
|
+
* (DECSTBM), pero lo que sale por arriba de una region asi NO entra en el
|
|
25
|
+
* historial del terminal en la mayoria de emuladores: se pierde. En un agente
|
|
26
|
+
* de codigo, donde media sesion consiste en subir a releer lo que salio, eso es
|
|
27
|
+
* inaceptable.
|
|
28
|
+
*
|
|
29
|
+
* Asi que se hace al reves, sin regiones: el pie se BORRA antes de cada
|
|
30
|
+
* escritura permanente y se REPINTA justo despues. El texto permanente se
|
|
31
|
+
* escribe con saltos de linea normales, entra en el historial como siempre, y
|
|
32
|
+
* el pie acaba de nuevo abajo del todo. Visto desde fuera, el pie no se mueve.
|
|
33
|
+
*
|
|
34
|
+
* LA INVARIANTE QUE LO SOSTIENE
|
|
35
|
+
*
|
|
36
|
+
* Tras pintar, el cursor queda al final de la ultima fila del bloque. Con eso
|
|
37
|
+
* basta para borrarlo entero: subir `filasPintadas - 1` filas, ir a la columna
|
|
38
|
+
* cero y borrar hacia abajo. Se cuentan filas FISICAS (lo que el terminal parte
|
|
39
|
+
* al llegar al ancho), no elementos del array: contar lineas logicas es
|
|
40
|
+
* exactamente el fallo que dejaba barras huerfanas en el prompt.
|
|
41
|
+
*
|
|
42
|
+
* Y LA SEGUNDA, LA QUE EVITA CORROMPER LA SALIDA AJENA
|
|
43
|
+
*
|
|
44
|
+
* Sin secciones puestas, el pie no pinta nada y la pantalla queda byte a byte
|
|
45
|
+
* como la habria dejado un `console.log` normal. Por eso el resto del programa
|
|
46
|
+
* -las herramientas, el renderizador, los avisos- puede seguir escribiendo
|
|
47
|
+
* directo sin saber que esto existe. Solo hay que pasar por aqui mientras haya
|
|
48
|
+
* pie, que es justo el rato que el bucle controla toda la salida.
|
|
49
|
+
*/
|
|
50
|
+
const OCULTAR_CURSOR = "\x1b[?25l";
|
|
51
|
+
const MOSTRAR_CURSOR = "\x1b[?25h";
|
|
52
|
+
/**
|
|
53
|
+
* Las secciones, de arriba abajo. El orden es fijo a proposito: si cada quien
|
|
54
|
+
* pintara donde le tocara, dos productores independientes -el razonamiento y el
|
|
55
|
+
* spinner llegan por caminos distintos- se turnarian el sitio y el pie bailaria.
|
|
56
|
+
*/
|
|
57
|
+
const ORDEN = ["razonamiento", "spinner", "entrada"];
|
|
58
|
+
const secciones = new Map();
|
|
59
|
+
/**
|
|
60
|
+
* Texto permanente que aun no ha recibido su salto de linea.
|
|
61
|
+
*
|
|
62
|
+
* Un `flush()` de markdown puede dejar media linea escrita. Si el pie se
|
|
63
|
+
* pintara detras, empezaria en mitad de esa fila. En vez de calcular columnas
|
|
64
|
+
* -aritmetica que se rompe con cada emoji- se guarda ese resto AQUI y se
|
|
65
|
+
* repinta como primera linea del bloque. Deja de ser texto permanente hasta que
|
|
66
|
+
* llegue su salto, y entonces se escribe de verdad.
|
|
67
|
+
*/
|
|
68
|
+
let colaSinSalto = "";
|
|
69
|
+
/** Filas fisicas que ocupo el bloque en el ultimo pintado. Es lo que se borra. */
|
|
70
|
+
let filasPintadas = 0;
|
|
71
|
+
/**
|
|
72
|
+
* Lo ultimo que escribimos NOSOTROS se quedo a medio renglon.
|
|
73
|
+
*
|
|
74
|
+
* Sin secciones puestas, la linea a medias deja de estar protegida y sale como
|
|
75
|
+
* texto permanente (ver `repintar`). Si despues vuelve a haber pie, tiene que
|
|
76
|
+
* empezar en un renglon nuevo y no pegado al final de esa frase.
|
|
77
|
+
*/
|
|
78
|
+
let aMediaLinea = false;
|
|
79
|
+
let cursorEscondido = false;
|
|
80
|
+
/** `null` = decidirlo por el terminal. Las pruebas lo fuerzan. */
|
|
81
|
+
let habilitado = null;
|
|
82
|
+
function pieUsable() {
|
|
83
|
+
return habilitado ?? Boolean(process.stdout.isTTY);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Enciende o apaga el pie a mano. Con `null` vuelve a mandar el terminal.
|
|
87
|
+
*
|
|
88
|
+
* Las pruebas lo encienden porque en `node --test` no hay TTY y sin esto el pie
|
|
89
|
+
* seria un no-op silencioso: pasarian todas sin comprobar nada.
|
|
90
|
+
*/
|
|
91
|
+
export function habilitarPie(valor) {
|
|
92
|
+
habilitado = valor;
|
|
93
|
+
}
|
|
94
|
+
function ancho() {
|
|
95
|
+
return Math.max(20, process.stdout.columns || 80);
|
|
96
|
+
}
|
|
97
|
+
function alto() {
|
|
98
|
+
return Math.max(4, process.stdout.rows || 24);
|
|
99
|
+
}
|
|
100
|
+
function sinColores(texto) {
|
|
101
|
+
return texto.replace(/\x1b\[[0-9;]*m/g, "");
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Filas de pantalla que ocupa una linea con este ancho.
|
|
105
|
+
*
|
|
106
|
+
* `ceil(largo / cols)` coincide con el ajuste diferido de los terminales: tras
|
|
107
|
+
* escribir exactamente `cols` caracteres el cursor sigue en esa fila esperando,
|
|
108
|
+
* y eso es una fila, no dos.
|
|
109
|
+
*/
|
|
110
|
+
function filasFisicas(linea, cols) {
|
|
111
|
+
return sinColores(linea)
|
|
112
|
+
.split("\n")
|
|
113
|
+
.reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
|
|
114
|
+
}
|
|
115
|
+
function lineasDelBloque() {
|
|
116
|
+
const lineas = [];
|
|
117
|
+
if (colaSinSalto)
|
|
118
|
+
lineas.push(colaSinSalto);
|
|
119
|
+
for (const nombre of ORDEN) {
|
|
120
|
+
const trozo = secciones.get(nombre);
|
|
121
|
+
if (trozo && trozo.length > 0)
|
|
122
|
+
lineas.push(...trozo);
|
|
123
|
+
}
|
|
124
|
+
return lineas;
|
|
125
|
+
}
|
|
126
|
+
/** Secuencia que deja el cursor donde empezaba el bloque, con todo borrado debajo. */
|
|
127
|
+
function borrado() {
|
|
128
|
+
if (filasPintadas === 0)
|
|
129
|
+
return "";
|
|
130
|
+
const subir = filasPintadas > 1 ? `\x1b[${filasPintadas - 1}A` : "";
|
|
131
|
+
return `\r${subir}\x1b[0J`;
|
|
132
|
+
}
|
|
133
|
+
/** Secuencia del bloque nuevo. Actualiza `filasPintadas`. */
|
|
134
|
+
function pintado() {
|
|
135
|
+
let lineas = lineasDelBloque();
|
|
136
|
+
if (lineas.length === 0) {
|
|
137
|
+
filasPintadas = 0;
|
|
138
|
+
return "";
|
|
139
|
+
}
|
|
140
|
+
const cols = ancho();
|
|
141
|
+
// Un pie mas alto que la ventana no se puede repintar: al pintarlo el
|
|
142
|
+
// terminal desplaza, y la cuenta de filas -que es lo unico que sabemos para
|
|
143
|
+
// borrarlo- deja de valer. Se recorta por abajo, que es donde esta lo menos
|
|
144
|
+
// urgente; la cola sin salto va primera y nunca se cae.
|
|
145
|
+
const tope = alto() - 1;
|
|
146
|
+
let filas = lineas.reduce((n, l) => n + filasFisicas(l, cols), 0);
|
|
147
|
+
while (filas > tope && lineas.length > 1) {
|
|
148
|
+
lineas = lineas.slice(0, -1);
|
|
149
|
+
filas = lineas.reduce((n, l) => n + filasFisicas(l, cols), 0);
|
|
150
|
+
}
|
|
151
|
+
filasPintadas = filas;
|
|
152
|
+
return lineas.join("\n");
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* El unico sitio que escribe. Borra lo de antes, suelta el texto permanente en
|
|
156
|
+
* medio, y vuelve a pintar el pie debajo. Todo en UNA escritura: un pie pintado
|
|
157
|
+
* a trozos es un pie que parpadea.
|
|
158
|
+
*/
|
|
159
|
+
function repintar(permanente) {
|
|
160
|
+
const fuera = borrado();
|
|
161
|
+
// Sin secciones ya no hay de que proteger la linea a medias, y retenerla seria
|
|
162
|
+
// peor que soltarla: el pie seguiria contando esa fila como suya, y el
|
|
163
|
+
// siguiente repintado borraria lo que hubiera escrito cualquier otro por su
|
|
164
|
+
// cuenta -las herramientas imprimen con console.log y no pasan por aqui-.
|
|
165
|
+
// Soltandola, `filasPintadas` vuelve a cero y el pie no debe nada a nadie.
|
|
166
|
+
let suelto = "";
|
|
167
|
+
if (secciones.size === 0 && colaSinSalto) {
|
|
168
|
+
suelto = colaSinSalto;
|
|
169
|
+
colaSinSalto = "";
|
|
170
|
+
}
|
|
171
|
+
let dentro = pintado();
|
|
172
|
+
const texto = permanente + suelto;
|
|
173
|
+
if (texto)
|
|
174
|
+
aMediaLinea = !texto.endsWith("\n");
|
|
175
|
+
if (dentro && aMediaLinea) {
|
|
176
|
+
// El salto va DELANTE del bloque, y no cuenta como fila suya: el borrado
|
|
177
|
+
// sube hasta la primera fila del pie, no hasta la frase de encima.
|
|
178
|
+
dentro = `\n${dentro}`;
|
|
179
|
+
aMediaLinea = false;
|
|
180
|
+
}
|
|
181
|
+
let salida = fuera + texto + dentro;
|
|
182
|
+
if (dentro && !cursorEscondido) {
|
|
183
|
+
// Con el pie repintandose cada 120 ms, un cursor visible salta de sitio sin
|
|
184
|
+
// parar. Se esconde mientras dure y se devuelve al soltarlo.
|
|
185
|
+
salida = OCULTAR_CURSOR + salida;
|
|
186
|
+
cursorEscondido = true;
|
|
187
|
+
}
|
|
188
|
+
else if (!dentro && cursorEscondido) {
|
|
189
|
+
salida += MOSTRAR_CURSOR;
|
|
190
|
+
cursorEscondido = false;
|
|
191
|
+
}
|
|
192
|
+
if (salida)
|
|
193
|
+
dibujar(salida);
|
|
194
|
+
}
|
|
195
|
+
/** ¿Hay algo pintado abajo que haya que respetar? */
|
|
196
|
+
export function hayPie() {
|
|
197
|
+
return filasPintadas > 0;
|
|
198
|
+
}
|
|
199
|
+
function iguales(a, b) {
|
|
200
|
+
if (!a || a.length !== b.length)
|
|
201
|
+
return false;
|
|
202
|
+
return a.every((l, i) => l === b[i]);
|
|
203
|
+
}
|
|
204
|
+
/** Guarda la seccion. Devuelve si algo cambio, que es lo que decide repintar. */
|
|
205
|
+
function anotar(nombre, lineas) {
|
|
206
|
+
if (lineas.length === 0) {
|
|
207
|
+
return secciones.delete(nombre);
|
|
208
|
+
}
|
|
209
|
+
if (iguales(secciones.get(nombre), lineas))
|
|
210
|
+
return false;
|
|
211
|
+
secciones.set(nombre, lineas);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Pone (o reemplaza) una seccion del pie y repinta.
|
|
216
|
+
*
|
|
217
|
+
* Con la lista vacia la seccion desaparece, que es lo mismo que quitarla. Si el
|
|
218
|
+
* contenido es identico al que ya habia, no se repinta: el spinner llama ocho
|
|
219
|
+
* veces por segundo y la barra de escribir casi nunca cambia entre dos.
|
|
220
|
+
*/
|
|
221
|
+
export function fijarSeccion(nombre, lineas) {
|
|
222
|
+
if (!pieUsable())
|
|
223
|
+
return;
|
|
224
|
+
if (anotar(nombre, lineas))
|
|
225
|
+
repintar("");
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Varias secciones de una vez, con UN solo repintado.
|
|
229
|
+
*
|
|
230
|
+
* El spinner cambia su linea y la barra de escribir en el mismo tick. Puestas
|
|
231
|
+
* por separado se pintaba el bloque dos veces cada 120 ms, y la segunda pasada
|
|
232
|
+
* borraba y reescribia lo que la primera acababa de dejar: el doble de bytes
|
|
233
|
+
* para el mismo resultado, y un parpadeo que se nota en un terminal lento.
|
|
234
|
+
*/
|
|
235
|
+
export function fijarSecciones(entradas) {
|
|
236
|
+
if (!pieUsable())
|
|
237
|
+
return;
|
|
238
|
+
let cambio = false;
|
|
239
|
+
for (const [nombre, lineas] of entradas) {
|
|
240
|
+
if (anotar(nombre, lineas))
|
|
241
|
+
cambio = true;
|
|
242
|
+
}
|
|
243
|
+
if (cambio)
|
|
244
|
+
repintar("");
|
|
245
|
+
}
|
|
246
|
+
export function quitarSeccion(nombre) {
|
|
247
|
+
fijarSeccion(nombre, []);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Escribe texto permanente sin que el pie se lo lleve por delante.
|
|
251
|
+
*
|
|
252
|
+
* Sin pie es un write normal y no cuesta nada; por eso puede sustituir a
|
|
253
|
+
* `console.log` en el camino del agente sin pensarlo dos veces.
|
|
254
|
+
*/
|
|
255
|
+
export function escribirEncimaDelPie(texto) {
|
|
256
|
+
if (!texto)
|
|
257
|
+
return;
|
|
258
|
+
if (!pieUsable() || (filasPintadas === 0 && !colaSinSalto)) {
|
|
259
|
+
dibujar(texto);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const completo = colaSinSalto + texto;
|
|
263
|
+
const corte = completo.lastIndexOf("\n");
|
|
264
|
+
const permanente = corte >= 0 ? completo.slice(0, corte + 1) : "";
|
|
265
|
+
colaSinSalto = corte >= 0 ? completo.slice(corte + 1) : completo;
|
|
266
|
+
repintar(permanente);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Quita el pie entero y deja el terminal como si nunca hubiera estado.
|
|
270
|
+
*
|
|
271
|
+
* Lo que quede a medias de linea se conserva en pantalla: es texto del agente,
|
|
272
|
+
* no adorno, y borrarlo seria perder salida.
|
|
273
|
+
*/
|
|
274
|
+
export function soltarPie() {
|
|
275
|
+
if (secciones.size === 0 && filasPintadas === 0)
|
|
276
|
+
return;
|
|
277
|
+
secciones.clear();
|
|
278
|
+
repintar("");
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Olvida el estado sin tocar la pantalla.
|
|
282
|
+
*
|
|
283
|
+
* Para las pruebas y para la salida del proceso: ahi no queda terminal al que
|
|
284
|
+
* repintar, pero el cursor escondido si sobrevive y hay que devolverlo.
|
|
285
|
+
*/
|
|
286
|
+
export function reiniciarPie() {
|
|
287
|
+
secciones.clear();
|
|
288
|
+
colaSinSalto = "";
|
|
289
|
+
filasPintadas = 0;
|
|
290
|
+
aMediaLinea = false;
|
|
291
|
+
if (cursorEscondido) {
|
|
292
|
+
dibujar(MOSTRAR_CURSOR);
|
|
293
|
+
cursorEscondido = false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Un cursor escondido sobrevive al proceso: la shell de despues se queda sin
|
|
298
|
+
* cursor y el usuario no tiene forma de saber por que. Se devuelve pase lo que
|
|
299
|
+
* pase, tambien si el programa se cae.
|
|
300
|
+
*/
|
|
301
|
+
process.on("exit", () => {
|
|
302
|
+
if (cursorEscondido) {
|
|
303
|
+
try {
|
|
304
|
+
process.stdout.write(MOSTRAR_CURSOR);
|
|
305
|
+
}
|
|
306
|
+
catch { }
|
|
307
|
+
cursorEscondido = false;
|
|
308
|
+
}
|
|
309
|
+
});
|
package/dist/ui/prompt.d.ts
CHANGED
|
@@ -1,47 +1,10 @@
|
|
|
1
|
+
export { buildPromptFrame } from "./marco.js";
|
|
2
|
+
export type { DropdownItem, PromptFrame, PromptFrameOptions } from "./marco.js";
|
|
3
|
+
export { SLASH_COMMANDS } from "./comandos.js";
|
|
4
|
+
export type { SlashCommandItem } from "./comandos.js";
|
|
1
5
|
/**
|
|
2
6
|
* Imprime sin romper el prompt. Uselo en vez de `console.log` para todo lo que
|
|
3
7
|
* pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
|
|
4
8
|
*/
|
|
5
9
|
export declare function imprimirSobreElPrompt(texto: string): void;
|
|
6
|
-
export interface SlashCommandItem {
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
args?: string;
|
|
10
|
-
}
|
|
11
|
-
export declare const SLASH_COMMANDS: SlashCommandItem[];
|
|
12
|
-
export interface DropdownItem {
|
|
13
|
-
type: "command" | "file";
|
|
14
|
-
label: string;
|
|
15
|
-
secondary?: string;
|
|
16
|
-
insertText: string;
|
|
17
|
-
}
|
|
18
|
-
export interface PromptFrameOptions {
|
|
19
|
-
cols: number;
|
|
20
|
-
/** Alto de la ventana; sirve para no dibujar un marco que no quepa. */
|
|
21
|
-
rows?: number;
|
|
22
|
-
promptSymbol?: string;
|
|
23
|
-
input?: string;
|
|
24
|
-
cursorIndex?: number;
|
|
25
|
-
items?: DropdownItem[];
|
|
26
|
-
selectedIndex?: number;
|
|
27
|
-
}
|
|
28
|
-
export interface PromptFrame {
|
|
29
|
-
/** Lineas LOGICAS. Cualquiera puede ocupar mas de una fila fisica. */
|
|
30
|
-
lines: string[];
|
|
31
|
-
/** Filas de pantalla que ocupara el marco entero con este ancho. */
|
|
32
|
-
totalRows: number;
|
|
33
|
-
/** Fila fisica del cursor, con 0 en la primera fila del marco. */
|
|
34
|
-
cursorRow: number;
|
|
35
|
-
cursorCol: number;
|
|
36
|
-
/** Primer elemento del desplegable que se muestra (ventana deslizante). */
|
|
37
|
-
dropdownOffset: number;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Arma el marco del prompt y dice exactamente donde queda el cursor.
|
|
41
|
-
*
|
|
42
|
-
* Va aparte de render() por dos razones: se puede probar sin terminal, y la
|
|
43
|
-
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
44
|
-
* lugar de estimarse por otro lado y descuadrarse.
|
|
45
|
-
*/
|
|
46
|
-
export declare function buildPromptFrame(opts: PromptFrameOptions): PromptFrame;
|
|
47
10
|
export declare function askInteractivePrompt(promptSymbol?: string, cwd?: string): Promise<string>;
|
package/dist/ui/prompt.js
CHANGED
|
@@ -4,11 +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
|
+
// 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";
|
|
12
22
|
import { esGrande, guardarPegado, expandirPegados, marcaQueTerminaEn, limpiarPegados } from "./pegados.js";
|
|
13
23
|
/**
|
|
14
24
|
* Escribir algo mientras el prompt esta dibujado.
|
|
@@ -40,28 +50,10 @@ export function imprimirSobreElPrompt(texto) {
|
|
|
40
50
|
escribirEncimaDelPrompt(texto);
|
|
41
51
|
return;
|
|
42
52
|
}
|
|
43
|
-
|
|
53
|
+
// Sin prompt abierto puede haber pie: el spinner y la linea de razonamiento
|
|
54
|
+
// viven ahi. Un aviso que llegue tarde tiene que pasar por encima igual.
|
|
55
|
+
escribirEncimaDelPie(texto.endsWith("\n") ? texto : `${texto}\n`);
|
|
44
56
|
}
|
|
45
|
-
export const SLASH_COMMANDS = [
|
|
46
|
-
{ name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
|
|
47
|
-
{ name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
|
|
48
|
-
{ name: "/export", description: "Exportar bitácora de la sesión a archivo Markdown", args: "[nombre.md]" },
|
|
49
|
-
{ name: "/plan", description: "Alternar Modo Plan (solo lectura e investigación previa)" },
|
|
50
|
-
{ name: "/resume", description: "Reanudar una sesión anterior guardada", args: "[id]" },
|
|
51
|
-
{ name: "/skills", description: "Ver skills disponibles y cómo usarlos" },
|
|
52
|
-
{ name: "/memory", description: "Consultar la memoria persistente transversal (~/.chocolatito/memory/)" },
|
|
53
|
-
{ name: "/effort", description: "Regular potencia del motor (1.0, 1.5, 2.0, 2.5)", args: "[nivel]" },
|
|
54
|
-
{ name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
|
|
55
|
-
{ name: "/compact", description: "Comprimir contexto y optimizar memoria de la sesión" },
|
|
56
|
-
{ name: "/diff", description: "Ver diferencias de código (Git diff) del proyecto" },
|
|
57
|
-
{ name: "/commit", description: "Crear un commit en Git con mensaje semántico", args: "<mensaje>" },
|
|
58
|
-
{ name: "/cd", description: "Cambiar el directorio de trabajo activo", args: "<ruta>" },
|
|
59
|
-
{ name: "/cost", description: "Ver consumo detallado de tokens e inversión en USD" },
|
|
60
|
-
{ name: "/clear", description: "Limpiar historial y reiniciar contexto" },
|
|
61
|
-
{ name: "/init", description: "Crear archivo de directivas CHOCOLATITO.md" },
|
|
62
|
-
{ name: "/help", description: "Mostrar guía completa de comandos y ayuda" },
|
|
63
|
-
{ name: "/exit", description: "Cerrar sesión de Chocolatito Code" },
|
|
64
|
-
];
|
|
65
57
|
let isKeypressInitialized = false;
|
|
66
58
|
/**
|
|
67
59
|
* PEGADO DE VARIAS LINEAS
|
|
@@ -148,143 +140,18 @@ function scanProjectFiles(dir, maxResults = 40) {
|
|
|
148
140
|
walk(dir);
|
|
149
141
|
return results;
|
|
150
142
|
}
|
|
151
|
-
const PLACEHOLDER = "Escribe una instrucción, '@' para archivos o '/' para comandos...";
|
|
152
|
-
/**
|
|
153
|
-
* Filas FISICAS que ocupa una linea del marco.
|
|
154
|
-
*
|
|
155
|
-
* Aqui estaba el fallo de verdad: el repintado contaba elementos del array del
|
|
156
|
-
* marco mientras el terminal cuenta filas de pantalla. Cualquier linea que no
|
|
157
|
-
* cupiera en el ancho se partia en dos filas, el cursor subia una fila de menos
|
|
158
|
-
* y el marco entero se iba bajando, dejando una barra huerfana por pulsacion.
|
|
159
|
-
* La entrada ademas puede llevar saltos de linea dentro (continuacion con "\").
|
|
160
|
-
*/
|
|
161
|
-
function physicalRows(line, cols) {
|
|
162
|
-
return stripAnsi(line)
|
|
163
|
-
.split("\n")
|
|
164
|
-
.reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
|
|
165
|
-
}
|
|
166
|
-
/** Recorta a `width` columnas visibles. Texto sin pintar: aqui no hay que cerrar colores. */
|
|
167
|
-
function truncatePlain(text, width) {
|
|
168
|
-
if (width <= 0)
|
|
169
|
-
return "";
|
|
170
|
-
if (text.length <= width)
|
|
171
|
-
return text;
|
|
172
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Arma el marco del prompt y dice exactamente donde queda el cursor.
|
|
176
|
-
*
|
|
177
|
-
* Va aparte de render() por dos razones: se puede probar sin terminal, y la
|
|
178
|
-
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
179
|
-
* lugar de estimarse por otro lado y descuadrarse.
|
|
180
|
-
*/
|
|
181
|
-
export function buildPromptFrame(opts) {
|
|
182
|
-
const cols = Math.max(20, opts.cols || 80);
|
|
183
|
-
const viewportRows = Math.max(6, opts.rows || 24);
|
|
184
|
-
const promptSymbol = opts.promptSymbol ?? "❯";
|
|
185
|
-
const input = opts.input ?? "";
|
|
186
|
-
const cursorIndex = Math.max(0, Math.min(opts.cursorIndex ?? input.length, input.length));
|
|
187
|
-
const items = opts.items ?? [];
|
|
188
|
-
const selectedIndex = items.length > 0 ? (((opts.selectedIndex ?? 0) % items.length) + items.length) % items.length : 0;
|
|
189
|
-
const isPlan = promptSymbol.includes("plan");
|
|
190
|
-
const symbolStr = isPlan
|
|
191
|
-
? `${chalk.cyan.bold("plan ❯")} `
|
|
192
|
-
: `${chalk.bold.hex("#D97757")("❯")} `;
|
|
193
|
-
const symbolWidth = isPlan ? 7 : 2;
|
|
194
|
-
const bar = chalk.gray("─".repeat(cols));
|
|
195
|
-
// 1. Linea de entrada. Lo que el usuario escribe NO se recorta nunca: se deja
|
|
196
|
-
// que el terminal la parta y despues se cuentan las filas que ocupa.
|
|
197
|
-
let formattedInput;
|
|
198
|
-
if (input.length === 0) {
|
|
199
|
-
// La pista si se recorta: en una ventana estrecha ocupaba una segunda fila
|
|
200
|
-
// para no decir nada que el usuario no sepa ya.
|
|
201
|
-
formattedInput = chalk.gray(truncatePlain(PLACEHOLDER, cols - symbolWidth));
|
|
202
|
-
}
|
|
203
|
-
else if (input.startsWith("/")) {
|
|
204
|
-
const parts = input.split(" ");
|
|
205
|
-
const cmdPart = chalk.bold.hex("#A855F7")(parts[0]);
|
|
206
|
-
const restPart = parts.slice(1).join(" ");
|
|
207
|
-
formattedInput = restPart ? `${cmdPart} ${chalk.white(restPart)}` : cmdPart;
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
formattedInput = input.replace(/(@[^\s]+)/g, (m) => chalk.bold.hex("#D97757")(m));
|
|
211
|
-
}
|
|
212
|
-
const inputLine = symbolStr + formattedInput;
|
|
213
|
-
const inputRows = physicalRows(inputLine, cols);
|
|
214
|
-
// 2. Desplegable. Un marco mas alto que la ventana obliga al terminal a
|
|
215
|
-
// desplazar el contenido, y a partir de ahi el repintado ya no sabe donde
|
|
216
|
-
// esta su primera fila: se muestra solo una ventana alrededor del elegido.
|
|
217
|
-
const room = Math.max(1, viewportRows - 1 - (1 + inputRows + 1 + 1));
|
|
218
|
-
const shown = Math.min(items.length, room);
|
|
219
|
-
const dropdownOffset = shown > 0 && items.length > shown
|
|
220
|
-
? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
|
|
221
|
-
: 0;
|
|
222
|
-
const dropdownLines = [];
|
|
223
|
-
for (let i = dropdownOffset; i < dropdownOffset + shown; i++) {
|
|
224
|
-
const item = items[i];
|
|
225
|
-
const isSelected = i === selectedIndex;
|
|
226
|
-
const colorHex = item.type === "file" ? "#D97757" : "#A855F7";
|
|
227
|
-
const pointer = isSelected ? chalk.bold.hex(colorHex)(" ❯ ") : chalk.gray(" ");
|
|
228
|
-
// El puntero se lleva 3 columnas fijas. De lo que queda manda la etiqueta:
|
|
229
|
-
// la descripcion es lo primero que se cae, porque una ruta a medias no se
|
|
230
|
-
// puede ni leer ni elegir.
|
|
231
|
-
const free = cols - 3;
|
|
232
|
-
const label = truncatePlain(item.label, free);
|
|
233
|
-
const roomForDesc = free - label.length - 2;
|
|
234
|
-
const secondary = item.secondary && roomForDesc >= 8 ? truncatePlain(item.secondary, roomForDesc) : "";
|
|
235
|
-
const paintedLabel = isSelected ? chalk.bold.white(label) : chalk.hex(colorHex)(label);
|
|
236
|
-
const paintedDesc = secondary
|
|
237
|
-
? isSelected
|
|
238
|
-
? chalk.whiteBright(` ${secondary}`)
|
|
239
|
-
: chalk.gray(` ${secondary}`)
|
|
240
|
-
: "";
|
|
241
|
-
dropdownLines.push(`${pointer}${paintedLabel}${paintedDesc}`);
|
|
242
|
-
}
|
|
243
|
-
// 3. Pie de estado. El relleno se calculaba con Math.max(1, ...): cuando no
|
|
244
|
-
// cabia, en vez de recortarse se quedaba con un espacio y la linea salia MAS
|
|
245
|
-
// ancha que el terminal. En una consola de 80 columnas el pie media 87 y se
|
|
246
|
-
// partia en dos filas desde el primer dibujado.
|
|
247
|
-
const modeLook = describeMode(getMode());
|
|
248
|
-
const hintPlain = "? /help para atajos · shift+tab modo · esc cancelar";
|
|
249
|
-
const modePlain = `⇥ modo ${modeLook.label}`;
|
|
250
|
-
const enginePlain = "· Espectro 1.0 (Ágil)";
|
|
251
|
-
const hint = chalk.gray(hintPlain);
|
|
252
|
-
const modeTag = chalk.hex(modeLook.color)(modePlain);
|
|
253
|
-
const engine = chalk.gray(enginePlain);
|
|
254
|
-
let footerLine;
|
|
255
|
-
if (hintPlain.length + 1 + modePlain.length + 1 + enginePlain.length <= cols) {
|
|
256
|
-
const padLen = cols - hintPlain.length - modePlain.length - 1 - enginePlain.length;
|
|
257
|
-
footerLine = `${hint}${" ".repeat(padLen)}${modeTag} ${engine}`;
|
|
258
|
-
}
|
|
259
|
-
else if (modePlain.length + 1 + enginePlain.length <= cols) {
|
|
260
|
-
// Los atajos son opcionales; el modo activo no, que es lo que decide si te
|
|
261
|
-
// van a preguntar antes de tocar tus archivos.
|
|
262
|
-
footerLine = `${" ".repeat(cols - modePlain.length - 1 - enginePlain.length)}${modeTag} ${engine}`;
|
|
263
|
-
}
|
|
264
|
-
else if (modePlain.length <= cols) {
|
|
265
|
-
footerLine = `${" ".repeat(cols - modePlain.length)}${modeTag}`;
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, cols));
|
|
269
|
-
}
|
|
270
|
-
const lines = [bar, inputLine, ...dropdownLines, bar, footerLine];
|
|
271
|
-
// 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
|
|
272
|
-
// total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
|
|
273
|
-
// cuanto la entrada se partia el acento se iba a escribir a otra linea.
|
|
274
|
-
const typed = input.length === 0 ? "" : input.slice(0, cursorIndex);
|
|
275
|
-
const segments = typed.split("\n");
|
|
276
|
-
let cursorRow = physicalRows(bar, cols);
|
|
277
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
278
|
-
cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
|
|
279
|
-
}
|
|
280
|
-
const lastSegWidth = (segments.length === 1 ? symbolWidth : 0) + segments[segments.length - 1].length;
|
|
281
|
-
cursorRow += Math.floor(lastSegWidth / cols);
|
|
282
|
-
const cursorCol = lastSegWidth % cols;
|
|
283
|
-
const totalRows = lines.reduce((n, l) => n + physicalRows(l, cols), 0);
|
|
284
|
-
return { lines, totalRows, cursorRow, cursorCol, dropdownOffset };
|
|
285
|
-
}
|
|
286
143
|
export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.cwd()) {
|
|
287
144
|
return new Promise((resolve) => {
|
|
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();
|
|
288
155
|
let inputBuffer = "";
|
|
289
156
|
let cursorIndex = 0;
|
|
290
157
|
let selectedIndex = 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
import { fijarSeccion, quitarSeccion, escribirEncimaDelPie } from "./pieFijo.js";
|
|
3
4
|
/**
|
|
4
5
|
* Lo que se ve mientras el modelo piensa.
|
|
5
6
|
*
|
|
@@ -23,9 +24,17 @@ import process from "node:process";
|
|
|
23
24
|
* que aparecia justo en las tareas largas -las que mas razonan- y nunca al
|
|
24
25
|
* saludar.
|
|
25
26
|
*
|
|
26
|
-
* Ahora se resume: una sola linea que
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* Ahora se resume: una sola linea que dice cuanto lleva pensando y en que anda.
|
|
28
|
+
* Cero avance de scroll, una escritura por trozo recibido en vez de una por
|
|
29
|
+
* letra.
|
|
30
|
+
*
|
|
31
|
+
* DONDE VIVE ESA LINEA
|
|
32
|
+
*
|
|
33
|
+
* En el pie fijo, no en la fila actual. Antes se reescribia sobre si misma con
|
|
34
|
+
* `\r\x1b[2K`, que la ata a la fila donde nacio: al empezar a llegar la
|
|
35
|
+
* respuesta, el terminal desplaza y se la lleva hacia arriba. Es la "nubecita
|
|
36
|
+
* que sube con lo que me respondes". Entregandola al pie, se repinta abajo tras
|
|
37
|
+
* cada linea de respuesta y se queda quieta.
|
|
29
38
|
*
|
|
30
39
|
* Quien quiera verlo entero: CHOCOLATITO_RAZONAMIENTO=completo.
|
|
31
40
|
*/
|
|
@@ -56,7 +65,7 @@ export class ReasoningStream {
|
|
|
56
65
|
this.caracteres = 0;
|
|
57
66
|
this.cola = "";
|
|
58
67
|
if (COMPLETO) {
|
|
59
|
-
|
|
68
|
+
escribirEncimaDelPie(`\n ${chalk.bold.hex("#8B5CF6")("💭 Razonando:")}\n`);
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
if (COMPLETO) {
|
|
@@ -87,22 +96,20 @@ export class ReasoningStream {
|
|
|
87
96
|
}
|
|
88
97
|
// Un solo write, y el color se aplica una vez al bloque entero en vez de a
|
|
89
98
|
// cada letra: mismo resultado en pantalla, una fraccion de los bytes.
|
|
90
|
-
|
|
99
|
+
escribirEncimaDelPie(chalk.gray(salida));
|
|
91
100
|
}
|
|
92
101
|
refrescar(forzar) {
|
|
93
102
|
const ahora = Date.now();
|
|
94
103
|
if (!forzar && ahora - this.ultimoRefresco < MS_ENTRE_REFRESCOS)
|
|
95
104
|
return;
|
|
96
105
|
this.ultimoRefresco = ahora;
|
|
97
|
-
if (!process.stdout.isTTY)
|
|
98
|
-
return;
|
|
99
106
|
const segundos = Math.round((ahora - this.inicio) / 1000);
|
|
100
107
|
const ancho = Math.max(20, (process.stdout.columns || 80) - 4);
|
|
101
108
|
const cabeza = ` ${chalk.hex("#8B5CF6")("💭")} ${chalk.gray(`pensando… ${segundos}s`)}`;
|
|
102
109
|
// La ultima frase a medias da una pista de por donde va sin contarlo todo.
|
|
103
110
|
const pista = unaLinea(this.cola, Math.max(0, ancho - 24));
|
|
104
111
|
const linea = pista ? `${cabeza} ${chalk.dim("·")} ${chalk.dim(pista)}` : cabeza;
|
|
105
|
-
|
|
112
|
+
fijarSeccion("razonamiento", [linea]);
|
|
106
113
|
this.lineaPintada = true;
|
|
107
114
|
}
|
|
108
115
|
flush() {
|
|
@@ -110,17 +117,20 @@ export class ReasoningStream {
|
|
|
110
117
|
return;
|
|
111
118
|
if (COMPLETO) {
|
|
112
119
|
if (!this.atLineStart) {
|
|
113
|
-
|
|
120
|
+
escribirEncimaDelPie("\n");
|
|
114
121
|
this.atLineStart = true;
|
|
115
122
|
}
|
|
116
|
-
|
|
123
|
+
escribirEncimaDelPie("\n");
|
|
117
124
|
this.hasStarted = false;
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
127
|
const segundos = Math.max(1, Math.round((Date.now() - this.inicio) / 1000));
|
|
128
|
+
// Primero se quita la linea viva del pie y despues se escribe el resumen
|
|
129
|
+
// como texto permanente. Al reves, el resumen quedaria por encima de una
|
|
130
|
+
// linea de "pensando…" que ya no viene a cuento.
|
|
131
|
+
quitarSeccion("razonamiento");
|
|
121
132
|
if (this.lineaPintada) {
|
|
122
|
-
|
|
123
|
-
process.stdout.write(`\r\x1b[2K ${chalk.hex("#8B5CF6")("💭")} ${chalk.gray(`pensó ${segundos}s`)}\n`);
|
|
133
|
+
escribirEncimaDelPie(` ${chalk.hex("#8B5CF6")("💭")} ${chalk.gray(`pensó ${segundos}s`)}\n`);
|
|
124
134
|
}
|
|
125
135
|
this.lineaPintada = false;
|
|
126
136
|
this.hasStarted = false;
|
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;
|