chocolatito-code 1.6.6 → 1.6.8

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.
Files changed (76) hide show
  1. package/README.md +437 -341
  2. package/dist/agent/context.d.ts +6 -0
  3. package/dist/agent/context.js +69 -2
  4. package/dist/agent/loop.js +8 -0
  5. package/dist/agent/toolGate.js +13 -7
  6. package/dist/agent/verifier.js +2 -1
  7. package/dist/config/permissions.d.ts +17 -0
  8. package/dist/config/permissions.js +47 -7
  9. package/dist/config/plataforma.d.ts +62 -0
  10. package/dist/config/plataforma.js +109 -0
  11. package/dist/config/updater.js +4 -1
  12. package/dist/hooks/manager.js +2 -1
  13. package/dist/index.js +164 -39
  14. package/dist/memory/manager.d.ts +15 -5
  15. package/dist/memory/manager.js +146 -29
  16. package/dist/servidor/captura.d.ts +36 -0
  17. package/dist/servidor/captura.js +74 -0
  18. package/dist/servidor/pagina.d.ts +27 -0
  19. package/dist/servidor/pagina.js +268 -0
  20. package/dist/servidor/puente.d.ts +34 -0
  21. package/dist/servidor/puente.js +115 -0
  22. package/dist/servidor/servidor.d.ts +61 -0
  23. package/dist/servidor/servidor.js +249 -0
  24. package/dist/sessions/manager.d.ts +8 -0
  25. package/dist/sessions/manager.js +44 -0
  26. package/dist/sessions/resume.d.ts +12 -0
  27. package/dist/sessions/resume.js +10 -0
  28. package/dist/tools/backgroundTask.d.ts +64 -0
  29. package/dist/tools/backgroundTask.js +264 -0
  30. package/dist/tools/browserExtension.d.ts +2 -1
  31. package/dist/tools/browserExtension.js +48 -0
  32. package/dist/tools/computerUse.js +51 -9
  33. package/dist/tools/definitions.js +93 -1
  34. package/dist/tools/gitAudit.d.ts +41 -0
  35. package/dist/tools/gitAudit.js +282 -0
  36. package/dist/tools/runCommand.js +5 -2
  37. package/dist/tools/runner.js +36 -2
  38. package/dist/tools/safety.d.ts +1 -0
  39. package/dist/tools/safety.js +3 -0
  40. package/dist/tools/todoTool.d.ts +2 -0
  41. package/dist/tools/todoTool.js +29 -0
  42. package/dist/tools/toolDefsComputer.js +9 -0
  43. package/dist/tools/win/hostScript.js +7 -2
  44. package/dist/ui/comandos.js +2 -0
  45. package/dist/ui/ink/App.d.ts +0 -23
  46. package/dist/ui/ink/App.js +86 -57
  47. package/dist/ui/ink/Prompt.d.ts +16 -6
  48. package/dist/ui/ink/Prompt.js +37 -62
  49. package/dist/ui/ink/montarApp.d.ts +34 -0
  50. package/dist/ui/ink/montarApp.js +93 -1
  51. package/dist/ui/ink/prestamo.d.ts +63 -0
  52. package/dist/ui/ink/prestamo.js +92 -0
  53. package/dist/ui/ink/teclado.d.ts +48 -0
  54. package/dist/ui/ink/teclado.js +96 -0
  55. package/dist/ui/ink/transcripcion.d.ts +114 -0
  56. package/dist/ui/ink/transcripcion.js +180 -0
  57. package/dist/ui/interrupt.js +4 -1
  58. package/dist/ui/loopPrompt.d.ts +6 -7
  59. package/dist/ui/loopPrompt.js +14 -1
  60. package/dist/ui/marco.d.ts +3 -0
  61. package/dist/ui/marco.js +22 -3
  62. package/dist/ui/pantalla.d.ts +45 -17
  63. package/dist/ui/pantalla.js +150 -92
  64. package/dist/ui/permissionPrompt.d.ts +19 -17
  65. package/dist/ui/permissionPrompt.js +44 -1
  66. package/dist/ui/pieFijo.d.ts +1 -1
  67. package/dist/ui/pieFijo.js +27 -5
  68. package/dist/ui/renderer.js +8 -0
  69. package/dist/ui/selector.d.ts +6 -4
  70. package/dist/ui/selector.js +14 -1
  71. package/extension/background.js +80 -0
  72. package/extension/content.js +106 -0
  73. package/extension/manifest.json +3 -2
  74. package/package.json +3 -2
  75. package/dist/ui/historial.d.ts +0 -49
  76. package/dist/ui/historial.js +0 -92
@@ -25,6 +25,7 @@ import { globalUndoManager } from "../agent/undoManager.js";
25
25
  import { validateFileSyntax } from "../agent/syntaxValidator.js";
26
26
  import { fileTracker } from "../agent/fileTracker.js";
27
27
  import { mcpManager } from "../mcp/manager.js";
28
+ import { taskManager } from "./backgroundTask.js";
28
29
  import fs from "node:fs";
29
30
  import path from "node:path";
