chocolatito-code 1.3.0 → 1.3.2
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 +76 -38
- 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 +193 -0
- package/dist/ui/modes.d.ts +9 -1
- package/dist/ui/modes.js +11 -3
- package/dist/ui/motorActual.d.ts +28 -0
- package/dist/ui/motorActual.js +34 -0
- package/dist/ui/prompt.d.ts +4 -41
- package/dist/ui/prompt.js +42 -165
- package/dist/ui/renderer.d.ts +13 -0
- package/dist/ui/renderer.js +41 -12
- package/dist/ui/selector.d.ts +37 -0
- package/dist/ui/selector.js +81 -0
- package/dist/ui/spinner.d.ts +33 -5
- package/dist/ui/spinner.js +70 -44
- package/extension/manifest.json +60 -60
- package/package.json +67 -67
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface DropdownItem {
|
|
2
|
+
type: "command" | "file";
|
|
3
|
+
label: string;
|
|
4
|
+
secondary?: string;
|
|
5
|
+
insertText: string;
|
|
6
|
+
}
|
|
7
|
+
export interface PromptFrameOptions {
|
|
8
|
+
cols: number;
|
|
9
|
+
/** Alto de la ventana; sirve para no dibujar un marco que no quepa. */
|
|
10
|
+
rows?: number;
|
|
11
|
+
promptSymbol?: string;
|
|
12
|
+
input?: string;
|
|
13
|
+
cursorIndex?: number;
|
|
14
|
+
items?: DropdownItem[];
|
|
15
|
+
selectedIndex?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface PromptFrame {
|
|
18
|
+
/** Lineas LOGICAS. Cualquiera puede ocupar mas de una fila fisica. */
|
|
19
|
+
lines: string[];
|
|
20
|
+
/** Filas de pantalla que ocupara el marco entero con este ancho. */
|
|
21
|
+
totalRows: number;
|
|
22
|
+
/** Fila fisica del cursor, con 0 en la primera fila del marco. */
|
|
23
|
+
cursorRow: number;
|
|
24
|
+
cursorCol: number;
|
|
25
|
+
/** Primer elemento del desplegable que se muestra (ventana deslizante). */
|
|
26
|
+
dropdownOffset: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Arma el marco del prompt y dice exactamente donde queda el cursor.
|
|
30
|
+
*
|
|
31
|
+
* Va aparte de render() por dos razones: se puede probar sin terminal, y la
|
|
32
|
+
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
33
|
+
* lugar de estimarse por otro lado y descuadrarse.
|
|
34
|
+
*/
|
|
35
|
+
export declare function buildPromptFrame(opts: PromptFrameOptions): PromptFrame;
|
package/dist/ui/marco.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { describeMode, getMode, TRIANGULOS } from "./modes.js";
|
|
3
|
+
import { motorActual } from "./motorActual.js";
|
|
4
|
+
/**
|
|
5
|
+
* EL MARCO DE LA BARRA, EN UN SOLO SITIO
|
|
6
|
+
*
|
|
7
|
+
* Vivia dentro de prompt.ts. Ahora lo necesitan dos: el prompt cuando espera una
|
|
8
|
+
* orden, y el spinner mientras el agente trabaja.
|
|
9
|
+
*
|
|
10
|
+
* POR QUE COMPARTIDO Y NO COPIADO
|
|
11
|
+
*
|
|
12
|
+
* Habia DOS barras distintas turnandose. Al esperar salia el marco entero
|
|
13
|
+
* -barra, entrada, barra, linea de ayuda-; al trabajar, una version recortada
|
|
14
|
+
* con otro texto. Se parecian lo justo para que el cambio de una a otra se viera
|
|
15
|
+
* como un salto, y el usuario lo dijo asi: "aun no es fija... debe ser fija y
|
|
16
|
+
* por debajo deben pasar los otros mensajes".
|
|
17
|
+
*
|
|
18
|
+
* Con el constructor compartido no pueden divergir: si alguien toca el marco, lo
|
|
19
|
+
* toca en los dos estados a la vez. Copiarlo habria durado hasta el primer
|
|
20
|
+
* retoque.
|
|
21
|
+
*
|
|
22
|
+
* Va aparte tambien por peso: prompt.ts arrastra readline, el historial en disco
|
|
23
|
+
* y el escaneo del proyecto, y el spinner no tiene por que cargar nada de eso.
|
|
24
|
+
*/
|
|
25
|
+
function stripAnsi(str) {
|
|
26
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
27
|
+
}
|
|
28
|
+
const PLACEHOLDER = "Escribe una instrucción, '@' para archivos o '/' para comandos...";
|
|
29
|
+
/**
|
|
30
|
+
* Filas FISICAS que ocupa una linea del marco.
|
|
31
|
+
*
|
|
32
|
+
* Aqui estaba el fallo de verdad: el repintado contaba elementos del array del
|
|
33
|
+
* marco mientras el terminal cuenta filas de pantalla. Cualquier linea que no
|
|
34
|
+
* cupiera en el ancho se partia en dos filas, el cursor subia una fila de menos
|
|
35
|
+
* y el marco entero se iba bajando, dejando una barra huerfana por pulsacion.
|
|
36
|
+
* La entrada ademas puede llevar saltos de linea dentro (continuacion con "\").
|
|
37
|
+
*/
|
|
38
|
+
function physicalRows(line, cols) {
|
|
39
|
+
return stripAnsi(line)
|
|
40
|
+
.split("\n")
|
|
41
|
+
.reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
|
|
42
|
+
}
|
|
43
|
+
/** Recorta a `width` columnas visibles. Texto sin pintar: aqui no hay que cerrar colores. */
|
|
44
|
+
function truncatePlain(text, width) {
|
|
45
|
+
if (width <= 0)
|
|
46
|
+
return "";
|
|
47
|
+
if (text.length <= width)
|
|
48
|
+
return text;
|
|
49
|
+
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Arma el marco del prompt y dice exactamente donde queda el cursor.
|
|
53
|
+
*
|
|
54
|
+
* Va aparte de render() por dos razones: se puede probar sin terminal, y la
|
|
55
|
+
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
56
|
+
* lugar de estimarse por otro lado y descuadrarse.
|
|
57
|
+
*/
|
|
58
|
+
export function buildPromptFrame(opts) {
|
|
59
|
+
const cols = Math.max(20, opts.cols || 80);
|
|
60
|
+
const viewportRows = Math.max(6, opts.rows || 24);
|
|
61
|
+
const promptSymbol = opts.promptSymbol ?? "❯";
|
|
62
|
+
const input = opts.input ?? "";
|
|
63
|
+
const cursorIndex = Math.max(0, Math.min(opts.cursorIndex ?? input.length, input.length));
|
|
64
|
+
const items = opts.items ?? [];
|
|
65
|
+
const selectedIndex = items.length > 0 ? (((opts.selectedIndex ?? 0) % items.length) + items.length) % items.length : 0;
|
|
66
|
+
const isPlan = promptSymbol.includes("plan");
|
|
67
|
+
const symbolStr = isPlan
|
|
68
|
+
? `${chalk.cyan.bold("plan ❯")} `
|
|
69
|
+
: `${chalk.bold.hex("#D97757")("❯")} `;
|
|
70
|
+
const symbolWidth = isPlan ? 7 : 2;
|
|
71
|
+
const bar = chalk.gray("─".repeat(cols));
|
|
72
|
+
// 1. Linea de entrada. Lo que el usuario escribe NO se recorta nunca: se deja
|
|
73
|
+
// que el terminal la parta y despues se cuentan las filas que ocupa.
|
|
74
|
+
let formattedInput;
|
|
75
|
+
if (input.length === 0) {
|
|
76
|
+
// La pista si se recorta: en una ventana estrecha ocupaba una segunda fila
|
|
77
|
+
// para no decir nada que el usuario no sepa ya.
|
|
78
|
+
formattedInput = chalk.gray(truncatePlain(PLACEHOLDER, cols - symbolWidth));
|
|
79
|
+
}
|
|
80
|
+
else if (input.startsWith("/")) {
|
|
81
|
+
const parts = input.split(" ");
|
|
82
|
+
const cmdPart = chalk.bold.hex("#A855F7")(parts[0]);
|
|
83
|
+
const restPart = parts.slice(1).join(" ");
|
|
84
|
+
formattedInput = restPart ? `${cmdPart} ${chalk.white(restPart)}` : cmdPart;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
formattedInput = input.replace(/(@[^\s]+)/g, (m) => chalk.bold.hex("#D97757")(m));
|
|
88
|
+
}
|
|
89
|
+
const inputLine = symbolStr + formattedInput;
|
|
90
|
+
const inputRows = physicalRows(inputLine, cols);
|
|
91
|
+
// 2. Desplegable. Un marco mas alto que la ventana obliga al terminal a
|
|
92
|
+
// desplazar el contenido, y a partir de ahi el repintado ya no sabe donde
|
|
93
|
+
// esta su primera fila: se muestra solo una ventana alrededor del elegido.
|
|
94
|
+
const room = Math.max(1, viewportRows - 1 - (1 + inputRows + 1 + 1));
|
|
95
|
+
const shown = Math.min(items.length, room);
|
|
96
|
+
const dropdownOffset = shown > 0 && items.length > shown
|
|
97
|
+
? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
|
|
98
|
+
: 0;
|
|
99
|
+
const dropdownLines = [];
|
|
100
|
+
for (let i = dropdownOffset; i < dropdownOffset + shown; i++) {
|
|
101
|
+
const item = items[i];
|
|
102
|
+
const isSelected = i === selectedIndex;
|
|
103
|
+
const colorHex = item.type === "file" ? "#D97757" : "#A855F7";
|
|
104
|
+
const pointer = isSelected ? chalk.bold.hex(colorHex)(" ❯ ") : chalk.gray(" ");
|
|
105
|
+
// El puntero se lleva 3 columnas fijas. De lo que queda manda la etiqueta:
|
|
106
|
+
// la descripcion es lo primero que se cae, porque una ruta a medias no se
|
|
107
|
+
// puede ni leer ni elegir.
|
|
108
|
+
const free = cols - 3;
|
|
109
|
+
const label = truncatePlain(item.label, free);
|
|
110
|
+
const roomForDesc = free - label.length - 2;
|
|
111
|
+
const secondary = item.secondary && roomForDesc >= 8 ? truncatePlain(item.secondary, roomForDesc) : "";
|
|
112
|
+
const paintedLabel = isSelected ? chalk.bold.white(label) : chalk.hex(colorHex)(label);
|
|
113
|
+
const paintedDesc = secondary
|
|
114
|
+
? isSelected
|
|
115
|
+
? chalk.whiteBright(` ${secondary}`)
|
|
116
|
+
: chalk.gray(` ${secondary}`)
|
|
117
|
+
: "";
|
|
118
|
+
dropdownLines.push(`${pointer}${paintedLabel}${paintedDesc}`);
|
|
119
|
+
}
|
|
120
|
+
// 3. Pie de estado. El relleno se calculaba con Math.max(1, ...): cuando no
|
|
121
|
+
// cabia, en vez de recortarse se quedaba con un espacio y la linea salia MAS
|
|
122
|
+
// ancha que el terminal. En una consola de 80 columnas el pie media 87 y se
|
|
123
|
+
// partia en dos filas desde el primer dibujado.
|
|
124
|
+
const modeLook = describeMode(getMode());
|
|
125
|
+
// 3. La linea de estado, con POSICIONES FIJAS.
|
|
126
|
+
//
|
|
127
|
+
// Antes iban los atajos a la izquierda y el modo y el motor apelotonados a la
|
|
128
|
+
// derecha, y encima la linea se reordenaba sola en cuatro variantes segun el
|
|
129
|
+
// ancho: al estrecharse la ventana, el modo saltaba de sitio. El usuario lo
|
|
130
|
+
// pidio asi: "el boton del modo debe ir a la izquierda... y el modelo a la
|
|
131
|
+
// derecha, pero deben ser fijos siempre y nunca cambiar".
|
|
132
|
+
//
|
|
133
|
+
// Ahora el modo esta SIEMPRE pegado a la izquierda y el motor SIEMPRE pegado a
|
|
134
|
+
// la derecha. Lo unico que aparece y desaparece es la pista del medio, que es
|
|
135
|
+
// lo prescindible: quien ya se sabe los atajos no la necesita, y quien no, la
|
|
136
|
+
// tiene en /help.
|
|
137
|
+
const modePlain = `${TRIANGULOS} modo ${modeLook.label}`;
|
|
138
|
+
// Estaba escrito a mano y por eso no cambiaba nunca. Vacio si nadie lo ha
|
|
139
|
+
// fijado: mejor un hueco que un nombre que no es el que se esta usando.
|
|
140
|
+
const enginePlain = motorActual();
|
|
141
|
+
const hintPlain = "shift+tab cambia · esc cancela · ? /help";
|
|
142
|
+
const modeTag = chalk.hex(modeLook.color)(modePlain);
|
|
143
|
+
const engine = chalk.gray(enginePlain);
|
|
144
|
+
// Dos columnas de respiro. Los triangulos son un glifo de anchura ambigua y
|
|
145
|
+
// hay terminales que lo pintan doble: si la linea llegara justa al ancho, el
|
|
146
|
+
// terminal la partiria en dos filas y el repintado del marco se descuadraria
|
|
147
|
+
// entero. Ese fallo ya costo una barra huerfana por pulsacion.
|
|
148
|
+
const util = Math.max(0, cols - 2);
|
|
149
|
+
let footerLine;
|
|
150
|
+
const huecoConPista = util - modePlain.length - 1 - hintPlain.length - enginePlain.length;
|
|
151
|
+
const hueco = util - modePlain.length - enginePlain.length;
|
|
152
|
+
if (!enginePlain) {
|
|
153
|
+
// Todavia nadie ha dicho que motor hay. Se enseña el modo y ya, pero SIN
|
|
154
|
+
// saltarse el recorte: "⏵⏵ modo acepta ediciones" son 24 columnas y en una
|
|
155
|
+
// consola de 20 se partiria en dos filas, que es lo que descuadra el marco.
|
|
156
|
+
const conPista = util - modePlain.length - 1 - hintPlain.length;
|
|
157
|
+
if (modePlain.length > util) {
|
|
158
|
+
footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, util));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
footerLine = conPista >= 0 ? `${modeTag} ${chalk.gray(hintPlain)}` : modeTag;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else if (huecoConPista >= 1) {
|
|
165
|
+
footerLine = `${modeTag} ${chalk.gray(hintPlain)}${" ".repeat(huecoConPista)}${engine}`;
|
|
166
|
+
}
|
|
167
|
+
else if (hueco >= 1) {
|
|
168
|
+
footerLine = `${modeTag}${" ".repeat(hueco)}${engine}`;
|
|
169
|
+
}
|
|
170
|
+
else if (modePlain.length <= util) {
|
|
171
|
+
// Si hay que sacrificar uno, se va el motor: el modo es lo que decide si te
|
|
172
|
+
// van a preguntar antes de tocar tus archivos.
|
|
173
|
+
footerLine = modeTag;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, util));
|
|
177
|
+
}
|
|
178
|
+
const lines = [bar, inputLine, ...dropdownLines, bar, footerLine];
|
|
179
|
+
// 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
|
|
180
|
+
// total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
|
|
181
|
+
// cuanto la entrada se partia el acento se iba a escribir a otra linea.
|
|
182
|
+
const typed = input.length === 0 ? "" : input.slice(0, cursorIndex);
|
|
183
|
+
const segments = typed.split("\n");
|
|
184
|
+
let cursorRow = physicalRows(bar, cols);
|
|
185
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
186
|
+
cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
|
|
187
|
+
}
|
|
188
|
+
const lastSegWidth = (segments.length === 1 ? symbolWidth : 0) + segments[segments.length - 1].length;
|
|
189
|
+
cursorRow += Math.floor(lastSegWidth / cols);
|
|
190
|
+
const cursorCol = lastSegWidth % cols;
|
|
191
|
+
const totalRows = lines.reduce((n, l) => n + physicalRows(l, cols), 0);
|
|
192
|
+
return { lines, totalRows, cursorRow, cursorCol, dropdownOffset };
|
|
193
|
+
}
|
package/dist/ui/modes.d.ts
CHANGED
|
@@ -22,7 +22,15 @@ export interface ModeLook {
|
|
|
22
22
|
color: string;
|
|
23
23
|
}
|
|
24
24
|
export declare function describeMode(mode?: InteractiveMode): ModeLook;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* El simbolo del modo.
|
|
27
|
+
*
|
|
28
|
+
* Dos triangulos, no la flecha de tabulacion. El "⇥" de antes es el simbolo del
|
|
29
|
+
* tabulador: dice COMO se cambia de modo, no QUE modo hay puesto. Y como ademas
|
|
30
|
+
* el atajo es shift+tab, se leia como si el modo fuera un atajo mas.
|
|
31
|
+
*/
|
|
32
|
+
export declare const TRIANGULOS = "\u23F5\u23F5";
|
|
33
|
+
/** Etiqueta de una linea del modo activo. */
|
|
26
34
|
export declare function modeBadge(mode?: InteractiveMode): string;
|
|
27
35
|
/** Aviso completo, para cuando el usuario acaba de cambiar de modo. */
|
|
28
36
|
export declare function modeAnnouncement(mode?: InteractiveMode): string;
|
package/dist/ui/modes.js
CHANGED
|
@@ -43,15 +43,23 @@ const LOOKS = {
|
|
|
43
43
|
export function describeMode(mode = current) {
|
|
44
44
|
return LOOKS[mode];
|
|
45
45
|
}
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* El simbolo del modo.
|
|
48
|
+
*
|
|
49
|
+
* Dos triangulos, no la flecha de tabulacion. El "⇥" de antes es el simbolo del
|
|
50
|
+
* tabulador: dice COMO se cambia de modo, no QUE modo hay puesto. Y como ademas
|
|
51
|
+
* el atajo es shift+tab, se leia como si el modo fuera un atajo mas.
|
|
52
|
+
*/
|
|
53
|
+
export const TRIANGULOS = "⏵⏵";
|
|
54
|
+
/** Etiqueta de una linea del modo activo. */
|
|
47
55
|
export function modeBadge(mode = current) {
|
|
48
56
|
if (mode === "default")
|
|
49
57
|
return "";
|
|
50
58
|
const look = LOOKS[mode];
|
|
51
|
-
return chalk.hex(look.color)(
|
|
59
|
+
return chalk.hex(look.color)(`${TRIANGULOS} modo ${look.label}`);
|
|
52
60
|
}
|
|
53
61
|
/** Aviso completo, para cuando el usuario acaba de cambiar de modo. */
|
|
54
62
|
export function modeAnnouncement(mode = current) {
|
|
55
63
|
const look = LOOKS[mode];
|
|
56
|
-
return `${chalk.bold.hex(look.color)(
|
|
64
|
+
return `${chalk.bold.hex(look.color)(`${TRIANGULOS} modo ${look.label}`)} ${chalk.gray(`· ${look.hint}`)} ${chalk.gray("· shift+tab para cambiar")}`;
|
|
57
65
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUE MOTOR ESTA PUESTO
|
|
3
|
+
*
|
|
4
|
+
* La linea de estado del marco lo llevaba escrito a mano:
|
|
5
|
+
*
|
|
6
|
+
* const enginePlain = "Espectro 1.0 (Ágil)";
|
|
7
|
+
*
|
|
8
|
+
* Asi que decia siempre lo mismo pasara lo que pasara. Cambiabas de motor con
|
|
9
|
+
* /effort, el agente empezaba a usar otro de verdad, y la barra seguia
|
|
10
|
+
* anunciando el primero. Lo vio el usuario: "al poner el modelo, la parte de
|
|
11
|
+
* abajo que es fija no se actualiza con el modelo que se pone".
|
|
12
|
+
*
|
|
13
|
+
* Un indicador que miente es peor que no tener indicador: te hace tomar
|
|
14
|
+
* decisiones -esperar mas, gastar mas- creyendo que estas en otro sitio.
|
|
15
|
+
*
|
|
16
|
+
* Vive aparte y no en el marco por lo mismo que el modo (ver modes.ts): lo
|
|
17
|
+
* escribe quien cambia de motor y lo lee quien pinta, y ninguno de los dos tiene
|
|
18
|
+
* por que conocer al otro.
|
|
19
|
+
*/
|
|
20
|
+
/** Lo pone index.ts al arrancar y cada vez que se cambia de motor. */
|
|
21
|
+
export declare function fijarMotor(nuevo: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* El nombre del motor activo.
|
|
24
|
+
*
|
|
25
|
+
* Vacio hasta que alguien lo fije. Quien pinta decide que hacer con eso: mejor
|
|
26
|
+
* no enseñar nada que enseñar un nombre inventado.
|
|
27
|
+
*/
|
|
28
|
+
export declare function motorActual(): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUE MOTOR ESTA PUESTO
|
|
3
|
+
*
|
|
4
|
+
* La linea de estado del marco lo llevaba escrito a mano:
|
|
5
|
+
*
|
|
6
|
+
* const enginePlain = "Espectro 1.0 (Ágil)";
|
|
7
|
+
*
|
|
8
|
+
* Asi que decia siempre lo mismo pasara lo que pasara. Cambiabas de motor con
|
|
9
|
+
* /effort, el agente empezaba a usar otro de verdad, y la barra seguia
|
|
10
|
+
* anunciando el primero. Lo vio el usuario: "al poner el modelo, la parte de
|
|
11
|
+
* abajo que es fija no se actualiza con el modelo que se pone".
|
|
12
|
+
*
|
|
13
|
+
* Un indicador que miente es peor que no tener indicador: te hace tomar
|
|
14
|
+
* decisiones -esperar mas, gastar mas- creyendo que estas en otro sitio.
|
|
15
|
+
*
|
|
16
|
+
* Vive aparte y no en el marco por lo mismo que el modo (ver modes.ts): lo
|
|
17
|
+
* escribe quien cambia de motor y lo lee quien pinta, y ninguno de los dos tiene
|
|
18
|
+
* por que conocer al otro.
|
|
19
|
+
*/
|
|
20
|
+
let nombre = "";
|
|
21
|
+
/** Lo pone index.ts al arrancar y cada vez que se cambia de motor. */
|
|
22
|
+
export function fijarMotor(nuevo) {
|
|
23
|
+
if (nuevo && nuevo.trim())
|
|
24
|
+
nombre = nuevo.trim();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* El nombre del motor activo.
|
|
28
|
+
*
|
|
29
|
+
* Vacio hasta que alguien lo fije. Quien pinta decide que hacer con eso: mejor
|
|
30
|
+
* no enseñar nada que enseñar un nombre inventado.
|
|
31
|
+
*/
|
|
32
|
+
export function motorActual() {
|
|
33
|
+
return nombre;
|
|
34
|
+
}
|
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,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;
|
|
@@ -603,10 +462,28 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
603
462
|
render();
|
|
604
463
|
return;
|
|
605
464
|
}
|
|
606
|
-
|
|
607
|
-
|
|
465
|
+
// EL DESPLEGABLE MANDA SOBRE LO TECLEADO
|
|
466
|
+
//
|
|
467
|
+
// Antes esto solo entraba con el buffer EXACTAMENTE en "/". Si habias
|
|
468
|
+
// escrito "/e" y el desplegable te estaba señalando "/effort", el Enter
|
|
469
|
+
// enviaba "/e" tal cual: un comando que no existe, mientras en pantalla
|
|
470
|
+
// ponia otra cosa. Lo conto el usuario asi: "me recomienda effort pero
|
|
471
|
+
// al dar enter se manda como /e".
|
|
472
|
+
const mencionActiva = getAtMention().active;
|
|
473
|
+
const escribiendoComando = inputBuffer.startsWith("/") && !inputBuffer.includes(" ");
|
|
474
|
+
const soloLaBarra = inputBuffer === "/";
|
|
475
|
+
if (items.length > 0 && (escribiendoComando || mencionActiva)) {
|
|
476
|
+
const elegido = items[selectedIndex];
|
|
477
|
+
const yaEstaEntero = Boolean(elegido) && inputBuffer === elegido.insertText;
|
|
478
|
+
if (!yaEstaEntero && applySelectedDropdownItem()) {
|
|
608
479
|
render();
|
|
609
|
-
|
|
480
|
+
// Se completa Y SE ENVIA, que es lo que uno espera al ver el comando
|
|
481
|
+
// señalado y pulsar Enter. Dos excepciones que solo completan:
|
|
482
|
+
// - una mencion de archivo casi siempre lleva mas texto detras;
|
|
483
|
+
// - con solo "/" escrito no has elegido nada todavia, y enviar ahi
|
|
484
|
+
// ejecutaria el primero de la lista sin querer.
|
|
485
|
+
if (mencionActiva || soloLaBarra)
|
|
486
|
+
return;
|
|
610
487
|
}
|
|
611
488
|
}
|
|
612
489
|
scheduleSubmit();
|
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;
|