chocolatito-code 1.1.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.
@@ -1 +1,16 @@
1
- export declare function writeFile(filePath: string, content: string, cwd?: string): Promise<string>;
1
+ /**
2
+ * Escribe un archivo, entero o por partes.
3
+ *
4
+ * POR QUE EXISTE "anadir"
5
+ *
6
+ * La respuesta del modelo tiene un techo de salida. Cuando le piden una pagina
7
+ * entera, el contenido no cabe, la respuesta se corta a mitad de los argumentos
8
+ * y no se escribe nada. Se le decia "hazlo en varias llamadas mas pequenas"...
9
+ * y no habia forma de hacerlo: write_file solo sabia sobrescribir, asi que la
10
+ * segunda llamada borraba la primera. Le pediamos algo imposible y luego le
11
+ * echabamos la culpa.
12
+ *
13
+ * Con `modo: "anadir"` la instruccion pasa a ser ejecutable: primera llamada
14
+ * con el principio, las siguientes anadiendo, y el archivo sale entero.
15
+ */
16
+ export declare function writeFile(filePath: string, content: string, cwd?: string, modo?: "sobrescribir" | "anadir"): Promise<string>;
@@ -1,7 +1,22 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { binaryExtensionLabel, fileLooksBinary } from "./binary.js";
4
- export async function writeFile(filePath, content, cwd = process.cwd()) {
4
+ /**
5
+ * Escribe un archivo, entero o por partes.
6
+ *
7
+ * POR QUE EXISTE "anadir"
8
+ *
9
+ * La respuesta del modelo tiene un techo de salida. Cuando le piden una pagina
10
+ * entera, el contenido no cabe, la respuesta se corta a mitad de los argumentos
11
+ * y no se escribe nada. Se le decia "hazlo en varias llamadas mas pequenas"...
12
+ * y no habia forma de hacerlo: write_file solo sabia sobrescribir, asi que la
13
+ * segunda llamada borraba la primera. Le pediamos algo imposible y luego le
14
+ * echabamos la culpa.
15
+ *
16
+ * Con `modo: "anadir"` la instruccion pasa a ser ejecutable: primera llamada
17
+ * con el principio, las siguientes anadiendo, y el archivo sale entero.
18
+ */
19
+ export async function writeFile(filePath, content, cwd = process.cwd(), modo = "sobrescribir") {
5
20
  const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
6
21
  const dir = path.dirname(resolved);
7
22
  const isNew = !fs.existsSync(resolved);
@@ -24,6 +39,12 @@ export async function writeFile(filePath, content, cwd = process.cwd()) {
24
39
  if (!fs.existsSync(dir)) {
25
40
  fs.mkdirSync(dir, { recursive: true });
26
41
  }
42
+ if (modo === "anadir") {
43
+ fs.appendFileSync(resolved, content, "utf-8");
44
+ const total = fs.statSync(resolved).size;
45
+ return (`✔ Anadido a "${filePath}" (${content.length} caracteres nuevos, ${total} en total). ` +
46
+ `Sigue con otra llamada en modo "anadir" si falta contenido.`);
47
+ }
27
48
  fs.writeFileSync(resolved, content, "utf-8");
28
49
  const action = isNew ? "creado" : "sobreescrito";
29
50
  return `✔ Archivo "${filePath}" ${action} exitosamente (${content.length} caracteres).`;
@@ -3,6 +3,7 @@ import process from "node:process";
3
3
  import chalk from "chalk";
4
4
  import { cycleMode, modeAnnouncement } from "./modes.js";
5
5
  import { pushTyped, backspaceTyped, commitTyped } from "./typeAhead.js";
6
+ import { dibujar } from "./salida.js";
6
7
  /**
7
8
  * Interrupcion con Esc mientras el agente trabaja.
8
9
  *
@@ -112,7 +113,7 @@ export class InterruptWatcher {
112
113
  Boolean(key && key.sequence === "\x1b[Z");
113
114
  if (isShiftTab) {
114
115
  cycleMode();
115
- process.stdout.write(`\r\x1b[2K${modeAnnouncement()}\n`);
116
+ dibujar(`\r\x1b[2K${modeAnnouncement()}\n`);
116
117
  return;
117
118
  }
118
119
  // Ignorar secuencias de escape multi-byte (flechas, inicio, fin, etc.) para que no aborten
@@ -130,7 +131,7 @@ export class InterruptWatcher {
130
131
  const texto = commitTyped();
131
132
  if (texto) {
132
133
  const corto = texto.length > 70 ? `${texto.slice(0, 67)}...` : texto;
133
- process.stdout.write(`\r\x1b[2K${chalk.gray("↳ en cola:")} ${chalk.white(corto)}\n`);
134
+ dibujar(`\r\x1b[2K${chalk.gray("↳ en cola:")} ${chalk.white(corto)}\n`);
134
135
  }
135
136
  return;
136
137
  }
@@ -53,3 +53,24 @@ export interface EntradaDeTeclado {
53
53
  export declare function repararTeclado(entrada: EntradaDeTeclado): string[];
54
54
  /** Cada cuanto se comprueba. Cuatro veces por segundo se nota como instantaneo. */
55
55
  export declare const MS_ENTRE_COMPROBACIONES = 250;
56
+ /**
57
+ * Vuelve a poner el modo crudo DE VERDAD, no solo en la cuenta de Node.
58
+ *
59
+ * EL AGUJERO QUE TAPA
60
+ *
61
+ * `setRawMode(true)` no siempre llega al terminal. Por debajo, libuv guarda el
62
+ * modo que cree tener y empieza con `if (tty->mode == mode) return 0;`. Asi que
63
+ * si algo cambia el modo de la consola por fuera de Node -un proceso hijo, la
64
+ * propia shell, el terminal- Node sigue creyendo que esta en crudo, y a partir
65
+ * de ahi TODOS los `setRawMode(true)` se convierten en no-op. Incluido el que
66
+ * hace el prompt al abrirse.
67
+ *
68
+ * Se veia asi, y se reporto cuatro veces: el usuario teclea, su texto sale al
69
+ * final de la linea de ayuda -lo esta haciendo el eco del terminal, no el
70
+ * prompt- y el recuadro no se entera de nada. El vigilante tampoco reparaba
71
+ * nada, porque preguntaba a Node y Node decia que todo estaba bien.
72
+ *
73
+ * Pasar por `false` primero rompe esa cache: la segunda llamada ya ve un cambio
74
+ * de verdad y lo aplica sobre la consola.
75
+ */
76
+ export declare function reafirmarModoCrudo(entrada: EntradaDeTeclado): void;
@@ -49,7 +49,7 @@ export function repararTeclado(entrada) {
49
49
  return [];
50
50
  const reparado = [];
51
51
  if (entrada.isRaw !== true) {
52
- entrada.setRawMode(true);
52
+ reafirmarModoCrudo(entrada);
53
53
  reparado.push("modo crudo");
54
54
  }
55
55
  // `pause()` corta el flujo sin avisar a nadie: el oyente de `keypress` sigue
@@ -63,3 +63,32 @@ export function repararTeclado(entrada) {
63
63
  }
64
64
  /** Cada cuanto se comprueba. Cuatro veces por segundo se nota como instantaneo. */
65
65
  export const MS_ENTRE_COMPROBACIONES = 250;
66
+ /**
67
+ * Vuelve a poner el modo crudo DE VERDAD, no solo en la cuenta de Node.
68
+ *
69
+ * EL AGUJERO QUE TAPA
70
+ *
71
+ * `setRawMode(true)` no siempre llega al terminal. Por debajo, libuv guarda el
72
+ * modo que cree tener y empieza con `if (tty->mode == mode) return 0;`. Asi que
73
+ * si algo cambia el modo de la consola por fuera de Node -un proceso hijo, la
74
+ * propia shell, el terminal- Node sigue creyendo que esta en crudo, y a partir
75
+ * de ahi TODOS los `setRawMode(true)` se convierten en no-op. Incluido el que
76
+ * hace el prompt al abrirse.
77
+ *
78
+ * Se veia asi, y se reporto cuatro veces: el usuario teclea, su texto sale al
79
+ * final de la linea de ayuda -lo esta haciendo el eco del terminal, no el
80
+ * prompt- y el recuadro no se entera de nada. El vigilante tampoco reparaba
81
+ * nada, porque preguntaba a Node y Node decia que todo estaba bien.
82
+ *
83
+ * Pasar por `false` primero rompe esa cache: la segunda llamada ya ve un cambio
84
+ * de verdad y lo aplica sobre la consola.
85
+ */
86
+ export function reafirmarModoCrudo(entrada) {
87
+ if (!entrada || typeof entrada.setRawMode !== "function")
88
+ return;
89
+ try {
90
+ entrada.setRawMode(false);
91
+ }
92
+ catch { }
93
+ entrada.setRawMode(true);
94
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Un pegado grande no se mete entero en el recuadro.
3
+ *
4
+ * EL PROBLEMA
5
+ *
6
+ * Pegar un error de compilacion de 300 lineas metia las 300 lineas dentro del
7
+ * prompt. El recuadro crecia hasta llenar la pantalla, el marco se volvia
8
+ * inutil, y para deshacerlo habia que pulsar borrar trescientas veces. Es el
9
+ * caso normal de usar un agente: uno pega un log, una traza, un archivo entero.
10
+ *
11
+ * COMO QUEDA
12
+ *
13
+ * El texto se guarda aparte y en el recuadro solo aparece una marca corta:
14
+ *
15
+ * ❯ arregla esto [pegado #1 · 347 lineas]
16
+ *
17
+ * Al enviar, la marca se sustituye por el texto completo, asi que el modelo
18
+ * recibe exactamente lo que se pego. Y un solo borrar se lleva la marca entera:
19
+ * lo que ocupa una linea se borra como una linea.
20
+ *
21
+ * POR QUE UNA MARCA DE TEXTO Y NO UNA ESTRUCTURA APARTE
22
+ *
23
+ * Porque el buffer del prompt es una cadena y todo lo demas -el cursor, el
24
+ * historial, el recorte del marco, la busqueda de menciones- cuenta caracteres.
25
+ * Meter ahi un objeto obligaria a tocar las seis cosas. Una marca de texto se
26
+ * lleva bien con todo lo que ya funciona.
27
+ */
28
+ /** A partir de aqui, un pegado se guarda aparte en vez de llenar el recuadro. */
29
+ export declare const MINIMO_LINEAS = 5;
30
+ export declare const MINIMO_CARACTERES = 400;
31
+ /** Si merece la pena guardarlo aparte, o es lo bastante corto para verlo entero. */
32
+ export declare function esGrande(texto: string): boolean;
33
+ /** Guarda el texto y devuelve la marca que va en el recuadro. */
34
+ export declare function guardarPegado(texto: string): string;
35
+ /** Devuelve el texto de verdad, con las marcas sustituidas. Es lo que ve el modelo. */
36
+ export declare function expandirPegados(entrada: string): string;
37
+ /**
38
+ * Si justo antes del cursor termina una marca, dice donde empieza.
39
+ *
40
+ * Es lo que hace que un solo borrar se lleve el pegado entero. Sin esto habria
41
+ * que borrar la marca letra a letra, que es un poco menos absurdo que borrar
42
+ * las 347 lineas pero sigue siendo absurdo.
43
+ */
44
+ export declare function marcaQueTerminaEn(buffer: string, cursor: number): {
45
+ inicio: number;
46
+ } | null;
47
+ /** Se olvidan al cerrar el prompt: cada turno empieza con su propia cuenta. */
48
+ export declare function limpiarPegados(): void;
49
+ /** Cuantos hay guardados ahora mismo. Solo para las pruebas. */
50
+ export declare function cuantosPegados(): number;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Un pegado grande no se mete entero en el recuadro.
3
+ *
4
+ * EL PROBLEMA
5
+ *
6
+ * Pegar un error de compilacion de 300 lineas metia las 300 lineas dentro del
7
+ * prompt. El recuadro crecia hasta llenar la pantalla, el marco se volvia
8
+ * inutil, y para deshacerlo habia que pulsar borrar trescientas veces. Es el
9
+ * caso normal de usar un agente: uno pega un log, una traza, un archivo entero.
10
+ *
11
+ * COMO QUEDA
12
+ *
13
+ * El texto se guarda aparte y en el recuadro solo aparece una marca corta:
14
+ *
15
+ * ❯ arregla esto [pegado #1 · 347 lineas]
16
+ *
17
+ * Al enviar, la marca se sustituye por el texto completo, asi que el modelo
18
+ * recibe exactamente lo que se pego. Y un solo borrar se lleva la marca entera:
19
+ * lo que ocupa una linea se borra como una linea.
20
+ *
21
+ * POR QUE UNA MARCA DE TEXTO Y NO UNA ESTRUCTURA APARTE
22
+ *
23
+ * Porque el buffer del prompt es una cadena y todo lo demas -el cursor, el
24
+ * historial, el recorte del marco, la busqueda de menciones- cuenta caracteres.
25
+ * Meter ahi un objeto obligaria a tocar las seis cosas. Una marca de texto se
26
+ * lleva bien con todo lo que ya funciona.
27
+ */
28
+ /** A partir de aqui, un pegado se guarda aparte en vez de llenar el recuadro. */
29
+ export const MINIMO_LINEAS = 5;
30
+ export const MINIMO_CARACTERES = 400;
31
+ const guardados = new Map();
32
+ let siguiente = 1;
33
+ /** Reconoce una marca. El id es lo unico que hace falta para recuperar el texto. */
34
+ const MARCA = /\[pegado #(\d+) · [^\]]*\]/g;
35
+ function resumen(texto) {
36
+ const lineas = texto.split("\n").length;
37
+ if (lineas > 1)
38
+ return `${lineas.toLocaleString("es")} lineas`;
39
+ return `${texto.length.toLocaleString("es")} caracteres`;
40
+ }
41
+ /** Si merece la pena guardarlo aparte, o es lo bastante corto para verlo entero. */
42
+ export function esGrande(texto) {
43
+ return texto.length >= MINIMO_CARACTERES || texto.split("\n").length > MINIMO_LINEAS;
44
+ }
45
+ /** Guarda el texto y devuelve la marca que va en el recuadro. */
46
+ export function guardarPegado(texto) {
47
+ const id = siguiente++;
48
+ guardados.set(id, texto);
49
+ return `[pegado #${id} · ${resumen(texto)}]`;
50
+ }
51
+ /** Devuelve el texto de verdad, con las marcas sustituidas. Es lo que ve el modelo. */
52
+ export function expandirPegados(entrada) {
53
+ // Una marca de un id que ya no existe se deja tal cual: es texto que el
54
+ // usuario escribio a mano, y borrarselo seria peor que dejarlo.
55
+ return entrada.replace(MARCA, (todo, id) => guardados.get(Number(id)) ?? todo);
56
+ }
57
+ /**
58
+ * Si justo antes del cursor termina una marca, dice donde empieza.
59
+ *
60
+ * Es lo que hace que un solo borrar se lleve el pegado entero. Sin esto habria
61
+ * que borrar la marca letra a letra, que es un poco menos absurdo que borrar
62
+ * las 347 lineas pero sigue siendo absurdo.
63
+ */
64
+ export function marcaQueTerminaEn(buffer, cursor) {
65
+ if (cursor <= 0 || buffer[cursor - 1] !== "]")
66
+ return null;
67
+ const trozo = buffer.slice(0, cursor);
68
+ const inicio = trozo.lastIndexOf("[pegado #");
69
+ if (inicio < 0)
70
+ return null;
71
+ // Que lo que hay entre los corchetes sea de verdad una marca y no un
72
+ // "[pegado #" que el usuario escribiera a mano.
73
+ return /^\[pegado #\d+ · [^\]]*\]$/.test(trozo.slice(inicio)) ? { inicio } : null;
74
+ }
75
+ /** Se olvidan al cerrar el prompt: cada turno empieza con su propia cuenta. */
76
+ export function limpiarPegados() {
77
+ guardados.clear();
78
+ siguiente = 1;
79
+ }
80
+ /** Cuantos hay guardados ahora mismo. Solo para las pruebas. */
81
+ export function cuantosPegados() {
82
+ return guardados.size;
83
+ }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Imprime sin romper el prompt. Uselo en vez de `console.log` para todo lo que
3
+ * pueda llegar TARDE: conexiones en segundo plano, avisos, temporizadores.
4
+ */
5
+ export declare function imprimirSobreElPrompt(texto: string): void;
1
6
  export interface SlashCommandItem {
2
7
  name: string;
3
8
  description: string;
package/dist/ui/prompt.js CHANGED
@@ -6,8 +6,42 @@ import os from "node:os";
6
6
  import chalk from "chalk";
7
7
  import { cycleMode, describeMode, getMode } from "./modes.js";
8
8
  import { releaseKeyboard, tomarTecladoParaPrompt, devolverTecladoDelPrompt } from "./interrupt.js";
9
- import { repararTeclado, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
9
+ import { repararTeclado, reafirmarModoCrudo, MS_ENTRE_COMPROBACIONES } from "./keyboardGuard.js";
10
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
+ }
11
45
  export const SLASH_COMMANDS = [
12
46
  { name: "/mcp", description: "Ver servidores MCP conectados y sus herramientas" },
13
47
  { name: "/undo", description: "Deshacer las últimas modificaciones de archivos de la sesión" },
@@ -257,6 +291,8 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
257
291
  let isFinished = false;
258
292
  // Estado del pegado. Ver la nota PEGADO DE VARIAS LINEAS de arriba.
259
293
  let isPasting = false;
294
+ /** Lo que va llegando del pegado, antes de decidir si cabe o va como marca. */
295
+ let loPegado = "";
260
296
  let pendingSubmit = null;
261
297
  let lastWasCR = false;
262
298
  // Filas FISICAS, no elementos del array: es la unica cuenta con la que el
@@ -284,9 +320,10 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
284
320
  // Lo que quedara a medio escribir del turno anterior no es de este prompt.
285
321
  clearTyped();
286
322
  const wasRaw = process.stdin.isRaw;
287
- if (process.stdin.setRawMode) {
288
- process.stdin.setRawMode(true);
289
- }
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);
290
327
  process.stdin.resume();
291
328
  // A partir de aqui el teclado es de este prompt: un vigilante que se pare
292
329
  // tarde ya no puede devolver el modo crudo a como estaba antes del turno,
@@ -318,7 +355,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
318
355
  // Se pide el pegado entre corchetes solo si hay terminal delante; a un
319
356
  // fichero o a una tuberia esto seria basura en la salida.
320
357
  if (process.stdout.isTTY) {
321
- process.stdout.write(PASTE_START_REQUEST);
358
+ dibujar(PASTE_START_REQUEST);
322
359
  }
323
360
  function getAtMention() {
324
361
  const textBeforeCursor = inputBuffer.slice(0, cursorIndex);
@@ -401,12 +438,14 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
401
438
  out.push("\r");
402
439
  if (frame.cursorCol > 0)
403
440
  out.push(`\x1b[${frame.cursorCol}C`);
404
- process.stdout.write(out.join(""));
441
+ dibujar(out.join(""));
405
442
  hasRendered = true;
406
443
  lastCursorRow = frame.cursorRow;
407
444
  }
408
445
  function cleanup() {
409
446
  isFinished = true;
447
+ limpiarPegados();
448
+ escribirEncimaDelPrompt = null;
410
449
  clearInterval(vigilanteTeclado);
411
450
  devolverTecladoDelPrompt();
412
451
  if (pendingSubmit) {
@@ -418,7 +457,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
418
457
  process.stdin.setRawMode(wasRaw || false);
419
458
  }
420
459
  if (process.stdout.isTTY) {
421
- process.stdout.write(PASTE_STOP_REQUEST);
460
+ dibujar(PASTE_STOP_REQUEST);
422
461
  }
423
462
  }
424
463
  /** Mete texto donde esta el cursor. Un solo sitio para no repetir el corte del buffer. */
@@ -466,7 +505,7 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
466
505
  out.push("\r\x1b[0J");
467
506
  }
468
507
  out.push(`${bar}\n${symbolStr}${chalk.bold.white(submittedText)}\n${bar}\n\n`);
469
- process.stdout.write(out.join(""));
508
+ dibujar(out.join(""));
470
509
  hasRendered = false;
471
510
  lastCursorRow = 0;
472
511
  }
@@ -474,10 +513,14 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
474
513
  if (isFinished)
475
514
  return;
476
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.
477
518
  appendHistory(inputBuffer);
519
+ // Pero el modelo recibe el texto entero, que es lo que el usuario pego.
520
+ const completo = expandirPegados(inputBuffer);
478
521
  cleanup();
479
522
  finalizePromptOnSubmit(inputBuffer);
480
- resolve(inputBuffer);
523
+ resolve(completo);
481
524
  }
482
525
  /**
483
526
  * Apunta el envio para el final del tick. Todo el chunk de stdin se emite
@@ -505,10 +548,17 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
505
548
  const sequence = (key && key.sequence) || str || "";
506
549
  if ((key && key.name === "paste-start") || sequence === PASTE_START) {
507
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 = "";
508
554
  return;
509
555
  }
510
556
  if ((key && key.name === "paste-end") || sequence === PASTE_END) {
511
557
  isPasting = false;
558
+ if (loPegado) {
559
+ insertText(esGrande(loPegado) ? guardarPegado(loPegado) : loPegado);
560
+ loPegado = "";
561
+ }
512
562
  render();
513
563
  return;
514
564
  }
@@ -526,10 +576,9 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
526
576
  const items = getDropdownItems();
527
577
  // Enter key
528
578
  if (isEnter) {
529
- // Dentro de un pegado, un salto de linea es texto y no envia nada.
579
+ // Dentro de un pegado, el salto va al buffer aparte, no al recuadro.
530
580
  if (isPasting) {
531
- insertText("\n");
532
- render();
581
+ loPegado += "\n";
533
582
  return;
534
583
  }
535
584
  // Segundo Enter del mismo tick: linea en blanco dentro del pegado.
@@ -629,8 +678,12 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
629
678
  // Backspace
630
679
  if (key && (key.name === "backspace" || (key.ctrl && key.name === "h"))) {
631
680
  if (cursorIndex > 0) {
632
- inputBuffer = inputBuffer.slice(0, cursorIndex - 1) + inputBuffer.slice(cursorIndex);
633
- cursorIndex--;
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;
634
687
  selectedIndex = 0;
635
688
  render();
636
689
  }
@@ -674,12 +727,44 @@ export async function askInteractivePrompt(promptSymbol = "❯", cwd = process.c
674
727
  if (str.charCodeAt(0) === 0x1b)
675
728
  return;
676
729
  const cleanStr = str.replace(/[\r\n]/g, "");
677
- if (cleanStr.length > 0) {
678
- insertText(cleanStr);
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));
679
743
  render();
744
+ return;
680
745
  }
746
+ insertText(cleanStr);
747
+ render();
681
748
  }
682
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
+ };
683
768
  process.stdin.on("keypress", onKeypress);
684
769
  render();
685
770
  });
@@ -0,0 +1,10 @@
1
+ /** Pinta encima del terminal. En produccion es `process.stdout.write` y nada mas. */
2
+ export declare function dibujar(texto: string): void;
3
+ /**
4
+ * Desvia el dibujo. Con `null` vuelve al terminal.
5
+ *
6
+ * Solo lo usan las pruebas, y siempre devolviendolo en un `finally`: si se
7
+ * queda desviado, las pruebas que vengan detras dibujan en el vacio sin
8
+ * enterarse.
9
+ */
10
+ export declare function redirigirDibujo(destino: ((texto: string) => void) | null): void;
@@ -0,0 +1,50 @@
1
+ import process from "node:process";
2
+ /**
3
+ * Por donde sale lo que se pinta encima del terminal.
4
+ *
5
+ * Aqui va SOLO el dibujo que ocupa el terminal a la fuerza: el marco del
6
+ * prompt, los avisos que se escriben pisando la linea actual con \r y \x1b[2K.
7
+ * Un `console.log` normal no pasa por aqui y no tiene por que.
8
+ *
9
+ * POR QUE EXISTE
10
+ *
11
+ * `npm publish` corre `prepublishOnly` -> `npm test`, y las pruebas del teclado
12
+ * abren el prompt de verdad. Con un terminal delante, el prompt mueve el
13
+ * cursor, borra lineas y repinta su marco encima de la sesion de quien esta
14
+ * publicando. En un `npm publish` real se vio esto:
15
+ *
16
+ * ❯ hola mundo
17
+ * ↳ en cola: y ahora los tests
18
+ *
19
+ * El terminal dejaba de ser suyo y la publicacion no arrancaba. Hubo que
20
+ * publicar con --ignore-scripts, saltandose las 488 pruebas; una red de
21
+ * seguridad que hay que apagar para trabajar no es una red.
22
+ *
23
+ * POR QUE NO SE SILENCIA `process.stdout` DESDE FUERA
24
+ *
25
+ * Se intento de las dos formas y las dos rompen algo peor. `node --test`
26
+ * instrumenta process.stdout para asociar cada linea a la prueba que la
27
+ * produjo: al sustituir el objeto -o solo su `write`- las pruebas se ejecutan
28
+ * pero desaparecen del informe, sin fallar y sin dejar rastro. Medido: de 6
29
+ * pruebas pasaban a contarse 2, en verde. Una prueba que se evapora es peor que
30
+ * una que falla.
31
+ *
32
+ * Con un conducto propio, process.stdout no se toca nunca.
33
+ */
34
+ let conducto = (texto) => {
35
+ process.stdout.write(texto);
36
+ };
37
+ /** Pinta encima del terminal. En produccion es `process.stdout.write` y nada mas. */
38
+ export function dibujar(texto) {
39
+ conducto(texto);
40
+ }
41
+ /**
42
+ * Desvia el dibujo. Con `null` vuelve al terminal.
43
+ *
44
+ * Solo lo usan las pruebas, y siempre devolviendolo en un `finally`: si se
45
+ * queda desviado, las pruebas que vengan detras dibujan en el vacio sin
46
+ * enterarse.
47
+ */
48
+ export function redirigirDibujo(destino) {
49
+ conducto = destino ?? ((texto) => { process.stdout.write(texto); });
50
+ }