30
31
  /**
@@ -82,10 +83,12 @@ export async function executeToolCall(name, args, cwd = process.cwd(), apiKey, m
82
83
  break;
83
84
  }
84
85
  case "save_memory":
85
- rawResult = memoryManager.saveMemory(args.topic, args.fact);
86
+ memoryManager.setCwd(cwd);
87
+ rawResult = memoryManager.saveMemory(args.topic, args.fact, args.scope);
86
88
  break;
87
89
  case "read_memory":
88
- rawResult = memoryManager.readMemory(args.topic);
90
+ memoryManager.setCwd(cwd);
91
+ rawResult = memoryManager.readMemory(args.topic, args.scope);
89
92
  break;
90
93
  case "spawn_agent": {
91
94
  if (!apiKey) {
@@ -201,6 +204,37 @@ export async function executeToolCall(name, args, cwd = process.cwd(), apiKey, m
201
204
  case "run_command":
202
205
  rawResult = await runCommand(args.command, cwd, args.timeout);
203
206
  break;
207
+ case "start_background_task": {
208
+ const res = await taskManager.startTask(args.command, args.cwd || cwd, args.description);
209
+ rawResult = `Tarea iniciada en segundo plano:\n- ID: ${res.taskId}\n- PID: ${res.pid}\n- Estado: ${res.status}\n- Log: ${res.logPath}`;
210
+ break;
211
+ }
212
+ case "read_task_output": {
213
+ const res = taskManager.readTaskOutput(args.taskId, args.lines, args.offset);
214
+ if (res.status === "not_found") {
215
+ rawResult = res.output;
216
+ }
217
+ else {
218
+ rawResult = `[Tarea ${res.taskId}] Estado: ${res.status} (Exit Code: ${res.exitCode !== null ? res.exitCode : "N/A"})\nTotal de líneas: ${res.totalLines}\n--- Salida ---\n${res.output || "(sin salida aún)"}`;
219
+ }
220
+ break;
221
+ }
222
+ case "list_background_tasks": {
223
+ const tasks = taskManager.listTasks();
224
+ if (tasks.length === 0) {
225
+ rawResult = "No hay tareas en segundo plano registradas.";
226
+ }
227
+ else {
228
+ const lines = tasks.map((t) => `- [${t.taskId}] PID ${t.pid} | ${t.status.toUpperCase()} | Ejecución: ${t.runtimeSeconds}s | Comando: "${t.command}"${t.description ? ` (${t.description})` : ""}${t.exitCode !== null ? ` | Exit code: ${t.exitCode}` : ""}`);
229
+ rawResult = `Tareas en segundo plano (${tasks.length}):\n${lines.join("\n")}`;
230
+ }
231
+ break;
232
+ }
233
+ case "stop_background_task": {
234
+ const res = await taskManager.stopTask(args.taskId, args.force);
235
+ rawResult = res.message;
236
+ break;
237
+ }
204
238
  case "list_dir":
205
239
  rawResult = await listDir(args.dirPath || ".", cwd);
206
240
  break;
@@ -7,6 +7,7 @@
7
7
  * estado. En cambio dos escrituras seguidas SI pueden pisarse, y el orden
8
8
  * importa, asi que esas van una detras de otra.
9
9
  */
10
+ export declare const READ_ONLY_TOOLS: Set<string>;
10
11
  export declare function isReadOnly(toolName: string): boolean;
