chocolatito-code 1.0.0 → 1.2.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 +155 -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 +642 -91
- 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/salidaDelAgente.d.ts +43 -0
- package/dist/agent/salidaDelAgente.js +53 -0
- 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/nodeVersion.d.ts +13 -0
- package/dist/config/nodeVersion.js +26 -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 +240 -37
- package/dist/mcp/client.d.ts +50 -0
- package/dist/mcp/client.js +155 -19
- package/dist/mcp/ide.d.ts +76 -0
- package/dist/mcp/ide.js +160 -0
- package/dist/mcp/manager.js +9 -0
- 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 +237 -22
- 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 +17 -4
- 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/grepSearch.js +111 -23
- 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 +58 -5
- 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.d.ts +16 -1
- package/dist/tools/writeFile.js +40 -2
- package/dist/ui/interrupt.d.ts +4 -1
- package/dist/ui/interrupt.js +59 -7
- package/dist/ui/keyboardGuard.d.ts +76 -0
- package/dist/ui/keyboardGuard.js +94 -0
- package/dist/ui/loopPrompt.d.ts +17 -0
- package/dist/ui/loopPrompt.js +59 -0
- package/dist/ui/pegados.d.ts +50 -0
- package/dist/ui/pegados.js +83 -0
- package/dist/ui/permissionPrompt.js +84 -2
- package/dist/ui/prompt.d.ts +40 -0
- package/dist/ui/prompt.js +436 -131
- package/dist/ui/reasoningStream.d.ts +8 -0
- package/dist/ui/reasoningStream.js +99 -7
- package/dist/ui/salida.d.ts +10 -0
- package/dist/ui/salida.js +50 -0
- package/extension/README.md +95 -74
- package/extension/background.js +772 -181
- package/extension/content.js +527 -46
- package/extension/fuentes/Chocolatito-Marca.ttf +0 -0
- 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/logotipo.png +0 -0
- package/extension/manifest.json +28 -4
- package/extension/popup.html +16 -6
- package/extension/popup.js +28 -12
- package/package.json +3 -2
package/dist/ui/prompt.js
CHANGED
|
@@ -5,8 +5,43 @@ 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, reafirmarModoCrudo, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
|
|
9
10
|
import { clearTyped } from "./typeAhead.js";
|
|
11
|
+
import { dibujar } from "./salida.js";
|
|
12
|
+
import { esGrande, guardarPegado, expandirPegados, marcaQueTerminaEn, limpiarPegados } from "./pegados.js";
|
|
13
|
+
/**
|
|
14
|
+
* Escribir algo mientras el prompt esta dibujado.
|
|
15
|
+
*
|
|
16
|
+
* Sale de una sesion real del usuario. Los servidores MCP se conectan en
|
|
17
|
+
* segundo plano para no retrasar el arranque, y cuando terminan sueltan su
|
|
18
|
+
* resumen con `console.log`. Si para entonces el prompt ya estaba en pantalla,
|
|
19
|
+
* el resumen se escribe ENCIMA del marco y lo destroza:
|
|
20
|
+
*
|
|
21
|
+
* ❯ Escribe una instruccion...
|
|
22
|
+
* 🔌 Servidores MCP:────────────────────────────────────
|
|
23
|
+
* ✔ ide (http): 4 herramientasb modo · esc cancelar ⇥ modo normal
|
|
24
|
+
*
|
|
25
|
+
* Ese "4 herramientasb modo" es el resumen pisando la linea de ayuda. Le pasa a
|
|
26
|
+
* cualquier cosa que se imprima tarde: el aviso de actualizacion, un servidor
|
|
27
|
+
* MCP lento, un hook que tarda.
|
|
28
|
+
*
|
|
29
|
+
* La regla es sencilla: si hay un prompt abierto, primero se borra su marco, se
|
|
30
|
+
* escribe el mensaje donde estaba, y se vuelve a dibujar debajo. Si no lo hay,
|
|
31
|
+
* es un console.log normal y ya.
|
|
32
|
+
*/
|
|
33
|
+
let escribirEncimaDelPrompt = null;
|
|
34
|
+
/**
|
|
35
|
+
* Imprime sin romper el prompt. Uselo en vez de `console.log` para todo lo que
|
|
36
|
+
* pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
|
|
37
|
+
*/
|
|
38
|
+
export function imprimirSobreElPrompt(texto) {
|
|
39
|
+
if (escribirEncimaDelPrompt) {
|
|
40
|
+
escribirEncimaDelPrompt(texto);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
dibujar(texto.endsWith("\n") ? texto : `${texto}\n`);
|
|
44
|
+
}
|
|
10
45
|
export const SLASH_COMMANDS = [
|
|
11
46
|
{ name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
|
|
12
47
|
{ name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
|
|
@@ -28,6 +63,33 @@ export const SLASH_COMMANDS = [
|
|
|
28
63
|
{ name: "/exit", description: "Cerrar sesión de Chocolatito Code" },
|
|
29
64
|
];
|
|
30
65
|
let isKeypressInitialized = false;
|
|
66
|
+
/**
|
|
67
|
+
* PEGADO DE VARIAS LINEAS
|
|
68
|
+
*
|
|
69
|
+
* Un pegado no llega tecla a tecla: llega como UN chunk de stdin que readline
|
|
70
|
+
* desmonta en pulsaciones y emite todas en el mismo tick sincrono. El primer
|
|
71
|
+
* "\r" del texto se emitia como key.name === "return", el manejador de Enter
|
|
72
|
+
* hacia cleanup() + resolve() y quitaba el oyente MIENTRAS el resto del chunk
|
|
73
|
+
* se seguia emitiendo: las lineas 2..n salian sin nadie escuchando y se perdian
|
|
74
|
+
* sin dejar rastro ni en el buffer ni en la cola. El usuario creia haber mandado
|
|
75
|
+
* la instruccion entera y el agente arrancaba con la primera linea.
|
|
76
|
+
*
|
|
77
|
+
* Dos defensas, porque ninguna cubre sola todos los terminales:
|
|
78
|
+
*
|
|
79
|
+
* 1. Modo de pegado entre corchetes. El terminal envuelve lo pegado en
|
|
80
|
+
* \x1b[200~ ... \x1b[201~, asi que se distingue de lo tecleado aunque el
|
|
81
|
+
* pegado venga partido en varios chunks (los grandes lo vienen). Dentro de
|
|
82
|
+
* las marcas, un salto de linea es texto y nunca envia.
|
|
83
|
+
* 2. El Enter no envia en el acto: se apunta y se decide en el setImmediate
|
|
84
|
+
* siguiente, cuando el chunk entero ya paso. Si detras venia mas texto, ese
|
|
85
|
+
* Enter era un salto del pegado. Esto salva a los terminales que no soportan
|
|
86
|
+
* el punto 1 (conhost antiguo, por ejemplo).
|
|
87
|
+
*/
|
|
88
|
+
const PASTE_START = "\x1b[200~";
|
|
89
|
+
const PASTE_END = "\x1b[201~";
|
|
90
|
+
/** Activar/desactivar el modo en el terminal. Se apaga al salir: si se queda puesto, la shell de despues recibe las marcas como texto. */
|
|
91
|
+
const PASTE_START_REQUEST = "\x1b[?2004h";
|
|
92
|
+
const PASTE_STOP_REQUEST = "\x1b[?2004l";
|
|
31
93
|
function stripAnsi(str) {
|
|
32
94
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
33
95
|
}
|
|
@@ -86,14 +148,157 @@ function scanProjectFiles(dir, maxResults = 40) {
|
|
|
86
148
|
walk(dir);
|
|
87
149
|
return results;
|
|
88
150
|
}
|
|
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
|
+
}
|
|
89
286
|
export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.cwd()) {
|
|
90
287
|
return new Promise((resolve) => {
|
|
91
288
|
let inputBuffer = "";
|
|
92
289
|
let cursorIndex = 0;
|
|
93
290
|
let selectedIndex = 0;
|
|
94
291
|
let isFinished = false;
|
|
95
|
-
|
|
96
|
-
let
|
|
292
|
+
// Estado del pegado. Ver la nota PEGADO DE VARIAS LINEAS de arriba.
|
|
293
|
+
let isPasting = false;
|
|
294
|
+
/** Lo que va llegando del pegado, antes de decidir si cabe o va como marca. */
|
|
295
|
+
let loPegado = "";
|
|
296
|
+
let pendingSubmit = null;
|
|
297
|
+
let lastWasCR = false;
|
|
298
|
+
// Filas FISICAS, no elementos del array: es la unica cuenta con la que el
|
|
299
|
+
// terminal esta de acuerdo.
|
|
300
|
+
let hasRendered = false;
|
|
301
|
+
let lastCursorRow = 0;
|
|
97
302
|
const history = loadHistory();
|
|
98
303
|
let historyIdx = history.length;
|
|
99
304
|
// Cache project files for @ autocompletion
|
|
@@ -115,10 +320,43 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
115
320
|
// Lo que quedara a medio escribir del turno anterior no es de este prompt.
|
|
116
321
|
clearTyped();
|
|
117
322
|
const wasRaw = process.stdin.isRaw;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
323
|
+
// Reafirmar y no solo poner: si algo cambio el modo de la consola por fuera
|
|
324
|
+
// de Node, un setRawMode(true) a secas es un no-op y el prompt se abre sin
|
|
325
|
+
// teclado sin enterarse. Ver reafirmarModoCrudo().
|
|
326
|
+
reafirmarModoCrudo(process.stdin);
|
|
121
327
|
process.stdin.resume();
|
|
328
|
+
// A partir de aqui el teclado es de este prompt: un vigilante que se pare
|
|
329
|
+
// tarde ya no puede devolver el modo crudo a como estaba antes del turno,
|
|
330
|
+
// porque "antes del turno" ya no es ahora.
|
|
331
|
+
tomarTecladoParaPrompt();
|
|
332
|
+
// Y por si alguien se lo quita igualmente. Ver keyboardGuard.ts: son
|
|
333
|
+
// demasiados los sitios que apagan el modo crudo con razon, y basta que uno
|
|
334
|
+
// llegue tarde para que el usuario teclee y su texto salga en la linea de
|
|
335
|
+
// ayuda en vez de en el recuadro.
|
|
336
|
+
const vigilanteTeclado = setInterval(() => {
|
|
337
|
+
if (isFinished)
|
|
338
|
+
return;
|
|
339
|
+
const reparado = repararTeclado(process.stdin);
|
|
340
|
+
if (reparado.length === 0)
|
|
341
|
+
return;
|
|
342
|
+
if (process.env.CHOCOLATITO_TECLADO === "debug") {
|
|
343
|
+
process.stderr.write(`\n[teclado] recuperado: ${reparado.join(" y ")}\n`);
|
|
344
|
+
}
|
|
345
|
+
// El terminal ya ha hecho eco de lo que se tecleara mientras tanto, en un
|
|
346
|
+
// sitio que este dibujo no controla. Intentar borrar el marco anterior
|
|
347
|
+
// contando filas se comeria ese texto ajeno y dejaria el cursor perdido:
|
|
348
|
+
// se dibuja uno limpio debajo y se sigue.
|
|
349
|
+
hasRendered = false;
|
|
350
|
+
lastCursorRow = 0;
|
|
351
|
+
render();
|
|
352
|
+
}, MS_ENTRE_COMPROBACIONES);
|
|
353
|
+
// Un temporizador de vigilancia no es motivo para que el proceso siga vivo.
|
|
354
|
+
vigilanteTeclado.unref?.();
|
|
355
|
+
// Se pide el pegado entre corchetes solo si hay terminal delante; a un
|
|
356
|
+
// fichero o a una tuberia esto seria basura en la salida.
|
|
357
|
+
if (process.stdout.isTTY) {
|
|
358
|
+
dibujar(PASTE_START_REQUEST);
|
|
359
|
+
}
|
|
122
360
|
function getAtMention() {
|
|
123
361
|
const textBeforeCursor = inputBuffer.slice(0, cursorIndex);
|
|
124
362
|
const match = textBeforeCursor.match(/@([^\s]*)$/);
|
|
@@ -163,115 +401,78 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
163
401
|
if (isFinished)
|
|
164
402
|
return;
|
|
165
403
|
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
404
|
const items = getDropdownItems();
|
|
189
|
-
const dropdownLines = [];
|
|
190
405
|
if (items.length > 0) {
|
|
191
406
|
if (selectedIndex >= items.length)
|
|
192
407
|
selectedIndex = 0;
|
|
193
408
|
if (selectedIndex < 0)
|
|
194
409
|
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
410
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
//
|
|
222
|
-
const
|
|
223
|
-
if (
|
|
224
|
-
|
|
411
|
+
const frame = buildPromptFrame({
|
|
412
|
+
cols,
|
|
413
|
+
rows: process.stdout.rows || 24,
|
|
414
|
+
promptSymbol,
|
|
415
|
+
input: inputBuffer,
|
|
416
|
+
cursorIndex,
|
|
417
|
+
items,
|
|
418
|
+
selectedIndex,
|
|
419
|
+
});
|
|
420
|
+
// Todo en una sola escritura: un marco pintado a trozos es un marco que
|
|
421
|
+
// parpadea.
|
|
422
|
+
const out = [];
|
|
423
|
+
if (hasRendered) {
|
|
424
|
+
// Volver a la primera fila del marco anterior y borrar de ahi hacia
|
|
425
|
+
// abajo. Antes se repintaba linea a linea con \x1b[2K contando lineas
|
|
426
|
+
// logicas, asi que lo que el terminal habia partido en dos filas se
|
|
427
|
+
// quedaba en pantalla para siempre.
|
|
428
|
+
if (lastCursorRow > 0)
|
|
429
|
+
out.push(`\x1b[${lastCursorRow}A`);
|
|
430
|
+
out.push("\r\x1b[0J");
|
|
225
431
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (lastCursorLineIndex > 0) {
|
|
239
|
-
process.stdout.write(`\x1b[${lastCursorLineIndex}A\r`);
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
process.stdout.write("\r");
|
|
243
|
-
}
|
|
244
|
-
const maxLines = Math.max(currentFrame.length, lastRenderedLines.length);
|
|
245
|
-
for (let i = 0; i < maxLines; i++) {
|
|
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
|
-
}
|
|
259
|
-
}
|
|
260
|
-
lastRenderedLines = currentFrame;
|
|
261
|
-
lastCursorLineIndex = inputLineIdx;
|
|
432
|
+
out.push(frame.lines.join("\n"));
|
|
433
|
+
// El cursor se quedo en la ultima fila pintada; hay que subirlo a la de la
|
|
434
|
+
// entrada. Ojo con los ceros: \x1b[0A y \x1b[0C mueven UNA posicion.
|
|
435
|
+
const moveUp = frame.totalRows - 1 - frame.cursorRow;
|
|
436
|
+
if (moveUp > 0)
|
|
437
|
+
out.push(`\x1b[${moveUp}A`);
|
|
438
|
+
out.push("\r");
|
|
439
|
+
if (frame.cursorCol > 0)
|
|
440
|
+
out.push(`\x1b[${frame.cursorCol}C`);
|
|
441
|
+
dibujar(out.join(""));
|
|
442
|
+
hasRendered = true;
|
|
443
|
+
lastCursorRow = frame.cursorRow;
|
|
262
444
|
}
|
|
263
445
|
function cleanup() {
|
|
264
446
|
isFinished = true;
|
|
447
|
+
limpiarPegados();
|
|
448
|
+
escribirEncimaDelPrompt = null;
|
|
449
|
+
clearInterval(vigilanteTeclado);
|
|
450
|
+
devolverTecladoDelPrompt();
|
|
451
|
+
if (pendingSubmit) {
|
|
452
|
+
clearImmediate(pendingSubmit);
|
|
453
|
+
pendingSubmit = null;
|
|
454
|
+
}
|
|
265
455
|
process.stdin.removeListener("keypress", onKeypress);
|
|
266
456
|
if (process.stdin.setRawMode) {
|
|
267
457
|
process.stdin.setRawMode(wasRaw || false);
|
|
268
458
|
}
|
|
459
|
+
if (process.stdout.isTTY) {
|
|
460
|
+
dibujar(PASTE_STOP_REQUEST);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** Mete texto donde esta el cursor. Un solo sitio para no repetir el corte del buffer. */
|
|
464
|
+
function insertText(text) {
|
|
465
|
+
inputBuffer = inputBuffer.slice(0, cursorIndex) + text + inputBuffer.slice(cursorIndex);
|
|
466
|
+
cursorIndex += text.length;
|
|
467
|
+
selectedIndex = 0;
|
|
269
468
|
}
|
|
270
469
|
function applySelectedDropdownItem() {
|
|
271
470
|
const items = getDropdownItems();
|
|
272
471
|
if (items.length === 0)
|
|
273
472
|
return false;
|
|
274
473
|
const item = items[selectedIndex];
|
|
474
|
+
if (!item)
|
|
475
|
+
return false;
|
|
275
476
|
if (item.type === "command") {
|
|
276
477
|
inputBuffer = item.insertText;
|
|
277
478
|
cursorIndex = inputBuffer.length;
|
|
@@ -292,38 +493,101 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
292
493
|
const symbolStr = promptSymbol.includes("plan")
|
|
293
494
|
? `${chalk.cyan.bold("plan ❯")} `
|
|
294
495
|
: `${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");
|
|
496
|
+
const bar = chalk.gray("─".repeat(cols));
|
|
497
|
+
const out = [];
|
|
498
|
+
// Subir a la primera fila del marco y borrar de ahi hacia abajo. Antes se
|
|
499
|
+
// contaban a mano las filas sobrantes (desplegable, pie) y esa cuenta era
|
|
500
|
+
// en lineas logicas: lo que el terminal habia partido se quedaba debajo
|
|
501
|
+
// del prompt ya enviado.
|
|
502
|
+
if (hasRendered) {
|
|
503
|
+
if (lastCursorRow > 0)
|
|
504
|
+
out.push(`\x1b[${lastCursorRow}A`);
|
|
505
|
+
out.push("\r\x1b[0J");
|
|
319
506
|
}
|
|
507
|
+
out.push(`${bar}\n${symbolStr}${chalk.bold.white(submittedText)}\n${bar}\n\n`);
|
|
508
|
+
dibujar(out.join(""));
|
|
509
|
+
hasRendered = false;
|
|
510
|
+
lastCursorRow = 0;
|
|
511
|
+
}
|
|
512
|
+
function submitNow() {
|
|
513
|
+
if (isFinished)
|
|
514
|
+
return;
|
|
515
|
+
pendingSubmit = null;
|
|
516
|
+
// El historial guarda la marca, no las 347 lineas: si no, el fichero de
|
|
517
|
+
// historial crece sin control y recorrerlo con las flechas es inutil.
|
|
518
|
+
appendHistory(inputBuffer);
|
|
519
|
+
// Pero el modelo recibe el texto entero, que es lo que el usuario pego.
|
|
520
|
+
const completo = expandirPegados(inputBuffer);
|
|
521
|
+
cleanup();
|
|
522
|
+
finalizePromptOnSubmit(inputBuffer);
|
|
523
|
+
resolve(completo);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Apunta el envio para el final del tick. Todo el chunk de stdin se emite
|
|
527
|
+
* antes de que corra el setImmediate, asi que para entonces ya se sabe si
|
|
528
|
+
* detras del Enter venia mas texto pegado.
|
|
529
|
+
*/
|
|
530
|
+
function scheduleSubmit() {
|
|
531
|
+
if (pendingSubmit)
|
|
532
|
+
return;
|
|
533
|
+
pendingSubmit = setImmediate(submitNow);
|
|
534
|
+
}
|
|
535
|
+
/** El Enter en espera no era una orden de enviar: era un salto del pegado. */
|
|
536
|
+
function absorbPendingEnter() {
|
|
537
|
+
if (!pendingSubmit)
|
|
538
|
+
return;
|
|
539
|
+
clearImmediate(pendingSubmit);
|
|
540
|
+
pendingSubmit = null;
|
|
541
|
+
insertText("\n");
|
|
320
542
|
}
|
|
321
543
|
function onKeypress(str, key) {
|
|
322
544
|
if (isFinished)
|
|
323
545
|
return;
|
|
546
|
+
// Marcas del pegado entre corchetes. Node >= 22 las nombra; en versiones
|
|
547
|
+
// anteriores solo llega la secuencia cruda, asi que se miran las dos.
|
|
548
|
+
const sequence = (key && key.sequence) || str || "";
|
|
549
|
+
if ((key && key.name === "paste-start") || sequence === PASTE_START) {
|
|
550
|
+
isPasting = true;
|
|
551
|
+
// Se recoge aparte: si resulta ser grande, en el recuadro va una marca
|
|
552
|
+
// corta en vez de las 347 lineas. Ver pegados.ts.
|
|
553
|
+
loPegado = "";
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if ((key && key.name === "paste-end") || sequence === PASTE_END) {
|
|
557
|
+
isPasting = false;
|
|
558
|
+
if (loPegado) {
|
|
559
|
+
insertText(esGrande(loPegado) ? guardarPegado(loPegado) : loPegado);
|
|
560
|
+
loPegado = "";
|
|
561
|
+
}
|
|
562
|
+
render();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const isEnter = Boolean(key && (key.name === "return" || key.name === "enter"));
|
|
566
|
+
// CRLF llega como DOS pulsaciones ("\r" y luego "\n") y es un unico salto.
|
|
567
|
+
// Sin esto, el portapapeles de Windows duplicaba todas las lineas en blanco.
|
|
568
|
+
const isPairedLF = isEnter && key.name === "enter" && lastWasCR;
|
|
569
|
+
lastWasCR = isEnter && key.name === "return";
|
|
570
|
+
if (isPairedLF)
|
|
571
|
+
return;
|
|
572
|
+
// Cualquier tecla que no sea el Enter y llegue en su mismo tick delata un
|
|
573
|
+
// pegado: aquel Enter pasa a ser salto de linea y se sigue con esta tecla.
|
|
574
|
+
if (!isEnter)
|
|
575
|
+
absorbPendingEnter();
|
|
324
576
|
const items = getDropdownItems();
|
|
325
577
|
// Enter key
|
|
326
|
-
if (
|
|
578
|
+
if (isEnter) {
|
|
579
|
+
// Dentro de un pegado, el salto va al buffer aparte, no al recuadro.
|
|
580
|
+
if (isPasting) {
|
|
581
|
+
loPegado += "\n";
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// Segundo Enter del mismo tick: linea en blanco dentro del pegado.
|
|
585
|
+
if (pendingSubmit) {
|
|
586
|
+
absorbPendingEnter();
|
|
587
|
+
scheduleSubmit();
|
|
588
|
+
render();
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
327
591
|
// Multi-line continuation if ends with \
|
|
328
592
|
if (inputBuffer.endsWith("\\")) {
|
|
329
593
|
inputBuffer = inputBuffer.slice(0, -1) + "\n";
|
|
@@ -337,10 +601,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
337
601
|
return;
|
|
338
602
|
}
|
|
339
603
|
}
|
|
340
|
-
|
|
341
|
-
cleanup();
|
|
342
|
-
finalizePromptOnSubmit(inputBuffer);
|
|
343
|
-
resolve(inputBuffer);
|
|
604
|
+
scheduleSubmit();
|
|
344
605
|
return;
|
|
345
606
|
}
|
|
346
607
|
// Shift+Tab: cambia el modo de permisos instantáneamente
|
|
@@ -417,8 +678,12 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
417
678
|
// Backspace
|
|
418
679
|
if (key && (key.name === "backspace" || (key.ctrl && key.name === "h"))) {
|
|
419
680
|
if (cursorIndex > 0) {
|
|
420
|
-
|
|
421
|
-
|
|
681
|
+
// Lo que ocupa una linea se borra como una linea: sin esto habria que
|
|
682
|
+
// pulsar borrar treinta veces para quitar una marca de pegado.
|
|
683
|
+
const marca = marcaQueTerminaEn(inputBuffer, cursorIndex);
|
|
684
|
+
const desde = marca ? marca.inicio : cursorIndex - 1;
|
|
685
|
+
inputBuffer = inputBuffer.slice(0, desde) + inputBuffer.slice(cursorIndex);
|
|
686
|
+
cursorIndex = desde;
|
|
422
687
|
selectedIndex = 0;
|
|
423
688
|
render();
|
|
424
689
|
}
|
|
@@ -435,14 +700,18 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
435
700
|
inputBuffer = "";
|
|
436
701
|
cursorIndex = 0;
|
|
437
702
|
selectedIndex = 0;
|
|
703
|
+
// Salida de emergencia: si un terminal raro mandara la marca de apertura
|
|
704
|
+
// del pegado y nunca la de cierre, sin esto el Enter no volveria a
|
|
705
|
+
// enviar en toda la sesion.
|
|
706
|
+
isPasting = false;
|
|
438
707
|
render();
|
|
439
708
|
return;
|
|
440
709
|
}
|
|
441
710
|
// Ctrl + L (clear screen)
|
|
442
711
|
if (key && key.ctrl && key.name === "l") {
|
|
443
712
|
console.clear();
|
|
444
|
-
|
|
445
|
-
|
|
713
|
+
hasRendered = false;
|
|
714
|
+
lastCursorRow = 0;
|
|
446
715
|
render();
|
|
447
716
|
return;
|
|
448
717
|
}
|
|
@@ -451,15 +720,51 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
|
|
|
451
720
|
!isShiftTab &&
|
|
452
721
|
str !== "\x1b[Z" &&
|
|
453
722
|
(!key || (!key.ctrl && !key.meta) || (key.ctrl && key.meta) || (!key.name && str.length > 0))) {
|
|
723
|
+
// Una secuencia de escape no es texto. Hoy readline manda str undefined
|
|
724
|
+
// cuando reconoce la secuencia, asi que este filtro casi nunca salta;
|
|
725
|
+
// esta para que una marca de pegado o una tecla de funcion que llegue
|
|
726
|
+
// cruda no acabe escrita como letras sueltas ("\x1b[200~") en el buffer.
|
|
727
|
+
if (str.charCodeAt(0) === 0x1b)
|
|
728
|
+
return;
|
|
454
729
|
const cleanStr = str.replace(/[\r\n]/g, "");
|
|
455
|
-
if (cleanStr.length
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
730
|
+
if (cleanStr.length === 0)
|
|
731
|
+
return;
|
|
732
|
+
if (isPasting) {
|
|
733
|
+
// Va al buffer del pegado, no al recuadro: al cerrarse se decide si
|
|
734
|
+
// cabe entero o si merece una marca corta.
|
|
735
|
+
loPegado += cleanStr;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
// Sin marcas de pegado -conhost antiguo, algun terminal raro- el pegado
|
|
739
|
+
// llega como un chunk enorme en una sola pulsacion. Tecleando nunca
|
|
740
|
+
// llega mas de un caracter, asi que esto solo puede ser un pegado.
|
|
741
|
+
if (esGrande(cleanStr)) {
|
|
742
|
+
insertText(guardarPegado(cleanStr));
|
|
459
743
|
render();
|
|
744
|
+
return;
|
|
460
745
|
}
|
|
746
|
+
insertText(cleanStr);
|
|
747
|
+
render();
|
|
461
748
|
}
|
|
462
749
|
}
|
|
750
|
+
// A partir de aqui, lo que llegue tarde se dibuja SIN romper el marco.
|
|
751
|
+
escribirEncimaDelPrompt = (texto) => {
|
|
752
|
+
if (isFinished)
|
|
753
|
+
return;
|
|
754
|
+
let salida = "";
|
|
755
|
+
// Se borra el marco igual que hace render() al repintar.
|
|
756
|
+
if (hasRendered) {
|
|
757
|
+
if (lastCursorRow > 0)
|
|
758
|
+
salida += `\x1b[${lastCursorRow}A`;
|
|
759
|
+
salida += "\r\x1b[0J";
|
|
760
|
+
}
|
|
761
|
+
salida += texto.endsWith("\n") ? texto : `${texto}\n`;
|
|
762
|
+
dibujar(salida);
|
|
763
|
+
// Y el prompt se vuelve a dibujar limpio debajo del mensaje.
|
|
764
|
+
hasRendered = false;
|
|
765
|
+
lastCursorRow = 0;
|
|
766
|
+
render();
|
|
767
|
+
};
|
|
463
768
|
process.stdin.on("keypress", onKeypress);
|
|
464
769
|
render();
|
|
465
770
|
});
|