chocolatito-code 1.0.0 → 1.1.0
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/README.md +89 -0
- package/dist/agent/compaction.d.ts +65 -0
- package/dist/agent/compaction.js +134 -0
- package/dist/agent/context.d.ts +3 -0
- package/dist/agent/context.js +35 -0
- package/dist/agent/loop.d.ts +91 -2
- package/dist/agent/loop.js +536 -78
- package/dist/agent/loopDetector.d.ts +57 -0
- package/dist/agent/loopDetector.js +0 -0
- package/dist/agent/mentions.d.ts +13 -0
- package/dist/agent/mentions.js +49 -0
- package/dist/agent/repoMap.d.ts +117 -0
- package/dist/agent/repoMap.js +560 -0
- package/dist/agent/result.d.ts +64 -0
- package/dist/agent/result.js +48 -0
- package/dist/agent/retry.d.ts +35 -0
- package/dist/agent/retry.js +123 -4
- package/dist/agent/subagent.js +62 -15
- package/dist/agent/syntaxValidator.js +288 -52
- package/dist/agent/toolGate.d.ts +58 -0
- package/dist/agent/toolGate.js +111 -0
- package/dist/agent/tracker.js +4 -7
- package/dist/agent/undoManager.d.ts +44 -11
- package/dist/agent/undoManager.js +95 -24
- package/dist/agent/usageMeter.d.ts +56 -0
- package/dist/agent/usageMeter.js +51 -0
- package/dist/agent/verifier.d.ts +49 -0
- package/dist/agent/verifier.js +158 -0
- package/dist/config/constants.js +14 -0
- package/dist/config/engine.d.ts +19 -0
- package/dist/config/engine.js +35 -3
- package/dist/config/license.d.ts +5 -0
- package/dist/config/license.js +107 -9
- package/dist/config/limits.d.ts +28 -0
- package/dist/config/limits.js +51 -0
- package/dist/config/permissions.d.ts +76 -1
- package/dist/config/permissions.js +379 -23
- package/dist/config/quota.js +24 -1
- package/dist/config/updater.d.ts +68 -0
- package/dist/config/updater.js +215 -0
- package/dist/hooks/manager.js +159 -14
- package/dist/index.js +222 -34
- package/dist/mcp/client.d.ts +22 -0
- package/dist/mcp/client.js +93 -11
- package/dist/prompts/systemPrompt.js +12 -1
- package/dist/sessions/manager.d.ts +12 -0
- package/dist/sessions/manager.js +33 -5
- package/dist/sessions/resume.d.ts +36 -0
- package/dist/sessions/resume.js +43 -0
- package/dist/skills/manager.d.ts +1 -0
- package/dist/skills/manager.js +9 -0
- package/dist/tools/binary.d.ts +46 -0
- package/dist/tools/binary.js +100 -0
- package/dist/tools/browserCdp.js +13 -1
- package/dist/tools/browserExtension.d.ts +5 -0
- package/dist/tools/browserExtension.js +202 -18
- package/dist/tools/browserSession.d.ts +23 -0
- package/dist/tools/browserSession.js +28 -0
- package/dist/tools/computerUse.js +33 -1
- package/dist/tools/createImage.d.ts +11 -0
- package/dist/tools/createImage.js +13 -1
- package/dist/tools/definitions.js +9 -3
- package/dist/tools/editDiagnosis.d.ts +54 -0
- package/dist/tools/editDiagnosis.js +142 -0
- package/dist/tools/editFile.d.ts +1 -0
- package/dist/tools/editFile.js +118 -62
- package/dist/tools/findFiles.d.ts +21 -0
- package/dist/tools/findFiles.js +75 -6
- package/dist/tools/getSystemInfo.js +15 -7
- package/dist/tools/gitTools.d.ts +24 -0
- package/dist/tools/gitTools.js +87 -26
- package/dist/tools/moveCopyFile.d.ts +18 -1
- package/dist/tools/moveCopyFile.js +74 -4
- package/dist/tools/patchCascade.d.ts +57 -0
- package/dist/tools/patchCascade.js +338 -0
- package/dist/tools/runCommand.js +213 -25
- package/dist/tools/runner.js +57 -4
- package/dist/tools/toolDefsComputer.js +12 -2
- package/dist/tools/truncator.d.ts +27 -1
- package/dist/tools/truncator.js +117 -11
- package/dist/tools/viewFile.js +15 -11
- package/dist/tools/visionBridge.js +7 -0
- package/dist/tools/webSearch.d.ts +21 -0
- package/dist/tools/webSearch.js +125 -14
- package/dist/tools/writeFile.js +18 -1
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +56 -5
- package/dist/ui/keyboardGuard.d.ts +55 -0
- package/dist/ui/keyboardGuard.js +65 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +35 -0
- package/dist/ui/prompt.js +345 -125
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/extension/README.md +95 -74
- package/extension/background.js +598 -178
- package/extension/content.js +92 -9
- package/extension/manifest.json +2 -1
- package/extension/popup.js +28 -12
- package/package.json +1 -1
package/dist/ui/prompt.js
CHANGED
|
@@ -5,7 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { cycleMode, describeMode, getMode } from "./modes.js";
|
|
8
|
-
import { releaseKeyboard } from "./interrupt.js";
|
|
8
|
+
import { releaseKeyboard, tomarTecladoParaPrompt, devolverTecladoDelPrompt } from "./interrupt.js";
|
|
9
|
+
import { repararTeclado, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
|
|
9
10
|
import { clearTyped } from "./typeAhead.js";
|
|
10
11
|
export const SLASH_COMMANDS = [
|
|
11
12
|
{ name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
|
|
@@ -28,6 +29,33 @@ export const SLASH_COMMANDS = [
|
|
|
28
29
|
{ name: "/exit", description: "Cerrar sesión de Chocolatito Code" },
|
|
29
30
|
];
|
|
30
31
|
let isKeypressInitialized = false;
|
|
32
|
+
/**
|
|
33
|
+
* PEGADO DE VARIAS LINEAS
|
|
34
|
+
*
|
|
35
|
+
* Un pegado no llega tecla a tecla: llega como UN chunk de stdin que readline
|
|
36
|
+
* desmonta en pulsaciones y emite todas en el mismo tick sincrono. El primer
|
|
37
|
+
* "\r" del texto se emitia como key.name === "return", el manejador de Enter
|
|
38
|
+
* hacia cleanup() + resolve() y quitaba el oyente MIENTRAS el resto del chunk
|
|
39
|
+
* se seguia emitiendo: las lineas 2..n salian sin nadie escuchando y se perdian
|
|
40
|
+
* sin dejar rastro ni en el buffer ni en la cola. El usuario creia haber mandado
|
|
41
|
+
* la instruccion entera y el agente arrancaba con la primera linea.
|
|
42
|
+
*
|
|
43
|
+
* Dos defensas, porque ninguna cubre sola todos los terminales:
|
|
44
|
+
*
|
|
45
|
+
* 1. Modo de pegado entre corchetes. El terminal envuelve lo pegado en
|
|
46
|
+
* \x1b[200~ ... \x1b[201~, asi que se distingue de lo tecleado aunque el
|
|
47
|
+
* pegado venga partido en varios chunks (los grandes lo vienen). Dentro de
|
|
48
|
+
* las marcas, un salto de linea es texto y nunca envia.
|
|
49
|
+
* 2. El Enter no envia en el acto: se apunta y se decide en el setImmediate
|
|
50
|
+
* siguiente, cuando el chunk entero ya paso. Si detras venia mas texto, ese
|
|
51
|
+
* Enter era un salto del pegado. Esto salva a los terminales que no soportan
|
|
52
|
+
* el punto 1 (conhost antiguo, por ejemplo).
|
|
53
|
+
*/
|
|
54
|
+
const PASTE_START = "\x1b[200~";
|
|
55
|
+
const PASTE_END = "\x1b[201~";
|
|
56
|
+
/** Activar/desactivar el modo en el terminal. Se apaga al salir: si se queda puesto, la shell de despues recibe las marcas como texto. */
|
|
57
|
+
const PASTE_START_REQUEST = "\x1b[?2004h";
|
|
58
|
+
const PASTE_STOP_REQUEST = "\x1b[?2004l";
|
|
31
59
|
function stripAnsi(str) {
|
|
32
60
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
33
61
|
}
|
|
@@ -86,14 +114,155 @@ function scanProjectFiles(dir, maxResults = 40) {
|
|
|
86
114
|
walk(dir);
|
|
87
115
|
return results;
|
|
88
116
|
}
|
|
117
|
+
const PLACEHOLDER = "Escribe una instrucción, '@' para archivos o '/' para comandos...";
|
|
118
|
+
/**
|
|
119
|
+
* Filas FISICAS que ocupa una linea del marco.
|
|
120
|
+
*
|
|
121
|
+
* Aqui estaba el fallo de verdad: el repintado contaba elementos del array del
|
|
122
|
+
* marco mientras el terminal cuenta filas de pantalla. Cualquier linea que no
|
|
123
|
+
* cupiera en el ancho se partia en dos filas, el cursor subia una fila de menos
|
|
124
|
+
* y el marco entero se iba bajando, dejando una barra huerfana por pulsacion.
|
|
125
|
+
* La entrada ademas puede llevar saltos de linea dentro (continuacion con "\").
|
|
126
|
+
*/
|
|
127
|
+
function physicalRows(line, cols) {
|
|
128
|
+
return stripAnsi(line)
|
|
129
|
+
.split("\n")
|
|
130
|
+
.reduce((n, seg) => n + Math.max(1, Math.ceil(seg.length / cols)), 0);
|
|
131
|
+
}
|
|
132
|
+
/** Recorta a `width` columnas visibles. Texto sin pintar: aqui no hay que cerrar colores. */
|
|
133
|
+
function truncatePlain(text, width) {
|
|
134
|
+
if (width <= 0)
|
|
135
|
+
return "";
|
|
136
|
+
if (text.length <= width)
|
|
137
|
+
return text;
|
|
138
|
+
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Arma el marco del prompt y dice exactamente donde queda el cursor.
|
|
142
|
+
*
|
|
143
|
+
* Va aparte de render() por dos razones: se puede probar sin terminal, y la
|
|
144
|
+
* aritmetica de filas se hace UNA vez sobre las mismas lineas que se pintan, en
|
|
145
|
+
* lugar de estimarse por otro lado y descuadrarse.
|
|
146
|
+
*/
|
|
147
|
+
export function buildPromptFrame(opts) {
|
|
148
|
+
const cols = Math.max(20, opts.cols || 80);
|
|
149
|
+
const viewportRows = Math.max(6, opts.rows || 24);
|
|
150
|
+
const promptSymbol = opts.promptSymbol ?? "❯";
|
|
151
|
+
const input = opts.input ?? "";
|
|
152
|
+
const cursorIndex = Math.max(0, Math.min(opts.cursorIndex ?? input.length, input.length));
|
|
153
|
+
const items = opts.items ?? [];
|
|
154
|
+
const selectedIndex = items.length > 0 ? (((opts.selectedIndex ?? 0) % items.length) + items.length) % items.length : 0;
|
|
155
|
+
const isPlan = promptSymbol.includes("plan");
|
|
156
|
+
const symbolStr = isPlan
|
|
157
|
+
? `${chalk.cyan.bold("plan ❯")} `
|
|
158
|
+
: `${chalk.bold.hex("#D97757")("❯")} `;
|
|
159
|
+
const symbolWidth = isPlan ? 7 : 2;
|
|
160
|
+
const bar = chalk.gray("─".repeat(cols));
|
|
161
|
+
// 1. Linea de entrada. Lo que el usuario escribe NO se recorta nunca: se deja
|
|
162
|
+
// que el terminal la parta y despues se cuentan las filas que ocupa.
|
|
163
|
+
let formattedInput;
|
|
164
|
+
if (input.length === 0) {
|
|
165
|
+
// La pista si se recorta: en una ventana estrecha ocupaba una segunda fila
|
|
166
|
+
// para no decir nada que el usuario no sepa ya.
|
|
167
|
+
formattedInput = chalk.gray(truncatePlain(PLACEHOLDER, cols - symbolWidth));
|
|
168
|
+
}
|
|
169
|
+
else if (input.startsWith("/")) {
|
|
170
|
+
const parts = input.split(" ");
|
|
171
|
+
const cmdPart = chalk.bold.hex("#A855F7")(parts[0]);
|
|
172
|
+
const restPart = parts.slice(1).join(" ");
|
|
173
|
+
formattedInput = restPart ? `${cmdPart} ${chalk.white(restPart)}` : cmdPart;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
formattedInput = input.replace(/(@[^\s]+)/g, (m) => chalk.bold.hex("#D97757")(m));
|
|
177
|
+
}
|
|
178
|
+
const inputLine = symbolStr + formattedInput;
|
|
179
|
+
const inputRows = physicalRows(inputLine, cols);
|
|
180
|
+
// 2. Desplegable. Un marco mas alto que la ventana obliga al terminal a
|
|
181
|
+
// desplazar el contenido, y a partir de ahi el repintado ya no sabe donde
|
|
182
|
+
// esta su primera fila: se muestra solo una ventana alrededor del elegido.
|
|
183
|
+
const room = Math.max(1, viewportRows - 1 - (1 + inputRows + 1 + 1));
|
|
184
|
+
const shown = Math.min(items.length, room);
|
|
185
|
+
const dropdownOffset = shown > 0 && items.length > shown
|
|
186
|
+
? Math.min(Math.max(0, selectedIndex - Math.floor(shown / 2)), items.length - shown)
|
|
187
|
+
: 0;
|
|
188
|
+
const dropdownLines = [];
|
|
189
|
+
for (let i = dropdownOffset; i < dropdownOffset + shown; i++) {
|
|
190
|
+
const item = items[i];
|
|
191
|
+
const isSelected = i === selectedIndex;
|
|
192
|
+
const colorHex = item.type === "file" ? "#D97757" : "#A855F7";
|
|
193
|
+
const pointer = isSelected ? chalk.bold.hex(colorHex)(" ❯ ") : chalk.gray(" ");
|
|
194
|
+
// El puntero se lleva 3 columnas fijas. De lo que queda manda la etiqueta:
|
|
195
|
+
// la descripcion es lo primero que se cae, porque una ruta a medias no se
|
|
196
|
+
// puede ni leer ni elegir.
|
|
197
|
+
const free = cols - 3;
|
|
198
|
+
const label = truncatePlain(item.label, free);
|
|
199
|
+
const roomForDesc = free - label.length - 2;
|
|
200
|
+
const secondary = item.secondary && roomForDesc >= 8 ? truncatePlain(item.secondary, roomForDesc) : "";
|
|
201
|
+
const paintedLabel = isSelected ? chalk.bold.white(label) : chalk.hex(colorHex)(label);
|
|
202
|
+
const paintedDesc = secondary
|
|
203
|
+
? isSelected
|
|
204
|
+
? chalk.whiteBright(` ${secondary}`)
|
|
205
|
+
: chalk.gray(` ${secondary}`)
|
|
206
|
+
: "";
|
|
207
|
+
dropdownLines.push(`${pointer}${paintedLabel}${paintedDesc}`);
|
|
208
|
+
}
|
|
209
|
+
// 3. Pie de estado. El relleno se calculaba con Math.max(1, ...): cuando no
|
|
210
|
+
// cabia, en vez de recortarse se quedaba con un espacio y la linea salia MAS
|
|
211
|
+
// ancha que el terminal. En una consola de 80 columnas el pie media 87 y se
|
|
212
|
+
// partia en dos filas desde el primer dibujado.
|
|
213
|
+
const modeLook = describeMode(getMode());
|
|
214
|
+
const hintPlain = "? /help para atajos · shift+tab modo · esc cancelar";
|
|
215
|
+
const modePlain = `⇥ modo ${modeLook.label}`;
|
|
216
|
+
const enginePlain = "· Espectro 1.0 (Ágil)";
|
|
217
|
+
const hint = chalk.gray(hintPlain);
|
|
218
|
+
const modeTag = chalk.hex(modeLook.color)(modePlain);
|
|
219
|
+
const engine = chalk.gray(enginePlain);
|
|
220
|
+
let footerLine;
|
|
221
|
+
if (hintPlain.length + 1 + modePlain.length + 1 + enginePlain.length <= cols) {
|
|
222
|
+
const padLen = cols - hintPlain.length - modePlain.length - 1 - enginePlain.length;
|
|
223
|
+
footerLine = `${hint}${" ".repeat(padLen)}${modeTag} ${engine}`;
|
|
224
|
+
}
|
|
225
|
+
else if (modePlain.length + 1 + enginePlain.length <= cols) {
|
|
226
|
+
// Los atajos son opcionales; el modo activo no, que es lo que decide si te
|
|
227
|
+
// van a preguntar antes de tocar tus archivos.
|
|
228
|
+
footerLine = `${" ".repeat(cols - modePlain.length - 1 - enginePlain.length)}${modeTag} ${engine}`;
|
|
229
|
+
}
|
|
230
|
+
else if (modePlain.length <= cols) {
|
|
231
|
+
footerLine = `${" ".repeat(cols - modePlain.length)}${modeTag}`;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
footerLine = chalk.hex(modeLook.color)(truncatePlain(modePlain, cols));
|
|
235
|
+
}
|
|
236
|
+
const lines = [bar, inputLine, ...dropdownLines, bar, footerLine];
|
|
237
|
+
// 4. Donde queda el cursor, en filas fisicas. Antes se hacia cursorCol =
|
|
238
|
+
// total % cols: aplicaba el modulo pero nunca sumaba la fila, asi que en
|
|
239
|
+
// cuanto la entrada se partia el acento se iba a escribir a otra linea.
|
|
240
|
+
const typed = input.length === 0 ? "" : input.slice(0, cursorIndex);
|
|
241
|
+
const segments = typed.split("\n");
|
|
242
|
+
let cursorRow = physicalRows(bar, cols);
|
|
243
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
244
|
+
cursorRow += Math.max(1, Math.ceil(((i === 0 ? symbolWidth : 0) + segments[i].length) / cols));
|
|
245
|
+
}
|
|
246
|
+
const lastSegWidth = (segments.length === 1 ? symbolWidth : 0) + segments[segments.length - 1].length;
|
|
247
|
+
cursorRow += Math.floor(lastSegWidth / cols);
|
|
248
|
+
const cursorCol = lastSegWidth % cols;
|
|
249
|
+
const totalRows = lines.reduce((n, l) => n + physicalRows(l, cols), 0);
|
|
250
|
+
return { lines, totalRows, cursorRow, cursorCol, dropdownOffset };
|
|
251
|
+
}
|
|
89
252
|
export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.cwd()) {
|
|
90
253
|
return new Promise((resolve) => {
|
|
91
254
|
let inputBuffer = "";
|
|
92
255
|
let cursorIndex = 0;
|
|
93
256
|
let selectedIndex = 0;
|
|
94
257
|
let isFinished = false;
|
|
95
|
-
|
|
96
|
-
let
|
|
258
|
+
// Estado del pegado. Ver la nota PEGADO DE VARIAS LINEAS de arriba.
|
|
259
|
+
let isPasting = false;
|
|
260
|
+
let pendingSubmit = null;
|
|
261
|
+
let lastWasCR = false;
|
|
262
|
+
// Filas FISICAS, no elementos del array: es la unica cuenta con la que el
|
|
263
|
+
// terminal esta de acuerdo.
|
|
264
|
+
let hasRendered = false;
|
|
265
|
+
let lastCursorRow = 0;
|
|
97
266
|
const history = loadHistory();
|
|
98
267
|
let historyIdx = history.length;
|
|
99
268
|
// Cache project files for @ autocompletion
|
|
@@ -119,6 +288,38 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
119
288
|
process.stdin.setRawMode(true);
|
|
120
289
|
}
|
|
121
290
|
process.stdin.resume();
|
|
291
|
+
// A partir de aqui el teclado es de este prompt: un vigilante que se pare
|
|
292
|
+
// tarde ya no puede devolver el modo crudo a como estaba antes del turno,
|
|
293
|
+
// porque "antes del turno" ya no es ahora.
|
|
294
|
+
tomarTecladoParaPrompt();
|
|
295
|
+
// Y por si alguien se lo quita igualmente. Ver keyboardGuard.ts: son
|
|
296
|
+
// demasiados los sitios que apagan el modo crudo con razon, y basta que uno
|
|
297
|
+
// llegue tarde para que el usuario teclee y su texto salga en la linea de
|
|
298
|
+
// ayuda en vez de en el recuadro.
|
|
299
|
+
const vigilanteTeclado = setInterval(() => {
|
|
300
|
+
if (isFinished)
|
|
301
|
+
return;
|
|
302
|
+
const reparado = repararTeclado(process.stdin);
|
|
303
|
+
if (reparado.length === 0)
|
|
304
|
+
return;
|
|
305
|
+
if (process.env.CHOCOLATITO_TECLADO === "debug") {
|
|
306
|
+
process.stderr.write(`\n[teclado] recuperado: ${reparado.join(" y ")}\n`);
|
|
307
|
+
}
|
|
308
|
+
// El terminal ya ha hecho eco de lo que se tecleara mientras tanto, en un
|
|
309
|
+
// sitio que este dibujo no controla. Intentar borrar el marco anterior
|
|
310
|
+
// contando filas se comeria ese texto ajeno y dejaria el cursor perdido:
|
|
311
|
+
// se dibuja uno limpio debajo y se sigue.
|
|
312
|
+
hasRendered = false;
|
|
313
|
+
lastCursorRow = 0;
|
|
314
|
+
render();
|
|
315
|
+
}, MS_ENTRE_COMPROBACIONES);
|
|
316
|
+
// Un temporizador de vigilancia no es motivo para que el proceso siga vivo.
|
|
317
|
+
vigilanteTeclado.unref?.();
|
|
318
|
+
// Se pide el pegado entre corchetes solo si hay terminal delante; a un
|
|
319
|
+
// fichero o a una tuberia esto seria basura en la salida.
|
|
320
|
+
if (process.stdout.isTTY) {
|
|
321
|
+
process.stdout.write(PASTE_START_REQUEST);
|
|
322
|
+
}
|
|
122
323
|
function getAtMention() {
|
|
123
324
|
const textBeforeCursor = inputBuffer.slice(0, cursorIndex);
|
|
124
325
|
const match = textBeforeCursor.match(/@([^\s]*)$/);
|
|
@@ -163,115 +364,76 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
163
364
|
if (isFinished)
|
|
164
365
|
return;
|
|
165
366
|
const cols = Math.max(20, process.stdout.columns || 80);
|
|
166
|
-
// 1. Top separator line
|
|
167
|
-
const topBar = chalk.gray("─".repeat(cols));
|
|
168
|
-
// 2. Prompt symbol & input text
|
|
169
|
-
const symbolStr = promptSymbol.includes("plan")
|
|
170
|
-
? `${chalk.cyan.bold("plan ❯")} `
|
|
171
|
-
: `${chalk.bold.hex("#D97757")("❯")} `;
|
|
172
|
-
const rawSymbolLen = promptSymbol.includes("plan") ? 7 : 2;
|
|
173
|
-
const placeholderText = "Escribe una instrucción, '@' para archivos o '/' para comandos...";
|
|
174
|
-
let formattedInput = "";
|
|
175
|
-
if (inputBuffer.length === 0) {
|
|
176
|
-
formattedInput = chalk.gray(placeholderText);
|
|
177
|
-
}
|
|
178
|
-
else if (inputBuffer.startsWith("/")) {
|
|
179
|
-
const parts = inputBuffer.split(" ");
|
|
180
|
-
const cmdPart = chalk.bold.hex("#A855F7")(parts[0]);
|
|
181
|
-
const restPart = parts.slice(1).join(" ");
|
|
182
|
-
formattedInput = restPart ? `${cmdPart} ${chalk.white(restPart)}` : cmdPart;
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
formattedInput = inputBuffer.replace(/(@[^\s]+)/g, (m) => chalk.bold.hex("#D97757")(m));
|
|
186
|
-
}
|
|
187
|
-
// 3. Dropdown items (if any)
|
|
188
367
|
const items = getDropdownItems();
|
|
189
|
-
const dropdownLines = [];
|
|
190
368
|
if (items.length > 0) {
|
|
191
369
|
if (selectedIndex >= items.length)
|
|
192
370
|
selectedIndex = 0;
|
|
193
371
|
if (selectedIndex < 0)
|
|
194
372
|
selectedIndex = items.length - 1;
|
|
195
|
-
items.forEach((item, idx) => {
|
|
196
|
-
const isSelected = idx === selectedIndex;
|
|
197
|
-
const isFile = item.type === "file";
|
|
198
|
-
const colorHex = isFile ? "#D97757" : "#A855F7";
|
|
199
|
-
const pointer = isSelected
|
|
200
|
-
? chalk.bold.hex(colorHex)(" ❯ ")
|
|
201
|
-
: chalk.gray(" ");
|
|
202
|
-
const label = isSelected
|
|
203
|
-
? chalk.bold.white(item.label)
|
|
204
|
-
: chalk.hex(colorHex)(item.label);
|
|
205
|
-
const desc = isSelected
|
|
206
|
-
? chalk.whiteBright(` ${item.secondary || ""}`)
|
|
207
|
-
: chalk.gray(` ${item.secondary || ""}`);
|
|
208
|
-
dropdownLines.push(`${pointer}${label}${desc}`);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
// 4. Bottom separator line
|
|
212
|
-
const bottomBar = chalk.gray("─".repeat(cols));
|
|
213
|
-
// 5. Footer status line
|
|
214
|
-
const modeLook = describeMode(getMode());
|
|
215
|
-
const leftFooter = chalk.gray("? /help para atajos · shift+tab modo · esc cancelar");
|
|
216
|
-
const rightFooter = `${chalk.hex(modeLook.color)(`⇥ modo ${modeLook.label}`)} ${chalk.gray("· Espectro 1.0 (Ágil)")}`;
|
|
217
|
-
const leftLen = stripAnsi(leftFooter).length;
|
|
218
|
-
const rightLen = stripAnsi(rightFooter).length;
|
|
219
|
-
const padLen = Math.max(1, cols - leftLen - rightLen);
|
|
220
|
-
const footerLine = `${leftFooter}${" ".repeat(padLen)}${rightFooter}`;
|
|
221
|
-
// Build current frame array
|
|
222
|
-
const currentFrame = [topBar, symbolStr + formattedInput];
|
|
223
|
-
if (dropdownLines.length > 0) {
|
|
224
|
-
currentFrame.push(...dropdownLines);
|
|
225
373
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const content = i < currentFrame.length ? currentFrame[i] : "";
|
|
247
|
-
process.stdout.write(`\x1b[2K${content}`);
|
|
248
|
-
if (i < maxLines - 1) {
|
|
249
|
-
process.stdout.write("\x1b[1E");
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
const moveUp = (maxLines - 1) - inputLineIdx;
|
|
253
|
-
if (moveUp > 0) {
|
|
254
|
-
process.stdout.write(`\x1b[${moveUp}A\r\x1b[${cursorCol}C`);
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
process.stdout.write(`\r\x1b[${cursorCol}C`);
|
|
258
|
-
}
|
|
374
|
+
const frame = buildPromptFrame({
|
|
375
|
+
cols,
|
|
376
|
+
rows: process.stdout.rows || 24,
|
|
377
|
+
promptSymbol,
|
|
378
|
+
input: inputBuffer,
|
|
379
|
+
cursorIndex,
|
|
380
|
+
items,
|
|
381
|
+
selectedIndex,
|
|
382
|
+
});
|
|
383
|
+
// Todo en una sola escritura: un marco pintado a trozos es un marco que
|
|
384
|
+
// parpadea.
|
|
385
|
+
const out = [];
|
|
386
|
+
if (hasRendered) {
|
|
387
|
+
// Volver a la primera fila del marco anterior y borrar de ahi hacia
|
|
388
|
+
// abajo. Antes se repintaba linea a linea con \x1b[2K contando lineas
|
|
389
|
+
// logicas, asi que lo que el terminal habia partido en dos filas se
|
|
390
|
+
// quedaba en pantalla para siempre.
|
|
391
|
+
if (lastCursorRow > 0)
|
|
392
|
+
out.push(`\x1b[${lastCursorRow}A`);
|
|
393
|
+
out.push("\r\x1b[0J");
|
|
259
394
|
}
|
|
260
|
-
|
|
261
|
-
|
|
395
|
+
out.push(frame.lines.join("\n"));
|
|
396
|
+
// El cursor se quedo en la ultima fila pintada; hay que subirlo a la de la
|
|
397
|
+
// entrada. Ojo con los ceros: \x1b[0A y \x1b[0C mueven UNA posicion.
|
|
398
|
+
const moveUp = frame.totalRows - 1 - frame.cursorRow;
|
|
399
|
+
if (moveUp > 0)
|
|
400
|
+
out.push(`\x1b[${moveUp}A`);
|
|
401
|
+
out.push("\r");
|
|
402
|
+
if (frame.cursorCol > 0)
|
|
403
|
+
out.push(`\x1b[${frame.cursorCol}C`);
|
|
404
|
+
process.stdout.write(out.join(""));
|
|
405
|
+
hasRendered = true;
|
|
406
|
+
lastCursorRow = frame.cursorRow;
|
|
262
407
|
}
|
|
263
408
|
function cleanup() {
|
|
264
409
|
isFinished = true;
|
|
410
|
+
clearInterval(vigilanteTeclado);
|
|
411
|
+
devolverTecladoDelPrompt();
|
|
412
|
+
if (pendingSubmit) {
|
|
413
|
+
clearImmediate(pendingSubmit);
|
|
414
|
+
pendingSubmit = null;
|
|
415
|
+
}
|
|
265
416
|
process.stdin.removeListener("keypress", onKeypress);
|
|
266
417
|
if (process.stdin.setRawMode) {
|
|
267
418
|
process.stdin.setRawMode(wasRaw || false);
|
|
268
419
|
}
|
|
420
|
+
if (process.stdout.isTTY) {
|
|
421
|
+
process.stdout.write(PASTE_STOP_REQUEST);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/** Mete texto donde esta el cursor. Un solo sitio para no repetir el corte del buffer. */
|
|
425
|
+
function insertText(text) {
|
|
426
|
+
inputBuffer = inputBuffer.slice(0, cursorIndex) + text + inputBuffer.slice(cursorIndex);
|
|
427
|
+
cursorIndex += text.length;
|
|
428
|
+
selectedIndex = 0;
|
|
269
429
|
}
|
|
270
430
|
function applySelectedDropdownItem() {
|
|
271
431
|
const items = getDropdownItems();
|
|
272
432
|
if (items.length === 0)
|
|
273
433
|
return false;
|
|
274
434
|
const item = items[selectedIndex];
|
|
435
|
+
if (!item)
|
|
436
|
+
return false;
|
|
275
437
|
if (item.type === "command") {
|
|
276
438
|
inputBuffer = item.insertText;
|
|
277
439
|
cursorIndex = inputBuffer.length;
|
|
@@ -292,38 +454,91 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
292
454
|
const symbolStr = promptSymbol.includes("plan")
|
|
293
455
|
? `${chalk.cyan.bold("plan ❯")} `
|
|
294
456
|
: `${chalk.bold.hex("#D97757")("❯")} `;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
process.stdout.write(`\x1b[2K${chalk.gray("─".repeat(cols))}\x1b[1E`);
|
|
306
|
-
// Clear any leftover rows (dropdown, old footer)
|
|
307
|
-
const extraLines = Math.max(0, lastRenderedLines.length - 3);
|
|
308
|
-
for (let i = 0; i < extraLines; i++) {
|
|
309
|
-
process.stdout.write(`\x1b[2K`);
|
|
310
|
-
if (i < extraLines - 1)
|
|
311
|
-
process.stdout.write("\x1b[1E");
|
|
312
|
-
}
|
|
313
|
-
// Move cursor directly below the bottom bar
|
|
314
|
-
if (extraLines > 0) {
|
|
315
|
-
process.stdout.write(`\x1b[${extraLines}A\r\x1b[1E\n`);
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
process.stdout.write("\n");
|
|
457
|
+
const bar = chalk.gray("─".repeat(cols));
|
|
458
|
+
const out = [];
|
|
459
|
+
// Subir a la primera fila del marco y borrar de ahi hacia abajo. Antes se
|
|
460
|
+
// contaban a mano las filas sobrantes (desplegable, pie) y esa cuenta era
|
|
461
|
+
// en lineas logicas: lo que el terminal habia partido se quedaba debajo
|
|
462
|
+
// del prompt ya enviado.
|
|
463
|
+
if (hasRendered) {
|
|
464
|
+
if (lastCursorRow > 0)
|
|
465
|
+
out.push(`\x1b[${lastCursorRow}A`);
|
|
466
|
+
out.push("\r\x1b[0J");
|
|
319
467
|
}
|
|
468
|
+
out.push(`${bar}\n${symbolStr}${chalk.bold.white(submittedText)}\n${bar}\n\n`);
|
|
469
|
+
process.stdout.write(out.join(""));
|
|
470
|
+
hasRendered = false;
|
|
471
|
+
lastCursorRow = 0;
|
|
472
|
+
}
|
|
473
|
+
function submitNow() {
|
|
474
|
+
if (isFinished)
|
|
475
|
+
return;
|
|
476
|
+
pendingSubmit = null;
|
|
477
|
+
appendHistory(inputBuffer);
|
|
478
|
+
cleanup();
|
|
479
|
+
finalizePromptOnSubmit(inputBuffer);
|
|
480
|
+
resolve(inputBuffer);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Apunta el envio para el final del tick. Todo el chunk de stdin se emite
|
|
484
|
+
* antes de que corra el setImmediate, asi que para entonces ya se sabe si
|
|
485
|
+
* detras del Enter venia mas texto pegado.
|
|
486
|
+
*/
|
|
487
|
+
function scheduleSubmit() {
|
|
488
|
+
if (pendingSubmit)
|
|
489
|
+
return;
|
|
490
|
+
pendingSubmit = setImmediate(submitNow);
|
|
491
|
+
}
|
|
492
|
+
/** El Enter en espera no era una orden de enviar: era un salto del pegado. */
|
|
493
|
+
function absorbPendingEnter() {
|
|
494
|
+
if (!pendingSubmit)
|
|
495
|
+
return;
|
|
496
|
+
clearImmediate(pendingSubmit);
|
|
497
|
+
pendingSubmit = null;
|
|
498
|
+
insertText("\n");
|
|
320
499
|
}
|
|
321
500
|
function onKeypress(str, key) {
|
|
322
501
|
if (isFinished)
|
|
323
502
|
return;
|
|
503
|
+
// Marcas del pegado entre corchetes. Node >= 22 las nombra; en versiones
|
|
504
|
+
// anteriores solo llega la secuencia cruda, asi que se miran las dos.
|
|
505
|
+
const sequence = (key && key.sequence) || str || "";
|
|
506
|
+
if ((key && key.name === "paste-start") || sequence === PASTE_START) {
|
|
507
|
+
isPasting = true;
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if ((key && key.name === "paste-end") || sequence === PASTE_END) {
|
|
511
|
+
isPasting = false;
|
|
512
|
+
render();
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const isEnter = Boolean(key && (key.name === "return" || key.name === "enter"));
|
|
516
|
+
// CRLF llega como DOS pulsaciones ("\r" y luego "\n") y es un unico salto.
|
|
517
|
+
// Sin esto, el portapapeles de Windows duplicaba todas las lineas en blanco.
|
|
518
|
+
const isPairedLF = isEnter && key.name === "enter" && lastWasCR;
|
|
519
|
+
lastWasCR = isEnter && key.name === "return";
|
|
520
|
+
if (isPairedLF)
|
|
521
|
+
return;
|
|
522
|
+
// Cualquier tecla que no sea el Enter y llegue en su mismo tick delata un
|
|
523
|
+
// pegado: aquel Enter pasa a ser salto de linea y se sigue con esta tecla.
|
|
524
|
+
if (!isEnter)
|
|
525
|
+
absorbPendingEnter();
|
|
324
526
|
const items = getDropdownItems();
|
|
325
527
|
// Enter key
|
|
326
|
-
if (
|
|
528
|
+
if (isEnter) {
|
|
529
|
+
// Dentro de un pegado, un salto de linea es texto y no envia nada.
|
|
530
|
+
if (isPasting) {
|
|
531
|
+
insertText("\n");
|
|
532
|
+
render();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Segundo Enter del mismo tick: linea en blanco dentro del pegado.
|
|
536
|
+
if (pendingSubmit) {
|
|
537
|
+
absorbPendingEnter();
|
|
538
|
+
scheduleSubmit();
|
|
539
|
+
render();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
327
542
|
// Multi-line continuation if ends with \
|
|
328
543
|
if (inputBuffer.endsWith("\\")) {
|
|
329
544
|
inputBuffer = inputBuffer.slice(0, -1) + "\n";
|
|
@@ -337,10 +552,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
337
552
|
return;
|
|
338
553
|
}
|
|
339
554
|
}
|
|
340
|
-
|
|
341
|
-
cleanup();
|
|
342
|
-
finalizePromptOnSubmit(inputBuffer);
|
|
343
|
-
resolve(inputBuffer);
|
|
555
|
+
scheduleSubmit();
|
|
344
556
|
return;
|
|
345
557
|
}
|
|
346
558
|
// Shift+Tab: cambia el modo de permisos instantáneamente
|
|
@@ -435,14 +647,18 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
435
647
|
inputBuffer = "";
|
|
436
648
|
cursorIndex = 0;
|
|
437
649
|
selectedIndex = 0;
|
|
650
|
+
// Salida de emergencia: si un terminal raro mandara la marca de apertura
|
|
651
|
+
// del pegado y nunca la de cierre, sin esto el Enter no volveria a
|
|
652
|
+
// enviar en toda la sesion.
|
|
653
|
+
isPasting = false;
|
|
438
654
|
render();
|
|
439
655
|
return;
|
|
440
656
|
}
|
|
441
657
|
// Ctrl + L (clear screen)
|
|
442
658
|
if (key && key.ctrl && key.name === "l") {
|
|
443
659
|
console.clear();
|
|
444
|
-
|
|
445
|
-
|
|
660
|
+
hasRendered = false;
|
|
661
|
+
lastCursorRow = 0;
|
|
446
662
|
render();
|
|
447
663
|
return;
|
|
448
664
|
}
|
|
@@ -451,11 +667,15 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
451
667
|
!isShiftTab &&
|
|
452
668
|
str !== "\x1b[Z" &&
|
|
453
669
|
(!key || (!key.ctrl && !key.meta) || (key.ctrl && key.meta) || (!key.name && str.length > 0))) {
|
|
670
|
+
// Una secuencia de escape no es texto. Hoy readline manda str undefined
|
|
671
|
+
// cuando reconoce la secuencia, asi que este filtro casi nunca salta;
|
|
672
|
+
// esta para que una marca de pegado o una tecla de funcion que llegue
|
|
673
|
+
// cruda no acabe escrita como letras sueltas ("\x1b[200~") en el buffer.
|
|
674
|
+
if (str.charCodeAt(0) === 0x1b)
|
|
675
|
+
return;
|
|
454
676
|
const cleanStr = str.replace(/[\r\n]/g, "");
|
|
455
677
|
if (cleanStr.length > 0) {
|
|
456
|
-
|
|
457
|
-
cursorIndex += cleanStr.length;
|
|
458
|
-
selectedIndex = 0;
|
|
678
|
+
insertText(cleanStr);
|
|
459
679
|
render();
|
|
460
680
|
}
|
|
461
681
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
export declare class ReasoningStream {
|
|
2
2
|
private hasStarted;
|
|
3
3
|
private atLineStart;
|
|
4
|
+
private inicio;
|
|
5
|
+
private caracteres;
|
|
6
|
+
private ultimoRefresco;
|
|
7
|
+
private cola;
|
|
8
|
+
private lineaPintada;
|
|
4
9
|
push(chunk: string): void;
|
|
10
|
+
/** El volcado literal, para quien lo pida. Una escritura por trozo, no por letra. */
|
|
11
|
+
private volcar;
|
|
12
|
+
private refrescar;
|
|
5
13
|
flush(): void;
|
|
6
14
|
}
|