11
12
  /**
12
13
  * Un lote solo se paraleliza si TODAS sus llamadas son de solo lectura y hay
@@ -18,7 +18,10 @@ const READ_ONLY = new Set([
18
18
  "web_search",
19
19
  "read_memory",
20
20
  "use_skill",
21
+ "read_task_output",
22
+ "list_background_tasks",
21
23
  ]);
24
+ export const READ_ONLY_TOOLS = READ_ONLY;
22
25
  export function isReadOnly(toolName) {
23
26
  return READ_ONLY.has(toolName);
24
27
  }
@@ -5,4 +5,6 @@ export interface TaskItem {
5
5
  }
6
6
  export declare function setSessionTasks(tasks: TaskItem[]): string;
7
7
  export declare function getSessionTasks(): TaskItem[];
8
+ export declare function renderTaskWidget(tasks?: TaskItem[]): string | null;
9
+ export declare function sincronizarTareasConPie(tasks?: TaskItem[]): void;
8
10
  export declare function renderTaskList(tasks: TaskItem[]): string;
@@ -1,12 +1,41 @@
1
1
  import chalk from "chalk";
2
+ import { fijarSeccion, quitarSeccion } from "../ui/pieFijo.js";
2
3
  let sessionTasks = [];
3
4
  export function setSessionTasks(tasks) {
4
5
  sessionTasks = tasks;
6
+ sincronizarTareasConPie(sessionTasks);
5
7
  return renderTaskList(sessionTasks);
6
8
  }
7
9
  export function getSessionTasks() {
8
10
  return sessionTasks;
9
11
  }
12
+ export function renderTaskWidget(tasks) {
13
+ const list = tasks !== undefined ? tasks : sessionTasks;
14
+ if (!list || list.length === 0)
15
+ return null;
16
+ const total = list.length;
17
+ const completed = list.filter((t) => t.status === "completed").length;
18
+ const inProgress = list.find((t) => t.status === "in_progress");
19
+ return (chalk.hex("#D97757")("⎿") +
20
+ " " +
21
+ chalk.green("✔") +
22
+ ` ${completed}/${total} tareas` +
23
+ (inProgress
24
+ ? chalk.gray(" · ") + chalk.hex("#D97757")("En curso: ") + chalk.bold.white(inProgress.text)
25
+ : completed === total
26
+ ? chalk.green(" · Completado")
27
+ : ""));
28
+ }
29
+ export function sincronizarTareasConPie(tasks) {
30
+ const list = tasks !== undefined ? tasks : sessionTasks;
31
+ const widget = renderTaskWidget(list);
32
+ if (widget) {
33
+ fijarSeccion("tareas", [widget]);
34
+ }
35
+ else {
36
+ quitarSeccion("tareas");
37
+ }
38
+ }
10
39
  export function renderTaskList(tasks) {
11
40
  if (tasks.length === 0)
12
41
  return "No hay tareas registradas.";
@@ -111,6 +111,8 @@ export const COMPUTER_USE_TOOL = {
111
111
  "scroll",
112
112
  "type",
113
113
  "key",
114
+ "hotkey",
115
+ "press",
114
116
  "wait",
115
117
  "wait_change",
116
118
  "cursor_position",
@@ -183,11 +185,14 @@ export const CHROME_TOOL = {
183
185
  "status", "tabs", "open", "select_tab", "navigate", "snapshot",
184
186
  "click", "type", "press", "get_text", "eval", "wait_for",
185
187
  "screenshot", "notice", "close_tab", "done",
188
+ "console_logs", "network_errors",
186
189
  ],
187
190
  description: "status: comprueba que la extension esta conectada. tabs: lista las pestanas del usuario. " +
188
191
  "open: abre una pestana nueva SIN quitarle la pantalla al usuario. select_tab: elige una ya abierta. " +
189
192
  "snapshot: LEE la pagina y numera los elementos. click/type: actuan sobre un ref. " +
190
193
  "wait_for: espera un texto o unos ms. screenshot: captura la pestana aunque no este visible y la describe. " +
194
+ "console_logs: obtiene errores y advertencias de consola y excepciones no controladas de la pagina. " +
195
+ "network_errors: obtiene peticiones de red HTTP fallidas (4xx/5xx o errores de conexion). " +
191
196
  "notice: cambia el texto del aviso flotante. close_tab: cierra una pestaña que abriste. done: cierra el trabajo y quita el aviso.",
192
197
  },
193
198
  url: { type: "string", description: "URL para open o navigate." },
@@ -212,6 +217,10 @@ export const CHROME_TOOL = {
212
217
  analyze: { type: "boolean", description: "false para no gastar tokens de vision." },
213
218
  outputPath: { type: "string", description: "Ruta donde guardar la captura." },
214
219
  note: { type: "string", description: "Texto inicial del aviso flotante al abrir la pestana." },
220
+ clear: {
221
+ type: "boolean",
222
+ description: "true para limpiar el buffer tras consultar (en console_logs o network_errors).",
223
+ },
215
224
  },
216
225
  required: ["action"],
217
226
  },
@@ -283,9 +283,14 @@ public static class Choco
283
283
  double scale = 1.0;
284
284
  int longEdge = Math.Max(w, h);
285
285
  if (maxLongEdge > 0 && longEdge > maxLongEdge) scale = (double)maxLongEdge / (double)longEdge;
286
+ ImageFormat fmt = ImageFormat.Png;
287
+ if (path.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase))
288
+ {
289
+ fmt = ImageFormat.Jpeg;
290
+ }
286
291
  if (scale >= 0.999)
287
292
  {
288
- bmp.Save(path, ImageFormat.Png);
293
+ bmp.Save(path, fmt);
289
294
  return 1.0;
290
295
  }
291
296
  int nw = (int)Math.Round(w * scale), nh = (int)Math.Round(h * scale);
@@ -294,7 +299,7 @@ public static class Choco
294
299
  {
295
300
  g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
296
301
  g.DrawImage(bmp, 0, 0, nw, nh);
297
- dst.Save(path, ImageFormat.Png);
302
+ dst.Save(path, fmt);
298
303
  }
299
304
  return scale;
300
305
  }
@@ -35,6 +35,8 @@ export const SLASH_COMMANDS = [
35
35
  { name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
36
36
  { name: "/compact", description: "Comprimir contexto y optimizar memoria de la sesión" },
37
37
  { name: "/diff", description: "Ver diferencias de código (Git diff) del proyecto" },
38
+ { name: "/audit", description: "Auditar git diff en busca de secretos, logs residuales y sugerir commit", args: "[review]" },
39
+ { name: "/review", description: "Revisar cambios pendientes de git (alias de /audit)" },
38
40
  { name: "/commit", description: "Crear un commit en Git con mensaje semántico", args: "<mensaje>" },
39
41
  { name: "/cd", description: "Cambiar el directorio de trabajo activo", args: "<ruta>" },
40
42
  { name: "/uso", description: "Ver cuánto llevas consumido de tu cuenta" },
@@ -1,25 +1,2 @@
1
1
  import React from "react";
2
- /**
3
- * EL ÁRBOL ÚNICO
4
- *
5
- * POR QUE EXISTE
6
- *
7
- * Antes Ink solo era dueño del prompt: se montaba para pedir una orden y se
8
- * desmontaba al enviarla. Mientras el agente trabajaba, el terminal volvía a ser
9
- * de siempre y la barra de abajo la pintaba `pieFijo` borrando y repintando a
10
- * mano.
11
- *
12
- * Eso son DOS dueños del mismo trozo de pantalla turnándose, y de ahí salían
13
- * casi todas las quejas: la barra que sube con el scroll, el hueco que aparece,
14
- * el marco que se descuadra. Cada arreglo era coordinar mejor a los dos dueños.
15
- *
16
- * Aquí hay uno solo, y vive toda la sesión. Lo que sale del agente entra en el
17
- * historial (historial.ts) y se pinta arriba; abajo va SIEMPRE la barra, sea la
18
- * de escribir o la de trabajar. Nadie tiene que ponerse de acuerdo con nadie.
19
- *
20
- * Y POR ESO LA BARRA SE QUEDA QUIETA
21
- *
22
- * No porque se repinte rápido, sino porque es la última fila de un árbol que
23
- * ocupa la ventana entera. No hay nada que pueda moverla, ni la rueda del ratón.
24
- */
25
2
  export declare function App(): React.ReactElement;
@@ -1,79 +1,108 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
- import { Box, Text, useStdout } from "ink";
3
+ import { Box, Static, Text, useApp, useStdout } from "ink";
4
4
  import { Prompt } from "./Prompt.js";
5
- import { alEscribir, tope, ventana } from "../historial.js";
5
+ import { colgando, pendientes, recortarCola } from "./transcripcion.js";
6
+ import { alCambiar } from "./transcripcion.js";
6
7
  import { cuandoCambieElPie, lineasDelPie } from "../pieFijo.js";
7
8
  import { alCambiarElTurno, estadoDelTurno } from "./control.js";
9
+ import { registrarPrestamo } from "./prestamo.js";
10
+ import { useTecladoSiempreNuestro } from "./teclado.js";
8
11
  /**
9
- * EL ÁRBOL ÚNICO
12
+ * EL ÁRBOL ÚNICO, EN LA PANTALLA DE SIEMPRE
10
13
  *
11
- * POR QUE EXISTE
14
+ * DE DÓNDE VIENE ESTE ARCHIVO
12
15
  *
13
- * Antes Ink solo era dueño del prompt: se montaba para pedir una orden y se
14
- * desmontaba al enviarla. Mientras el agente trabajaba, el terminal volvía a ser
15
- * de siempre y la barra de abajo la pintaba `pieFijo` borrando y repintando a
16
- * mano.
16
+ * La versión anterior tomaba la pantalla alternativa —la de vim o htop— para
17
+ * clavar la barra de abajo. Funcionaba en el emulador y se publicó rota dos
18
+ * veces. Y sobre todo: no era lo que hace Claude Code, que era lo que se pedía.
17
19
  *
18
- * Eso son DOS dueños del mismo trozo de pantalla turnándose, y de ahí salían
19
- * casi todas las quejas: la barra que sube con el scroll, el hueco que aparece,
20
- * el marco que se descuadra. Cada arreglo era coordinar mejor a los dos dueños.
20
+ * Claude Code renderiza en el buffer NORMAL. La conversación se emite una sola
21
+ * vez con `<Static>` y pasa al historial del terminal; debajo queda una zona que
22
+ * se repinta con el spinner y la caja de escribir. Por eso allí la rueda del
23
+ * ratón funciona y se puede releer la sesión entera.
21
24
  *
22
- * Aquí hay uno solo, y vive toda la sesión. Lo que sale del agente entra en el
23
- * historial (historial.ts) y se pinta arriba; abajo va SIEMPRE la barra, sea la
24
- * de escribir o la de trabajar. Nadie tiene que ponerse de acuerdo con nadie.
25
+ * LO QUE ESO SIGNIFICA, DICHO CLARO
25
26
  *
26
- * Y POR ESO LA BARRA SE QUEDA QUIETA
27
+ * La barra está siempre pegada al final de lo que ha salido, y cualquier línea
28
+ * nueva te devuelve ahí. Pero si subes con la rueda, la barra sube con el resto:
29
+ * es texto del terminal, no una capa encima. En Claude Code pasa exactamente lo
30
+ * mismo. Clavarla de verdad solo se consigue tomando la pantalla entera, y el
31
+ * precio es quedarse sin historial ni rueda del ratón.
27
32
  *
28
- * No porque se repinte rápido, sino porque es la última fila de un árbol que
29
- * ocupa la ventana entera. No hay nada que pueda moverla, ni la rueda del ratón.
33
+ * LAS TRES ZONAS
34
+ *
35
+ * <Static> la conversación, escrita una vez y para siempre
36
+ * cola el trozo de línea que el modelo está escribiendo ahora mismo
37
+ * barra el spinner, o la caja de escribir
38
+ *
39
+ * Las dos últimas se repintan; la primera no puede.
40
+ */
41
+ /** Filas que se reservan para la caja cuando el prompt está puesto. */
42
+ const ALTO_DE_LA_CAJA = 6;
43
+ /**
44
+ * UNA LÍNEA VACÍA TIENE QUE OCUPAR SU FILA
45
+ *
46
+ * Un `<Text>` sin contenido mide cero y desaparece. Y las líneas en blanco de la
47
+ * respuesta no son adorno: son lo que separa un párrafo del siguiente, y lo que
48
+ * deja aire entre el mensaje que mandaste y lo que contesta el modelo.
49
+ *
50
+ * Sin esto, la conversación sale apelmazada: los párrafos pegados unos a otros y
51
+ * la lista de herramientas encima del texto sin un respiro. Se vio al mirar la
52
+ * pantalla emulada; ninguna prueba lo cazaba, porque todas comprobaban qué
53
+ * líneas salen y ninguna cuántas.
30
54
  */
55
+ function conCuerpo(texto) {
56
+ return texto.length > 0 ? texto : " ";
57
+ }
31
58
  export function App() {
32
59
  const { stdout } = useStdout();
33
- // UNA FILA MENOS QUE LA VENTANA, Y ES LA CLAVE
34
- //
35
- // Ink escribe su arbol y detras un salto de linea. Si el arbol mide justo lo
36
- // que la ventana, ese salto la desborda: el terminal desplaza una fila, y como
37
- // Ink repinta subiendo el numero de filas que escribio, a partir de ahi borra
38
- // una fila que ya no es la suya. En la pantalla alternativa, que no tiene
39
- // historial al que recurrir, eso degenera en pantalla en negro.
40
- //
41
- // Es lo que se publico roto en la 1.6.0: "esta fallando, no sale nada". Las
42
- // pruebas no lo cazan porque el terminal de mentira de la libreria no desplaza.
43
- const filas = Math.max(6, (stdout?.rows || 24) - 1);
60
+ const { suspendTerminal } = useApp();
44
61
  const [, setRedibujar] = useState(0);
45
- const [subido, setSubido] = useState(0);
46
62
  const [turno, setTurno] = useState(estadoDelTurno());
47
63
  const repintar = () => setRedibujar((n) => n + 1);
48
- // Tres fuentes de cambio, todas fuera de React: lo que escribe el agente, lo
49
- // que publica el spinner, y el paso de "esperando orden" a "trabajando".
50
- useEffect(() => alEscribir(repintar), []);
64
+ // Tres fuentes de cambio, las tres fuera de React: lo que escribe el agente,
65
+ // lo que publica el spinner, y el paso de "esperando orden" a "trabajando".
66
+ useEffect(() => alCambiar(repintar), []);
51
67
  useEffect(() => cuandoCambieElPie(repintar), []);
52
68
  useEffect(() => alCambiarElTurno((nuevo) => setTurno(nuevo)), []);
53
- // Al empezar un turno se vuelve al final: lo interesante es lo que va a salir,
54
- // no donde estabas mirando.
69
+ // El diálogo de permisos y los menús necesitan el teclado para ellos solos, y
70
+ // se abren con el árbol vivo. Aquí se deja apuntado el préstamo que Ink ya
71
+ // trae para eso; ver ink/prestamo.ts.
55
72
  useEffect(() => {
56
- if (turno.esperando)
57
- return;
58
- setSubido(0);
59
- }, [turno.esperando]);
60
- const barraDeTrabajo = turno.esperando ? [] : lineasDelPie();
61
- const altoBarra = turno.esperando ? 0 : barraDeTrabajo.length;
62
- const altoAviso = subido > 0 ? 1 : 0;
63
- // Cuando espera orden, el hueco de abajo lo ocupa el prompt y su alto lo
64
- // decide él; se le pasa cuánto sitio hay para lo de arriba.
65
- const altoDelTexto = Math.max(1, filas - altoBarra - altoAviso - (turno.esperando ? 5 : 0));
66
- const visible = ventana(altoDelTexto, subido);
67
- const huecos = Math.max(0, altoDelTexto - visible.length);
68
- return (_jsxs(Box, { flexDirection: "column", height: filas, children: [Array.from({ length: huecos }, (_, i) => (_jsx(Text, { children: " " }, `hueco-${i}`))), visible.map((linea, i) => (_jsx(Text, { children: linea }, `h-${i}`))), subido > 0 && (_jsx(Text, { dimColor: true, children: ` ↑ ${subido} lineas mas arriba · Av Pag para volver al final` })), turno.esperando ? (_jsx(Prompt, { alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [], sinHistorial: true, alDesplazar: (direccion) => setSubido((n) => {
69
- // El salto sale del alto SIN el aviso de "estas mirando atras".
70
- // Si se contara, subir y bajar usarian bases distintas -el aviso
71
- // solo existe cuando ya has subido- y bajar no deshacia lo que
72
- // acababas de subir: el scroll derivaba solo.
73
- const base = Math.max(1, filas - altoBarra - 5);
74
- const salto = Math.max(1, Math.floor(base / 2));
75
- return direccion > 0
76
- ? Math.min(tope(base), n + salto)
77
- : Math.max(0, n - salto);
78
- }) })) : (barraDeTrabajo.map((linea, i) => _jsx(Text, { children: linea }, `t-${i}`)))] }));
73
+ registrarPrestamo(suspendTerminal);
74
+ return () => registrarPrestamo(null);
75
+ }, [suspendTerminal]);
76
+ // El teclado tiene que seguir siendo de la caja aunque un diálogo, un menú o
77
+ // un proceso hijo lo suelten por el camino. Ver ink/teclado.ts: es el fallo
78
+ // del "no puedo escribir hasta que pulso Enter".
79
+ useTecladoSiempreNuestro(turno.esperando);
80
+ const cols = Math.max(20, stdout?.columns || 80);
81
+ const filas = Math.max(6, stdout?.rows || 24);
82
+ const permanentes = pendientes();
83
+ const barra = turno.esperando ? [] : lineasDelPie();
84
+ // EL TECHO DE LA ZONA DE ABAJO, Y POR QUÉ SE MIDE TAN DE CERCA
85
+ //
86
+ // En Windows basta con que el bloque dinámico MIDA lo que la ventana —no que
87
+ // se pase, que lo iguale— para que Ink cambie de repintado por diferencias a
88
+ // `clearTerminal` + reimprimir toda la conversación acumulada, en cada
89
+ // fotograma (ink.js:100-102). Con el spinner a 30 por segundo eso es la
90
+ // pantalla entera parpadeando y el coste creciendo con la sesión.
91
+ //
92
+ // Así que la zona de abajo se queda SIEMPRE por debajo de las filas de la
93
+ // ventana. Una fila de margen, y quien se pasa es la cola, que se recorta.
94
+ const techo = Math.max(1, filas - 1);
95
+ const sitioParaLaCola = Math.max(0, techo - (turno.esperando ? ALTO_DE_LA_CAJA : barra.length));
96
+ const cola = recortarCola(colgando(), cols, sitioParaLaCola);
97
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: permanentes, children: (linea) => _jsx(Text, { children: conCuerpo(linea.texto) }, linea.id) }), _jsxs(Box, { flexDirection: "column", children: [cola.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, `cola-${i}`))), turno.esperando ? (_jsx(Prompt, { alEnviar: turno.responder, alCancelar: () => turno.responder("/exit"), cwd: turno.cwd, promptSymbol: turno.promptSymbol, avisos: [],
98
+ // NO SE DESMONTA EL ÁRBOL AL ENVIAR
99
+ //
100
+ // El prompt nació montándose y desmontándose en cada vuelta, y para
101
+ // eso llamaba a `exit()` de Ink al enviar. Dentro del árbol único ese
102
+ // `exit()` no cierra un prompt: cierra la aplicación ENTERA.
103
+ //
104
+ // Es el fallo que se publicó como "escribo y se cierra": la primera
105
+ // vez que pulsabas Enter el árbol moría, ya no quedaba nadie
106
+ // pintando, y la promesa que esperaba la orden no se resolvía jamás.
107
+ cerrarAlEnviar: false, alto: Math.max(3, techo - cola.length) })) : (barra.map((linea, i) => _jsx(Text, { children: conCuerpo(linea) }, `t-${i}`)))] })] }));
79
108
  }
@@ -39,12 +39,22 @@ export interface PromptProps {
39
39
  /** Mensajes que llegaron tarde y hay que dejar por encima del marco. */
40
40
  avisos: string[];
41
41
  /**
42
- * El historial lo pinta otro (el arbol de App). Aqui solo la barra.
42
+ * ¿Al enviar hay que cerrar el árbol de Ink?
43
43
  *
44
- * Sin esto se pintaria dos veces: una desde App y otra desde aqui.
44
+ * cuando el prompt ES el árbol —se monta para pedir una orden y se
45
+ * desmonta al recibirla— y NO cuando vive dentro del árbol único de App: allí
46
+ * `exit()` no cierra el prompt, cierra la aplicación entera y deja la promesa
47
+ * de la orden colgada para siempre. Ese fue el "escribo y se cierra".
45
48
  */
46
- sinHistorial?: boolean;
47
- /** Re Pag / Av Pag: +1 sube, -1 baja. El salto lo decide quien recibe. */
48
- alDesplazar?: (direccion: number) => void;
49
+ cerrarAlEnviar?: boolean;
50
+ /**
51
+ * Filas de las que puede disponer el marco.
52
+ *
53
+ * App se lo dice porque ahí arriba hay más cosas —el trozo de línea que el
54
+ * modelo está escribiendo— y el bloque de abajo entero tiene que quedarse por
55
+ * debajo del alto de la ventana. Sin esto, el marco se mide contra la ventana
56
+ * como si estuviera solo.
57
+ */
58
+ alto?: number;
49
59
  }
50
- export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHistorial, alDesplazar, }: PromptProps): React.ReactElement;
60
+ export declare function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar, alto, }: PromptProps): React.ReactElement;
@@ -1,15 +1,27 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { useMemo, useRef, useState } from "react";
3
3
  import { Box, Text, useApp, useInput, useStdout } from "ink";
4
4
  import { buildPromptFrame } from "../marco.js";
5
5
  import { cycleMode } from "../modes.js";
6
6
  import { aplicarOpcion, archivosDelProyecto, leerHistorial, mencionEnCurso, opcionesDelDesplegable, } from "../entrada.js";
7
7
  import { esGrande, guardarPegado, marcaQueTerminaEn } from "../pegados.js";
8
- import { alEscribir, tope, ventana } from "../historial.js";
9
- import { enPantallaFija } from "../pantalla.js";
10
8
  /** Marca del cursor. Ink esconde el de verdad, así que este se dibuja. */
11
9
  const CARET = "█";
12
- export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHistorial = false, alDesplazar, }) {
10
+ /**
11
+ * UNA LÍNEA VACÍA TIENE QUE OCUPAR SU FILA
12
+ *
13
+ * Un `<Text>` sin contenido mide cero y desaparece. Y el marco empieza con dos
14
+ * líneas en blanco a propósito: es el aire entre la conversación y la caja, que
15
+ * se pidió expresamente ("pon más espacio ahí para que se vea exacto y chévere",
16
+ * y después "que sea bien pues").
17
+ *
18
+ * Sin esto el aire se lo come Ink y la caja queda pegada a la última frase. No
19
+ * lo cazaba ninguna prueba: todas miraban QUÉ líneas salen, ninguna CUÁNTAS.
20
+ */
21
+ function conCuerpo(texto) {
22
+ return texto.length > 0 ? texto : " ";
23
+ }
24
+ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, cerrarAlEnviar = true, alto, }) {
13
25
  const { stdout } = useStdout();
14
26
  const { exit } = useApp();
15
27
  const [texto, setTexto] = useState("");
@@ -19,18 +31,6 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
19
31
  const [, setRedibujar] = useState(0);
20
32
  const historial = useRef(leerHistorial());
21
33
  const [posHistorial, setPosHistorial] = useState(historial.current.length);
22
- // Cuanto se ha subido con Re Pag. Cero es el fondo, que es donde se esta
23
- // mientras se trabaja. Solo se usa en pantalla fija.
24
- const [subido, setSubido] = useState(0);
25
- const fija = enPantallaFija();
26
- // En pantalla fija el terminal ya no guarda nada: lo que sale lo pintamos
27
- // nosotros, asi que hay que repintar en cada escritura del agente. El
28
- // historial vive fuera de React y React no puede adivinar que ha crecido.
29
- useEffect(() => {
30
- if (!fija)
31
- return;
32
- return alEscribir(() => setRedibujar((n) => n + 1));
33
- }, [fija]);
34
34
  // Se escanea UNA vez por prompt, no en cada tecla: recorrer el proyecto entero
35
35
  // por cada letra se nota en cuanto el proyecto pasa de mediano.
36
36
  const archivos = useMemo(() => {
@@ -44,7 +44,9 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
44
44
  const opciones = opcionesDelDesplegable(texto, cursor, archivos);
45
45
  const indice = opciones.length > 0 ? ((elegido % opciones.length) + opciones.length) % opciones.length : 0;
46
46
  const cols = Math.max(20, stdout?.columns || 80);
47
- const rows = stdout?.rows || 24;
47
+ // El alto que da App ya viene descontado de lo que hay por encima; sin él, la
48
+ // ventana entera.
49
+ const rows = Math.max(6, alto ?? stdout?.rows ?? 24);
48
50
  // El cursor se dibuja: Ink esconde el del terminal.
49
51
  const conCaret = texto.slice(0, cursor) + CARET + texto.slice(cursor);
50
52
  const marco = buildPromptFrame({
@@ -56,51 +58,33 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
56
58
  items: opciones,
57
59
  selectedIndex: indice,
58
60
  });
59
- // Filas que le quedan a la conversacion una vez puesta la barra. En pantalla
60
- // fija es lo que se pinta; en la normal no se usa, que ahi el historial es del
61
- // terminal.
62
- // Cuando se ha subido, una fila se va en el aviso de que se esta mirando
63
- // atras. Sin descontarla, el arbol mide una fila mas que la ventana y el
64
- // terminal desplaza: la barra dejaria de estar clavada justo al usar el scroll,
65
- // que es para lo que existe este modo.
66
- // El alto SIN el aviso de "estas mirando atras". El salto se calcula sobre
67
- // esto y no sobre el alto real: el aviso solo existe cuando ya has subido, asi
68
- // que contarlo hacia que bajar no deshiciera lo que acababas de subir y el
69
- // scroll derivara solo.
70
- const baseDelSalto = Math.max(1, rows - marco.lines.length);
71
- const altoDelTexto = Math.max(1, baseDelSalto - (subido > 0 ? 1 : 0));
61
+ // CERRAR EL ARBOL O NO
62
+ //
63
+ // Dentro del arbol unico, `exit()` de Ink desmonta la aplicacion entera. Ver
64
+ // la nota larga en la prop `cerrarAlEnviar`.
65
+ function cerrar() {
66
+ if (cerrarAlEnviar)
67
+ exit();
68
+ }
72
69
  function enviar() {
73
70
  const orden = texto.trim();
74
71
  if (!orden)
75
72
  return;
76
73
  alEnviar(orden);
77
- exit();
74
+ cerrar();
78
75
  }
79
76
  useInput((entrada, tecla) => {
80
- // ----------------------------------------------- recorrer lo que salio
77
+ // RE PAG Y AV PAG NO SE TOCAN, Y ESO ES LO CORRECTO
81
78
  //
82
- // Solo Re Pag y Av Pag. Las flechas y los caracteres son para escribir:
83
- // robarselos aqui dejaria al usuario sin poder teclear.
84
- if (tecla.pageUp || tecla.pageDown) {
85
- const salto = Math.max(1, Math.floor(baseDelSalto / 2));
86
- // Con el arbol de App al mando se le manda la DIRECCION, no el salto:
87
- // aqui no se sabe cuanto sitio hay arriba, y calcular el salto en los dos
88
- // sitios con alturas distintas hacia que bajar no deshiciera lo que
89
- // acababa de subir. Con la direccion, el salto sale de un solo sitio y las
90
- // dos teclas son simetricas.
91
- if (alDesplazar) {
92
- alDesplazar(tecla.pageUp ? 1 : -1);
93
- return;
94
- }
95
- if (fija) {
96
- setSubido((n) => tecla.pageUp ? Math.min(tope(baseDelSalto), n + salto) : Math.max(0, n - salto));
97
- return;
98
- }
99
- }
79
+ // Las llevaba este componente cuando la conversacion vivia en memoria y la
80
+ // pintabamos nosotros. Ahora la conversacion es del terminal: la rueda del
81
+ // raton, Re Pag y la barra de scroll de la ventana funcionan solas y mejor
82
+ // que cualquier cosa que hicieramos aqui. Interceptarlas seria quitarle al
83
+ // usuario un scroll que ya tiene.
100
84
  // ------------------------------------------------------------- salir
101
85
  if (tecla.ctrl && entrada === "c") {
102
86
  alCancelar();
103
- exit();
87
+ cerrar();
104
88
  return;
105
89
  }
106
90
  // ------------------------------------------- cambiar el modo de permisos
@@ -141,7 +125,7 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
141
125
  if (arroba || soloLaBarra)
142
126
  return;
143
127
  alEnviar(aplicado.texto.trim());
144
- exit();
128
+ cerrar();
145
129
  return;
146
130
  }
147
131
  }
@@ -231,14 +215,5 @@ export function Prompt({ alEnviar, alCancelar, cwd, promptSymbol, avisos, sinHis
231
215
  setElegido(0);
232
216
  }
233
217
  });
234
- if (!fija || sinHistorial) {
235
- return (_jsxs(Box, { flexDirection: "column", children: [avisos.map((a, i) => (_jsx(Text, { children: a }, `aviso-${i}`))), marco.lines.map((linea, i) => (_jsx(Text, { children: linea }, i)))] }));
236
- }
237
- // PANTALLA FIJA: el arbol ocupa la ventana entera y la barra va la ultima, asi
238
- // que no puede moverse. Lo de arriba se rellena con huecos cuando hay poca
239
- // conversacion; sin eso la barra subiria a media pantalla, que es justo la
240
- // queja que este modo viene a resolver.
241
- const visible = ventana(altoDelTexto, subido);
242
- const huecos = Math.max(0, altoDelTexto - visible.length);
243
- return (_jsxs(Box, { flexDirection: "column", height: rows, children: [Array.from({ length: huecos }, (_, i) => (_jsx(Text, { children: " " }, `hueco-${i}`))), visible.map((linea, i) => (_jsx(Text, { children: linea }, `h-${i}`))), subido > 0 && (_jsx(Text, { dimColor: true, children: ` ↑ ${subido} lineas mas arriba · Av Pag para volver al final` })), marco.lines.map((linea, i) => (_jsx(Text, { children: linea }, `b-${i}`)))] }));
218
+ return (_jsxs(Box, { flexDirection: "column", children: [avisos.map((a, i) => (_jsx(Text, { children: conCuerpo(a) }, `aviso-${i}`))), marco.lines.map((linea, i) => (_jsx(Text, { children: conCuerpo(linea) }, i)))] }));
244
219
  }
@@ -1,2 +1,36 @@
1
1
  export declare function montarArbol(): void;
2
+ /**
3
+ * Suelta el árbol.
4
+ *
5
+ * `darPorPintado()` ANTES de desmontar, y el orden importa: si el árbol se
6
+ * vuelve a montar —un diálogo que necesita el teclado para él solo—, el
7
+ * `<Static>` nuevo empieza su cuenta de cero y reimprimiría la conversación
8
+ * entera. En la pantalla normal esas líneas siguen ahí, en el terminal; la marca
9
+ * dice por dónde iba para que el montaje nuevo solo escriba lo que falta.
10
+ *
11
+ * `clear()` borra el marco de abajo antes de irse. Sin eso queda una copia
12
+ * muerta de la barra en mitad del historial, y en la próxima sesión de scroll el
13
+ * usuario se encuentra dos.
14
+ */
2
15
  export declare function desmontarArbol(): void;
16
+ export declare function hayArbol(): boolean;
17
+ /**
18
+ * Borra la pantalla de verdad. Es lo que hacen /clear, /cd y Ctrl+L.
19
+ *
20
+ * POR QUE NO VALE UN `console.clear()`
21
+ *
22
+ * `console.clear()` escribe la secuencia de borrado directa al terminal, y no
23
+ * pasa por la interceptación de consola: ahí solo van `log`, `error`, `warn` e
24
+ * `info`. Con el árbol montado eso borra filas que Ink cree suyas, y a partir de
25
+ * ahí Ink borra y repinta sobre un sitio que ya no existe. Lo que se ve es
26
+ * basura, o nada.
27
+ *
28
+ * Aquí se hace en el orden que no rompe nada: se borra el marco con la cuenta
29
+ * todavía buena, se suelta el árbol, se limpia la pantalla —y el historial del
30
+ * terminal con `3J`, que es lo que el usuario espera de un /clear— y se vuelve a
31
+ * montar de cero.
32
+ *
33
+ * Y se vacía la transcripción: si no, el <Static> nuevo tendría por delante toda
34
+ * la conversación que se acaba de borrar.
35
+ */
36
+ export declare function limpiarLaPantalla(): void;