@milpa/comal 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +30 -28
  2. package/backend/contracts/remote-commands-v1.json +125 -0
  3. package/bin/comal.mjs +16 -5
  4. package/docs/investigation.md +2 -2
  5. package/governance/chrome-task-v2.json +67 -0
  6. package/node_modules/@jitl/quickjs-ffi-types/LICENSE +21 -0
  7. package/node_modules/@jitl/quickjs-ffi-types/README.md +5 -0
  8. package/node_modules/@jitl/quickjs-ffi-types/dist/index.d.mts +549 -0
  9. package/node_modules/@jitl/quickjs-ffi-types/dist/index.d.ts +549 -0
  10. package/node_modules/@jitl/quickjs-ffi-types/dist/index.js +2 -0
  11. package/node_modules/@jitl/quickjs-ffi-types/dist/index.js.map +1 -0
  12. package/node_modules/@jitl/quickjs-ffi-types/dist/index.mjs +2 -0
  13. package/node_modules/@jitl/quickjs-ffi-types/dist/index.mjs.map +1 -0
  14. package/node_modules/@jitl/quickjs-ffi-types/package.json +36 -0
  15. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/LICENSE +47 -0
  16. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/README.md +93 -0
  17. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.browser.d.ts +15 -0
  18. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.browser.mjs +2 -0
  19. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cjs +2 -0
  20. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cloudflare.cjs +2 -0
  21. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.cloudflare.d.ts +15 -0
  22. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.d.ts +15 -0
  23. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.mjs +2 -0
  24. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.wasm +0 -0
  25. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/emscripten-module.wasm.map +1 -0
  26. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.d.mts +94 -0
  27. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.d.ts +94 -0
  28. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.js +2 -0
  29. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.js.map +1 -0
  30. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.mjs +2 -0
  31. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/ffi.mjs.map +1 -0
  32. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.d.mts +20 -0
  33. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.d.ts +20 -0
  34. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.js +2 -0
  35. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.js.map +1 -0
  36. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.mjs +2 -0
  37. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/dist/index.mjs.map +1 -0
  38. package/node_modules/@jitl/quickjs-wasmfile-debug-asyncify/package.json +61 -0
  39. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/LICENSE +47 -0
  40. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/README.md +88 -0
  41. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.browser.d.ts +11 -0
  42. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.browser.mjs +5430 -0
  43. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cjs +5485 -0
  44. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cloudflare.cjs +5424 -0
  45. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  46. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.d.ts +11 -0
  47. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs +5541 -0
  48. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.wasm +0 -0
  49. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.wasm.map +1 -0
  50. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.d.mts +85 -0
  51. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.d.ts +85 -0
  52. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.js +2 -0
  53. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.js.map +1 -0
  54. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.mjs +2 -0
  55. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/ffi.mjs.map +1 -0
  56. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.d.mts +20 -0
  57. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.d.ts +20 -0
  58. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.js +2 -0
  59. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.js.map +1 -0
  60. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.mjs +2 -0
  61. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/index.mjs.map +1 -0
  62. package/node_modules/@jitl/quickjs-wasmfile-debug-sync/package.json +61 -0
  63. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/LICENSE +47 -0
  64. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/README.md +89 -0
  65. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.browser.d.ts +15 -0
  66. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.browser.mjs +26 -0
  67. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cjs +25 -0
  68. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cloudflare.cjs +25 -0
  69. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.cloudflare.d.ts +15 -0
  70. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.d.ts +15 -0
  71. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.mjs +30 -0
  72. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/emscripten-module.wasm +0 -0
  73. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.d.mts +94 -0
  74. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.d.ts +94 -0
  75. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.js +2 -0
  76. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.js.map +1 -0
  77. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.mjs +2 -0
  78. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/ffi.mjs.map +1 -0
  79. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.d.mts +20 -0
  80. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.d.ts +20 -0
  81. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.js +2 -0
  82. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.js.map +1 -0
  83. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.mjs +2 -0
  84. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/dist/index.mjs.map +1 -0
  85. package/node_modules/@jitl/quickjs-wasmfile-release-asyncify/package.json +61 -0
  86. package/node_modules/@jitl/quickjs-wasmfile-release-sync/LICENSE +47 -0
  87. package/node_modules/@jitl/quickjs-wasmfile-release-sync/README.md +82 -0
  88. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.browser.d.ts +11 -0
  89. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.browser.mjs +22 -0
  90. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cjs +21 -0
  91. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cloudflare.cjs +21 -0
  92. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.cloudflare.d.ts +11 -0
  93. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.d.ts +11 -0
  94. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.mjs +25 -0
  95. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/emscripten-module.wasm +0 -0
  96. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.d.mts +85 -0
  97. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.d.ts +85 -0
  98. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.js +2 -0
  99. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.js.map +1 -0
  100. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs +2 -0
  101. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/ffi.mjs.map +1 -0
  102. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.d.mts +20 -0
  103. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.d.ts +20 -0
  104. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.js +2 -0
  105. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.js.map +1 -0
  106. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.mjs +2 -0
  107. package/node_modules/@jitl/quickjs-wasmfile-release-sync/dist/index.mjs.map +1 -0
  108. package/node_modules/@jitl/quickjs-wasmfile-release-sync/package.json +61 -0
  109. package/node_modules/quickjs-emscripten/LICENSE +21 -0
  110. package/node_modules/quickjs-emscripten/README.md +928 -0
  111. package/node_modules/quickjs-emscripten/dist/chunk-OHAYRCBA.mjs +2 -0
  112. package/node_modules/quickjs-emscripten/dist/chunk-OHAYRCBA.mjs.map +1 -0
  113. package/node_modules/quickjs-emscripten/dist/index.d.mts +57 -0
  114. package/node_modules/quickjs-emscripten/dist/index.d.ts +57 -0
  115. package/node_modules/quickjs-emscripten/dist/index.global.js +7309 -0
  116. package/node_modules/quickjs-emscripten/dist/index.js +2 -0
  117. package/node_modules/quickjs-emscripten/dist/index.js.map +1 -0
  118. package/node_modules/quickjs-emscripten/dist/index.mjs +2 -0
  119. package/node_modules/quickjs-emscripten/dist/index.mjs.map +1 -0
  120. package/node_modules/quickjs-emscripten/dist/variants.d.mts +39 -0
  121. package/node_modules/quickjs-emscripten/dist/variants.d.ts +39 -0
  122. package/node_modules/quickjs-emscripten/dist/variants.js +2 -0
  123. package/node_modules/quickjs-emscripten/dist/variants.js.map +1 -0
  124. package/node_modules/quickjs-emscripten/dist/variants.mjs +2 -0
  125. package/node_modules/quickjs-emscripten/dist/variants.mjs.map +1 -0
  126. package/node_modules/quickjs-emscripten/package.json +69 -0
  127. package/node_modules/quickjs-emscripten-core/LICENSE +21 -0
  128. package/node_modules/quickjs-emscripten-core/README.md +341 -0
  129. package/node_modules/quickjs-emscripten-core/README.template.md +70 -0
  130. package/node_modules/quickjs-emscripten-core/dist/chunk-TAV5CUKK.mjs +2 -0
  131. package/node_modules/quickjs-emscripten-core/dist/chunk-TAV5CUKK.mjs.map +1 -0
  132. package/node_modules/quickjs-emscripten-core/dist/chunk-V2S4ZYJR.mjs +7 -0
  133. package/node_modules/quickjs-emscripten-core/dist/chunk-V2S4ZYJR.mjs.map +1 -0
  134. package/node_modules/quickjs-emscripten-core/dist/index.d.mts +2033 -0
  135. package/node_modules/quickjs-emscripten-core/dist/index.d.ts +2033 -0
  136. package/node_modules/quickjs-emscripten-core/dist/index.js +7 -0
  137. package/node_modules/quickjs-emscripten-core/dist/index.js.map +1 -0
  138. package/node_modules/quickjs-emscripten-core/dist/index.mjs +2 -0
  139. package/node_modules/quickjs-emscripten-core/dist/index.mjs.map +1 -0
  140. package/node_modules/quickjs-emscripten-core/dist/module-ES6BEMUI.mjs +2 -0
  141. package/node_modules/quickjs-emscripten-core/dist/module-ES6BEMUI.mjs.map +1 -0
  142. package/node_modules/quickjs-emscripten-core/dist/module-asyncify-2EFITU5U.mjs +2 -0
  143. package/node_modules/quickjs-emscripten-core/dist/module-asyncify-2EFITU5U.mjs.map +1 -0
  144. package/node_modules/quickjs-emscripten-core/package.json +49 -0
  145. package/package.json +6 -2
  146. package/src/agent.mjs +17 -6
  147. package/src/artifact-tools.mjs +3 -2
  148. package/src/auto-guard.mjs +5 -1
  149. package/src/cli.mjs +267 -201
  150. package/src/cloud-client.mjs +58 -8
  151. package/src/code-run-worker.mjs +70 -0
  152. package/src/code-run.mjs +97 -0
  153. package/src/codex-provider.mjs +28 -28
  154. package/src/compaction.mjs +1 -1
  155. package/src/goals.mjs +2 -2
  156. package/src/house.mjs +24 -24
  157. package/src/i18n/cli-es.mjs +97 -0
  158. package/src/i18n/es.mjs +227 -0
  159. package/src/i18n/index.mjs +48 -0
  160. package/src/image-attachments.mjs +263 -0
  161. package/src/model-controls.mjs +4 -4
  162. package/src/onboarding.mjs +91 -0
  163. package/src/openai-provider.mjs +24 -24
  164. package/src/peer-tools.mjs +4 -3
  165. package/src/permissions.mjs +53 -17
  166. package/src/provider-auth.mjs +30 -30
  167. package/src/provider-profiles.mjs +27 -27
  168. package/src/provider.mjs +30 -30
  169. package/src/remote-commands.mjs +11 -0
  170. package/src/remote-control.mjs +180 -0
  171. package/src/sessions.mjs +3 -3
  172. package/src/subagents.mjs +7 -3
  173. package/src/tool-packs.mjs +5 -2
  174. package/src/tools.mjs +92 -34
  175. package/src/tui/brand.mjs +2 -1
  176. package/src/tui/clipboard.mjs +5 -4
  177. package/src/tui/composer.mjs +77 -11
  178. package/src/tui/controller.mjs +111 -29
  179. package/src/tui/discovery.mjs +105 -100
  180. package/src/tui/markdown.mjs +10 -9
  181. package/src/tui/message-queue.mjs +27 -9
  182. package/src/tui/screen.mjs +209 -118
  183. package/src/vision.mjs +31 -3
  184. package/src/work-assertions.mjs +7 -7
  185. package/src/work-plan.mjs +189 -21
  186. package/src/work-recovery-proof.mjs +13 -1
  187. package/src/workflows/chrome-task.mjs +24 -7
@@ -1,3 +1,4 @@
1
+ import { t, formatNumber } from '../i18n/index.mjs';
1
2
  import { workProjection } from '../work-plan.mjs';
2
3
  import { Composer } from './composer.mjs';
3
4
  import { MarkdownRenderer, DataTableParser } from './markdown.mjs';
@@ -10,51 +11,20 @@ import { commandCatalog, commandSuggestions, applySuggestion, contextualTip } fr
10
11
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
11
12
  const MOUSE_ON = '\x1b[?1000h\x1b[?1006h';
12
13
  const MOUSE_OFF = '\x1b[?1000l\x1b[?1006l';
13
- const HELP = `# Comal · teclado
14
-
15
- | Acción | Tecla |
16
- | --- | --- |
17
- | Descubrir comandos y herramientas | F6 |
18
- | Completar comando o argumento | / y Tab; ↑/↓ elige; Esc cierra |
19
- | Enviar / encolar | Enter (las sugerencias sólo completan) |
20
- | Nueva línea | Alt+Enter o Ctrl+J |
21
- | Cambiar modo | Shift+Tab |
22
- | Editar con $VISUAL/$EDITOR | Ctrl+G |
23
- | Tablero de trabajo | F3 o /board |
24
- | Continuar desde checkpoint | /work resume |
25
- | Sesiones / reanudar | F2 o Ctrl+S |
26
- | Cola de mensajes | F4 o Ctrl+Q |
27
- | Desplegar resultados de herramientas | Ctrl+O |
28
- | Scroll del historial | Rueda del mouse / PgUp / PgDn |
29
- | Seleccionar texto con el mouse | F5 o /select; F5 o Esc vuelve |
30
- | Copiar última respuesta completa | Alt+C o /copy |
31
- | Ir al final | Ctrl+End |
32
- | Historial del composer | ↑/↓ en primera/última línea |
33
- | Inicio / final de línea | Home / End o Ctrl+A / Ctrl+E |
34
- | Borrar palabra / inicio / final | Ctrl+W / Ctrl+U / Ctrl+K |
35
- | Cancelar turno / pausar goal | Ctrl+C o Esc mientras trabaja |
36
- | Salir | Ctrl+D con composer vacío o /exit |
37
-
38
- Puedes seguir escribiendo durante el streaming. Los mensajes enviados se ejecutan en orden al terminar el turno actual. F4 permite pausar, editar, quitar y reordenar la cola.
39
-
40
- /context muestra el presupuesto de contexto. /compact deriva el estado sin LLM y espera su turno si hay trabajo activo. /state muestra petición y estado; /audit verifica el libro. El historial original se conserva.
41
- /house muestra recibos de ejecución y operaciones interrumpidas; /operations muestra contratos y efectos declarados. El éxito de ejecución no certifica el objetivo humano.
42
- /plugins muestra plugins, dependencias y operaciones; enable/disable/reload esperan su turno y sólo el usuario puede cambiar su ciclo de vida.
43
- /model ID y /effort NIVEL cambian la siguiente llamada; durante trabajo quedan en cola. /models lista modelos y /capabilities muestra soporte, procedencia y niveles. E indica esfuerzo; Ctx muestra la ventana efectiva (k/M tokens); ~ indica un límite configurado, ? capacidad desconocida/fallback. /context muestra valores exactos, reserva y fuente. /login PROVEEDOR autentica; /logout PROVEEDOR retira credenciales de Comal.
44
-
45
- **La ayuda mantiene activo el composer.** Puedes escribir mientras la consultas; PgUp/PgDn la desplaza y Esc/F1 la cierra. Al enviar vuelve la conversación. La rueda desplaza la conversación; las flechas editan el borrador. F5 libera el mouse y congela la vista: arrastra para seleccionar y usa la acción de copiar de tu terminal (normalmente Ctrl+Shift+C). El trabajo continúa y la vista se actualiza al volver. /copy conserva Markdown y bloques de código, sin el ajuste de líneas de la pantalla. El pegado multilínea no se envía automáticamente.`;
14
+ const HELP = "# Comal · keyboard\n\n| Action | Key |\n| --- | --- |\n| Discover commands and tools | F6 |\n| Complete command or argument | / and Tab; ↑/↓ chooses; Esc closes |\n| Send / enqueue | Enter (suggestions only complete) |\n| New line | Alt+Enter or Ctrl+J |\n| Switch mode | Shift+Tab |\n| Edit with $VISUAL/$EDITOR | Ctrl+G |\n| Work board | F3 or /board |\n| Continue from checkpoint | /work resume |\n| Sessions / resume | F2 or Ctrl+S |\n| Message queue | F4 or Ctrl+Q |\n| Expand tool results | Ctrl+O |\n| Scroll history | Mouse wheel / PgUp / PgDn |\n| Select text with mouse | F5 or /select; F5 or Esc returns |\n| Copy last full response | Alt+C or /copy |\n| Go to end | Ctrl+End |\n| Composer history | ↑/↓ on first/last line |\n| Start / end of line | Home / End or Ctrl+A / Ctrl+E |\n| Delete word / start / end | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancel turn / pause goal | Ctrl+C or Esc while working |\n| Exit | Ctrl+D with empty composer or /exit |\n\nYou can keep typing during streaming. Sent messages run in order when the current turn ends. F4 lets you pause, edit, remove, and reorder the queue.\n\n/context shows the context budget. /compact derives state without LLM and waits its turn if there is active work. /state shows request and state; /audit verifies the ledger. The original history is preserved.\n/house shows execution receipts and interrupted operations; /operations shows contracts and declared effects. Execution success does not certify the human goal.\n/plugins shows plugins, dependencies, and operations; enable/disable/reload wait their turn and only the user can change their lifecycle.\n/model ID and /effort LEVEL change the next call; during work they stay queued. /models lists models and /capabilities shows support, provenance, and levels. E indicates effort; Ctx shows the effective window (k/M tokens); ~ indicates a configured limit, ? unknown/fallback capability. /context shows exact values, reserve, and source. /login PROVIDER authenticates; /logout PROVIDER removes credentials from Comal.\n\n**Help keeps the composer active.** You can type while viewing it; PgUp/PgDn scrolls it and Esc/F1 closes it. Sending returns to the conversation. The wheel scrolls the conversation; the arrows edit the draft. F5 releases the mouse and freezes the view: drag to select and use your terminal's copy action (usually Ctrl+Shift+C). Work continues and the view updates when you return. /copy preserves Markdown and code blocks, without the screen line wrapping. Multi-line paste is not sent automatically.";
46
15
 
47
16
  export class TerminalScreen {
48
17
  constructor({ input = process.stdin, output = process.stdout, metadata = {}, clipboard = copyText, env = process.env } = {}) {
49
18
  Object.assign(this, { input, output, metadata, env }); this.composer = new Composer(); this.markdown = new MarkdownRenderer(); this.tables = new DataTableParser();
50
19
  this.clipboard = clipboard; this.selecting = false;
51
20
  this.blocks = []; this.toolBlocks = new Map(); this.subagentBlocks = new Map(); this.countedChildResults=new Set(); this.queue = { items: [], paused: false, interrupted: [] }; this.busy = false; this.ready = false;
52
- this.mode = metadata.mode || 'default'; this.phase = 'Conectando con el modelo'; this.frame = 0; this.scroll = 0; this.lastContentLength = 0; this.expandedTools = false;
21
+ this.mode = metadata.mode || 'default'; this.phase = t("Connecting to the model"); this.frame = 0; this.scroll = 0; this.lastContentLength = 0; this.expandedTools = false;
53
22
  this.discovery = {}; this.completionIndex = 0; this.brandStarted = Date.now();
54
23
  this.callbacks = {}; this.closed = false; this.suspended = false; this.previous = []; this.goal = null; this.usage = 0; this.turn = 0; this.maxTurns = 12;
55
24
  this.finished = new Promise(resolve => this.resolveFinished = resolve);
56
25
  }
57
26
  start() {
27
+ if (this.started || this.closed) return this;
58
28
  this.started = true;
59
29
  this.originalRaw = this.input.isRaw || false;
60
30
  this.input.setRawMode?.(true); this.input.resume();
@@ -65,11 +35,113 @@ export class TerminalScreen {
65
35
  this.output.write('\x1b[?1049h\x1b[?2004h\x1b[?25l' + MOUSE_ON);
66
36
  this.timer = setInterval(() => {
67
37
  const welcome = !this.blocks.length && this.motionEnabled() && this.brandFrame() < BRAND_WELCOME_FRAMES;
68
- if ((this.busy || !this.ready || welcome) && !this.suspended && !this.selecting) { this.frame++; this.render(); }
38
+ if ((this.busy || !this.ready || welcome || this.setup?.kind === 'status' || this.setup?.validating) && !this.suspended && !this.selecting) { this.frame++; this.render(); }
69
39
  }, 120);
70
40
  this.render(); return this;
71
41
  }
72
- configure(callbacks) { this.callbacks = callbacks; this.ready = true; this.phase = 'Listo'; this.schedule(); }
42
+ configure(callbacks) { this.clearSetup(); this.callbacks = callbacks; this.ready = true; this.phase = t("Ready"); this.schedule(); }
43
+ // Setup owns the existing terminal surface before a provider/session exists.
44
+ // Its editor is isolated from conversation drafts, history and callbacks.
45
+ beginSetup(state, signal, resolve = () => {}) {
46
+ this.clearSetup();
47
+ if (this.closed || signal?.aborted) { resolve(null); return null; }
48
+ let settled = false;
49
+ state.finish = value => {
50
+ if (settled) return; settled = true;
51
+ signal?.removeEventListener('abort', abort);
52
+ state.editor?.clear(); state.editor = null;
53
+ if (this.setup === state) this.setup = null;
54
+ resolve(value); this.schedule();
55
+ };
56
+ const abort = () => state.finish(null);
57
+ signal?.addEventListener('abort', abort, { once: true });
58
+ this.setup = state; this.schedule(); return state;
59
+ }
60
+ clearSetup() { this.setup?.finish(null); }
61
+ setupChoice({ title, description = '', items, signal } = {}) {
62
+ if (!Array.isArray(items) || !items.length || items.length > 100 || items.some(item => typeof item?.id !== 'string' || !item.id || typeof item.label !== 'string') || new Set(items.map(item => item.id)).size !== items.length) throw new Error(t('Setup requires between 1 and 100 distinct choices.'));
63
+ return new Promise(resolve => this.beginSetup({ kind: 'choice', title: safeText(title || t('Choose a connection')), description: safeText(description), items: items.map(item => ({ id: item.id, label: safeText(item.label), description: safeText(item.description || '') })), index: 0 }, signal, resolve));
64
+ }
65
+ setupInput({ title, description = '', value = '', secret = false, signal, validate } = {}) {
66
+ const clean = safeText(value).replace(/[\r\n\t]+/g, ' ');
67
+ if (Buffer.byteLength(clean) > 4096) throw new Error(t('Setup values support up to 4096 bytes.'));
68
+ return new Promise(resolve => this.beginSetup({ kind: 'input', title: safeText(title || t('Connection settings')), description: safeText(description), editor: new Composer(clean), secret: Boolean(secret), validate }, signal, resolve));
69
+ }
70
+ setupStatus({ title, description = '', signal } = {}) {
71
+ this.beginSetup({ kind: 'status', title: safeText(title || t('Connecting')), description: safeText(description) }, signal);
72
+ }
73
+ insertSetupText(text) {
74
+ const state = this.setup; if (state?.kind !== 'input' || state.validating) return;
75
+ const clean = safeText(text, { preserveTabs: true }).replace(/[\r\n\t]+/g, ' ');
76
+ if (Buffer.byteLength(state.editor.text + clean) > 4096) state.error = t('Use at most 4096 bytes.');
77
+ else { state.editor.insert(clean); state.error = null; }
78
+ this.schedule();
79
+ }
80
+ async handleSetupKey(text, key) {
81
+ const state = this.setup, name = key.name;
82
+ if (name === 'escape' || key.ctrl && name === 'c') {
83
+ state.finish(null);
84
+ if (state.kind === 'status') await this.callbacks.onSetupCancel?.();
85
+ if (key.ctrl && name === 'c') { if (this.callbacks.onInterrupt) await this.callbacks.onInterrupt(); else await this.callbacks.onAction?.('interrupt'); }
86
+ return;
87
+ }
88
+ if (state.kind === 'status' || state.validating) return;
89
+ if (state.kind === 'choice') {
90
+ if (name === 'up') state.index = Math.max(0, state.index - 1);
91
+ else if (name === 'down') state.index = Math.min(state.items.length - 1, state.index + 1);
92
+ else if (name === 'home') state.index = 0;
93
+ else if (name === 'end') state.index = state.items.length - 1;
94
+ else if (name === 'return' && !key.meta && !key.shift) state.finish(state.items[state.index].id);
95
+ this.schedule(); return;
96
+ }
97
+ if (name === 'return' && !key.meta && !key.shift) {
98
+ const value = state.editor.text; state.validating = true; state.error = null; this.schedule();
99
+ try {
100
+ const result = await state.validate?.(value);
101
+ if (this.setup !== state) return;
102
+ if (result === false || typeof result === 'string' && result.length) state.error = state.secret ? t('Check the value and try again.') : safeText(typeof result === 'string' ? result : t('Check the value and try again.'));
103
+ else state.finish(value);
104
+ } catch (error) { if (this.setup === state) state.error = state.secret ? t('Check the value and try again.') : safeText(error?.message || t('Check the value and try again.')); }
105
+ finally { state.validating = false; this.schedule(); }
106
+ return;
107
+ }
108
+ if (name === 'return' || name === 'linefeed' || key.ctrl && name === 'j') return;
109
+ if (!key.ctrl && !key.meta && text && !text.startsWith('\x1b')) this.insertSetupText(text);
110
+ else { state.editor.key('', key); state.error = null; this.schedule(); }
111
+ }
112
+ renderSetup(columns, rows) {
113
+ const state = this.setup, inner = Math.max(1, columns - 4);
114
+ const header = [tone.bold(' COMAL') + tone.accent(t(' · setup')), tone.muted('─'.repeat(columns))];
115
+ const title = wrap(state.title, inner).map(line => ' ' + tone.accent(line));
116
+ const description = wrap(state.description, inner).map(line => ' ' + tone.muted(line));
117
+ const prefix = [...header, '', ...title, '', ...description, ''];
118
+ const cancellation = this.ready ? t('Ctrl+C cancels') : t('Ctrl+C exits');
119
+ const hint = (state.kind === 'choice' ? t('↑/↓ choose · Enter continues · Esc back') : state.kind === 'input' ? t('Enter continues · Esc back') : t('Esc back')) + ' · ' + cancellation;
120
+ let body = [], cursor = null;
121
+ const budget = Math.max(1, rows - Math.min(prefix.length, rows - 4) - 2);
122
+ if (state.kind === 'choice') {
123
+ const start = Math.max(0, state.index - Math.max(1, Math.floor(budget / 2)) + 1);
124
+ for (let index = start; index < state.items.length && body.length < budget; index++) {
125
+ const item = state.items[index], selected = index === state.index;
126
+ body.push((selected ? tone.accent : tone.muted)(` ${selected ? '❯' : ' '} ${clip(item.label, Math.max(1, columns - 5))}`));
127
+ if (item.description && body.length < budget) body.push(' ' + tone.muted(clip(item.description, Math.max(1, columns - 5))));
128
+ }
129
+ } else if (state.kind === 'input') {
130
+ const editor = state.editor, display = state.secret ? new Composer('•'.repeat(editor.buffer.length)) : editor;
131
+ if (state.secret) display.cursor = editor.cursor;
132
+ const layout = display.layout(Math.max(2, columns - 6)), height = Math.max(1, budget - (state.error ? 1 : 0)), offset = Math.max(0, layout.cursorRow - height + 1);
133
+ body = layout.lines.slice(offset, offset + height).map((line, index) => tone.muted(index === 0 ? ' ❯ ' : ' │ ') + line);
134
+ cursor = { row: layout.cursorRow - offset + 1, col: layout.cursorColumn + 5 };
135
+ if (state.error) body.push(' ' + tone.warning(clip(state.error, inner)));
136
+ } else body.push(' ' + tone.accent(this.spinner() + ' ' + t('Working…')));
137
+ if (state.validating) body.push(' ' + tone.muted(this.spinner() + ' ' + t('Checking…')));
138
+ const visiblePrefix = prefix.slice(0, Math.max(0, rows - Math.min(body.length, budget) - 2));
139
+ const lines = [...visiblePrefix, ...body.slice(0, budget)];
140
+ if (cursor) cursor.row += visiblePrefix.length;
141
+ while (lines.length < rows - 1) lines.push(''); lines.push(' ' + tone.muted(clip(hint, columns - 1)));
142
+ if (state.validating) cursor = null;
143
+ this.draw(lines, cursor, columns, rows);
144
+ }
73
145
  schedule() {
74
146
  if (this.closed || this.suspended || this.selecting || this.renderTimer) return;
75
147
  this.renderTimer = setTimeout(() => { this.renderTimer = null; this.render(); }, 24);
@@ -80,7 +152,7 @@ export class TerminalScreen {
80
152
  setDiscovery(context = {}) { this.discovery = structuredClone(context); this.schedule(); }
81
153
  discoveryContext() { return { ...this.discovery, effortLevels: this.metadata.modelCapabilities?.effort?.levels || this.discovery.effortLevels, busy: this.busy, queue: this.queue, mode: this.mode, selecting: this.selecting, editingQueue: this.editingQueue }; }
82
154
  refreshDiscovery() { this.callbacks.onDiscover?.(); }
83
- hasModalOverlay() { return Boolean(this.overlay && this.overlay.kind !== 'help'); }
155
+ hasModalOverlay() { return Boolean(this.setup || this.overlay && this.overlay.kind !== 'help'); }
84
156
  completion() {
85
157
  const signature = JSON.stringify([this.composer.text, this.composer.cursor]);
86
158
  if (signature !== this.completionSignature) { this.completionSignature = signature; this.completionIndex = 0; }
@@ -98,7 +170,7 @@ export class TerminalScreen {
98
170
  if (!item) return false;
99
171
  if (result.exact === item) result.items = [item];
100
172
  const next = applySuggestion(this.composer.text, result, item);
101
- this.composer.setText(next.text, next.cursor); this.draftChanged(); return true;
173
+ this.composer.setText(next.text, next.cursor, this.composer.attachments.map(({start,end,...item})=>item)); this.draftChanged(); return true;
102
174
  }
103
175
  showPalette() { this.refreshDiscovery(); this.overlay = { kind: 'palette', filter: '', index: 0, tab: 'command', detail: null, scroll: 0 }; this.schedule(); }
104
176
  paletteItems() {
@@ -109,17 +181,18 @@ export class TerminalScreen {
109
181
  paletteTool() { return this.discovery.tools?.find(tool => tool.name === this.overlay?.detail); }
110
182
  preparePaletteItem(item) {
111
183
  if (item.inspectOnly) { this.overlay.detail = item.label; this.overlay.scroll = 0; this.schedule(); return; }
112
- if (!this.preparedDraft) this.preparedDraft = { text: this.composer.text, cursor: this.composer.cursor, sessionId: this.metadata.sessionId };
184
+ if (!this.preparedDraft) this.preparedDraft = { ...this.composer.snapshot(), sessionId: this.metadata.sessionId };
113
185
  this.composer.setText(item.value);
114
186
  this.closeOverlay(); this.draftChanged();
115
187
  }
116
- updateMetadata(metadata) { if(['model','provider','profile','effort'].some(key=>metadata[key]!==undefined && metadata[key]!==this.metadata[key]))this.cacheReadTokens=null; Object.assign(this.metadata, metadata); if (metadata.mode) this.mode = metadata.mode; this.schedule(); }
188
+ updateMetadata(metadata) { const changed=['model','provider','profile','effort'].some(key=>metadata[key]!==undefined && metadata[key]!==this.metadata[key]); if(changed)this.cacheReadTokens=null; Object.assign(this.metadata, metadata); if (metadata.mode) this.mode = metadata.mode; if(changed)this.callbacks.onCapabilitiesChanged?.(); this.schedule(); }
117
189
  setMode(mode) { this.mode = mode; this.schedule(); }
118
190
  setQueue(snapshot) { this.queue = snapshot; this.schedule(); }
119
- setBusy(value) { this.busy = value; if (value) { this.startedAt = Date.now(); this.turn = 0; this.phase = 'Preparando turno'; } else this.phase = 'Listo'; this.schedule(); }
191
+ setBusy(value) { this.busy = value; if (value) { this.startedAt = Date.now(); this.turn = 0; this.phase = t("Preparing turn"); } else this.phase = t("Ready"); this.schedule(); }
120
192
  notice(text, error = false) { this.blocks.push({ role: 'notice', text: safeText(text), error }); if (error && this.overlay) this.overlay.error = safeText(text); this.schedule(); }
121
- setDraft(text) { this.composer.setText(text); this.schedule(); }
122
- draftChanged() { this.callbacks.onDraft?.(this.composer.text); this.schedule(); }
193
+ setDraft(text, attachments = []) { this.composer.setText(text, undefined, attachments); this.schedule(); }
194
+ setAttachmentStatus(status) { this.attachmentStatus = status; this.schedule(); }
195
+ draftChanged() { if (!this.composer.attachments.length) this.attachmentStatus = null; this.callbacks.onDraft?.(this.composer.text, this.composer.attachments); this.schedule(); }
123
196
  addTable(text, format, title) { const data = this.tables.parse(text, format); this.blocks.push({ role: 'table', data, text: title }); this.schedule(); }
124
197
  loadSession(session) {
125
198
  this.cacheReadTokens=null;
@@ -128,16 +201,19 @@ export class TerminalScreen {
128
201
  for (const [responseId, row] of rows.entries()) {
129
202
  const message = row.message;
130
203
  if (message.role === 'user' && typeof message.content === 'string' && row.source !== 'goal') this.blocks.push({ role: 'user', text: message.content });
204
+ if (message.role === 'user' && row.source === 'human_composer' && Array.isArray(message.content)) this.blocks.push({role:'user',text:message.content.filter(block=>block.type==='text').map(block=>block.text).join('\n')});
131
205
  for (const block of Array.isArray(message.content) ? message.content : []) {
132
206
  if (message.role === 'assistant' && block.type === 'text') this.blocks.push({ role: 'assistant', text: block.text, responseId });
133
207
  if (message.role === 'assistant' && block.type === 'tool_use') { const item = { role: 'tool', id: block.id, name: block.name, input: block.input, state: 'interrupted' }; this.blocks.push(item); this.toolBlocks.set(block.id, item); }
134
208
  if (message.role === 'user' && block.type === 'tool_result') { const item = this.toolBlocks.get(block.tool_use_id); if (item) { item.state = block.is_error ? 'error' : 'done'; item.output = block.content; } }
135
209
  }
136
210
  }
137
- this.composer.history = rows.filter(row => row.message.role === 'user' && typeof row.message.content === 'string' && row.source !== 'goal').map(row => row.message.content).slice(-100);
138
- this.composer.setText(session.draft || ''); this.metadata.sessionId = session.id; this.metadata.model = session.model; this.metadata.name = session.name;
211
+ const history = rows.filter(row => row.message.role === 'user' && row.source !== 'goal' && (typeof row.message.content === 'string' || row.source === 'human_composer')).slice(-100);
212
+ this.composer.history = history.map(row => typeof row.message.content === 'string' ? row.message.content : row.message.content.filter(block=>block.type==='text').map(block=>block.text).join('\n'));
213
+ this.composer.historyAttachments = history.map(row=>row.attachments || []);
214
+ this.composer.setText(session.draft || '', undefined, session.draftAttachments || []); this.attachmentStatus = null; this.metadata.sessionId = session.id; this.metadata.model = session.model; this.metadata.name = session.name;
139
215
  this.context = null; this.work = workProjection(session);
140
- if (session.compaction) this.notice(`Contexto reanudado desde el libro: ~${session.compaction.after_tokens} tokens. Historial original conservado.`);
216
+ if (session.compaction) this.notice(t("Context resumed from the ledger: ~{v0} tokens. Original history preserved.", {v0: session.compaction.after_tokens}));
141
217
  this.countedChildResults.clear();let childUsage=0;
142
218
  for (const row of session.rows.filter(row => row.type === 'subagent')) {
143
219
  if(this.countChildResult(row))childUsage+=(row.usage?.input_tokens || 0)+(row.usage?.output_tokens || 0);
@@ -157,28 +233,28 @@ export class TerminalScreen {
157
233
  handleEvent(event) {
158
234
  if(event.type==='usage_accounting')this.cacheReadTokens=Number.isSafeInteger(event.accounting?.cache_read_tokens)?event.accounting.cache_read_tokens:null;
159
235
  if (event.type === 'model_stream_progress') {
160
- if (event.tool_argument_chars) this.phase = `Preparando herramientas · ${event.tool_argument_chars.toLocaleString()} caracteres`;
161
- else if (event.reasoning_chars && !event.text_chars) this.phase = `Modelo razonando · ${event.reasoning_chars.toLocaleString()} caracteres`;
236
+ if (event.tool_argument_chars) this.phase = t("Preparing tools · {v0} characters", {v0: formatNumber(event.tool_argument_chars)});
237
+ else if (event.reasoning_chars && !event.text_chars) this.phase = t("Model reasoning · {v0} characters", {v0: formatNumber(event.reasoning_chars)});
162
238
  }
163
239
  if (event.type === 'context_usage') this.context = event;
164
240
  if (event.type === 'vision_status') {
165
241
  const stamp = JSON.stringify([event.supported, event.source, event.images, event.references?.map(ref => ref.sha256)]);
166
- if (stamp !== this.lastVisionStamp) { this.lastVisionStamp = stamp; this.notice(event.images ? `Visión · ${event.images} imágenes en la petición · reserva ~${event.token_reserve} tokens.` : `Captura guardada como referencia · ${event.supported ? 'sin espacio para píxeles en el contexto' : 'el proveedor no anuncia visión'}.`); }
242
+ if (stamp !== this.lastVisionStamp) { this.lastVisionStamp = stamp; this.notice(event.images ? t("Vision · {v0} images in the request · reserve ~{v1} tokens.", {v0: event.images, v1: event.token_reserve}) : t("Screenshot saved as a reference · {v0}.", {v0: event.supported ? t("no space for pixels in the context") : t("the provider does not advertise vision")})); }
167
243
  }
168
- if (event.type === 'context_projection_start') this.phase = 'Derivando estado del libro';
169
- if (event.type === 'context_projection') this.notice(`Estado derivado · ~${event.after_tokens} tokens · 0 llamadas de resumen al LLM · evidencia guardada.`);
244
+ if (event.type === 'context_projection_start') this.phase = t("Deriving state from the ledger");
245
+ if (event.type === 'context_projection') this.notice(t("State derived \u00b7 ~{v0} tokens \u00b7 0 LLM summary calls \u00b7 evidence saved.", {v0: event.after_tokens}));
170
246
  if (['model_retry', 'response_discarded'].includes(event.type)) {
171
- if (this.streaming) { this.streaming.text += '\n\n[Respuesta incompleta descartada; no se ejecutaron sus herramientas.]'; this.streaming = null; }
172
- if (event.type === 'model_retry') this.notice(`Respuesta descartada (${event.reason}) · reintento acotado · salida máxima ${event.max_tokens} tokens.`);
247
+ if (this.streaming) { this.streaming.text += t("\n\n[Incomplete response discarded; its tools were not executed.]"); this.streaming = null; }
248
+ if (event.type === 'model_retry') this.notice(t("Response discarded ({v0}) · bounded retry · maximum output {v1} tokens.", {v0: event.reason, v1: event.max_tokens}));
173
249
  }
174
250
  if (['mcp_reload', 'plugin_status'].includes(event.type)) this.refreshDiscovery();
175
- if (event.type === 'mcp_reload') this.notice(`MCP actualizado · ${event.servers.filter(server => server.status === 'connected').length} servidores conectados · ${event.tools.length} herramientas disponibles en packs.`);
251
+ if (event.type === 'mcp_reload') this.notice(t("MCP updated · {v0} servers connected · {v1} tools available in packs.", {v0: event.servers.filter(server => server.status === 'connected').length, v1: event.tools.length}));
176
252
  if (event.type === 'plugin_status') {
177
- if (event.state === 'loading') this.phase = `Activando plugin ${event.id}`;
178
- if (['active', 'disabled'].includes(event.state)) this.notice(`Plugin ${event.id} · ${event.state}${event.operations ? ` · ${event.operations.length} operaciones` : ''}`);
253
+ if (event.state === 'loading') this.phase = t("Enabling plugin {v0}", {v0: event.id});
254
+ if (['active', 'disabled'].includes(event.state)) this.notice(t("Plugin {v0} · {v1}{v2}", {v0: event.id, v1: event.state, v2: event.operations ? t(" · {v0} operations", {v0: event.operations.length}) : ''}));
179
255
  if (['failed', 'cleanup_failed', 'observer_failed'].includes(event.state)) this.notice(`Plugin ${event.id}: ${event.error}`, true);
180
256
  }
181
- if (event.type === 'mcp_reload_error') this.notice(`No se aplicó la recarga MCP: ${event.error}`, true);
257
+ if (event.type === 'mcp_reload_error') this.notice(t("MCP reload not applied: {v0}", {v0: event.error}), true);
182
258
  if (event.type === 'mcp_progress') {
183
259
  const tool = [...this.toolBlocks.values()].findLast(block => block.name === event.name && block.state === 'running');
184
260
  if (tool) { tool.progress = event; tool.cacheKey = null; }
@@ -203,19 +279,19 @@ export class TerminalScreen {
203
279
  if (event.type === 'user' && event.source !== 'goal') this.blocks.push({ role: 'user', text: event.userIntent || event.text });
204
280
  if (event.type === 'text_delta') {
205
281
  if (!this.streaming) { this.streaming = { role: 'assistant', text: '' }; this.blocks.push(this.streaming); }
206
- this.streaming.text += event.text; this.phase = 'Recibiendo respuesta';
282
+ this.streaming.text += event.text; this.phase = t("Receiving response");
207
283
  }
208
284
  if (event.type === 'assistant') {
209
285
  if (!this.streaming) { const text = event.message.content.filter(b => b.type === 'text').map(b => b.text).join(''); if (text) this.blocks.push({ role: 'assistant', text }); }
210
286
  this.streaming = null;
211
287
  }
212
- if (event.type === 'tool_use') { const item = { role: 'tool', id: event.id, name: event.name, input: event.input, state: 'running' }; this.blocks.push(item); this.toolBlocks.set(event.id, item); this.phase = `Ejecutando ${event.name}`; }
288
+ if (event.type === 'tool_use') { const item = { role: 'tool', id: event.id, name: event.name, input: event.input, state: 'running' }; this.blocks.push(item); this.toolBlocks.set(event.id, item); this.phase = t("Executing {v0}", {v0: event.name}); }
213
289
  if (event.type === 'tool_result') { const item = this.toolBlocks.get(event.tool_use_id); if (item) { item.state = event.is_error ? 'error' : 'done'; item.output = event.content; } }
214
290
  if (event.type === 'auto_decision') this.blocks.push({ role: 'notice', text: `Auto · ${event.decision}: ${event.reason}`, error: event.decision === 'deny' });
215
291
  if (event.type === 'model_progress') { this.turn = event.turn; this.maxTurns = event.maxTurns; }
216
292
  if (event.type === 'phase') {
217
- if (event.state === 'start') this.phase = { worker: 'Esperando al modelo', classifier: 'Revisando permiso auto', 'goal-evaluation': 'Verificando /goal' }[event.phase] || event.phase;
218
- else if (this.busy) this.phase = 'Procesando turno';
293
+ if (event.state === 'start') this.phase = { worker: t("Waiting for the model"), classifier: t("Checking auto permission"), 'goal-evaluation': t("Checking /goal") }[event.phase] || event.phase;
294
+ else if (this.busy) this.phase = t("Processing turn");
219
295
  }
220
296
  if (event.type === 'goal_status') {
221
297
  const key = `${event.goal.id}:${event.goal.status}:${event.goal.lastReason}`;
@@ -223,10 +299,10 @@ export class TerminalScreen {
223
299
  this.goal = event.goal;
224
300
  }
225
301
  if (event.type === 'result' && !event.usage_includes_subagents) this.usage += (event.usage?.input_tokens || 0) + (event.usage?.output_tokens || 0);
226
- if (event.type === 'work_continuation') this.notice(`Continúa ${event.task_id} · ${event.calls}/${event.limit} llamadas · checkpoint conservado.`);
227
- if(event.type==='peer_reply')this.notice(event.status==='failed'?`La casa no pudo enviar el informe: ${event.error}`:`La casa devolvió el informe al buzón ${event.to.slice(0,8)} · ${event.status}.`,event.status==='failed');
228
- if (event.type === 'result' && event.work_stop_reason) this.notice(`Trabajo: ${event.work_stop_reason}.`);
229
- if (event.type === 'work_open') this.notice(`Quedan ${event.remaining.length} tareas abiertas · /work resume continúa desde el checkpoint.`);
302
+ if (event.type === 'work_continuation') this.notice(t("Continues {v0} · {v1}/{v2} calls · checkpoint preserved.", {v0: event.task_id, v1: event.calls, v2: event.limit}));
303
+ if(event.type==='peer_reply')this.notice(event.status==='failed'?t("The house could not send the report: {v0}", {v0: event.error}):t("The house returned the report to mailbox {v0} · {v1}.", {v0: event.to.slice(0,8), v1: event.status}),event.status==='failed');
304
+ if (event.type === 'result' && event.work_stop_reason) this.notice(t("Work: {v0}.", {v0: event.work_stop_reason}));
305
+ if (event.type === 'work_open') this.notice(t("{v0} open tasks remain · /work resume continues from the checkpoint.", {v0: event.remaining.length}));
230
306
  if (event.type === 'work_board' && (!event.session_id || event.session_id === this.metadata.sessionId)) this.work = event.board;
231
307
  if (event.type === 'result' && event.work) this.work = event.work;
232
308
  if (event.type === 'init') this.metadata.model = event.model;
@@ -237,9 +313,11 @@ export class TerminalScreen {
237
313
  if (signal?.aborted || this.closed) return false;
238
314
  if (this.selecting) this.setSelection(false);
239
315
  return new Promise(resolve => {
240
- const finish = allowed => { signal?.removeEventListener('abort', abort); this.permission = null; this.schedule(); resolve(allowed); };
316
+ let settled=false;
317
+ const finish = allowed => { if(settled)return;settled=true;signal?.removeEventListener('abort', abort);if(this.permission===question)this.permission = null;this.schedule();resolve(allowed); };
241
318
  const abort = () => finish(false);
242
- this.permission = { name, input, allowed: false, scroll: 0, finish }; signal?.addEventListener('abort', abort, { once: true }); this.schedule();
319
+ const question = { name, input, allowed: false, scroll: 0, finish };
320
+ this.permission = question;signal?.addEventListener('abort', abort, { once: true });if(signal?.aborted)abort();else this.schedule();
243
321
  });
244
322
  }
245
323
  showSessions(items) { this.overlay = { kind: 'sessions', items, filter: '', index: Math.max(0, items.findIndex(item => item.id !== this.metadata.sessionId)) }; this.schedule(); }
@@ -248,6 +326,7 @@ export class TerminalScreen {
248
326
  closeOverlay() { this.overlay = null; this.callbacks.onAction?.('queueKick'); this.schedule(); }
249
327
  filteredSessions() { const query = this.overlay?.filter?.toLowerCase() || ''; return (this.overlay?.items || []).filter(item => `${item.name || ''} ${item.prompt} ${item.id} ${item.model}`.toLowerCase().includes(query)); }
250
328
  paste(text) {
329
+ if (this.setup) { this.insertSetupText(text); return; }
251
330
  if (this.selecting) return;
252
331
  if (this.permission) return;
253
332
  if (this.overlay?.kind === 'palette' && !this.overlay.detail) { this.overlay.filter += safeText(text).replace(/\n/g, ' ').slice(0, 150); this.overlay.index = 0; this.schedule(); return; }
@@ -264,14 +343,15 @@ export class TerminalScreen {
264
343
  }
265
344
  async copyResponse() {
266
345
  const block = this.blocks.findLast(block => block.role === 'assistant' && block.text?.trim());
267
- if (!block) throw new Error('Todavía no hay una respuesta para copiar.');
346
+ if (!block) throw new Error(t("There is no response to copy yet."));
268
347
  const partial = block === this.streaming;
269
348
  const text = block.responseId === undefined ? block.text : this.blocks.filter(item => item.role === 'assistant' && item.responseId === block.responseId).map(item => item.text).join('');
270
349
  await this.clipboard(text);
271
- this.notice(partial ? 'Texto recibido hasta ahora copiado al portapapeles; la respuesta sigue llegando.' : 'Respuesta completa copiada al portapapeles (Markdown original).');
350
+ this.notice(partial ? t("Text received so far copied to clipboard; the response is still arriving.") : t("Full response copied to clipboard (original Markdown)."));
272
351
  }
273
352
  handleMouse(event) {
274
353
  if (this.closed || this.suspended || this.selecting || event.type !== 'wheel') return;
354
+ if (this.setup) { if (this.setup.kind === 'choice') this.setup.index = Math.max(0, Math.min(this.setup.items.length - 1, this.setup.index + event.delta)); this.schedule(); return; }
275
355
  const lines = event.delta * 3;
276
356
  if (this.permission) this.permission.scroll = Math.max(0, this.permission.scroll + lines);
277
357
  else if (['help', 'board'].includes(this.overlay?.kind)) this.overlay.scroll = Math.max(0, this.overlay.scroll + lines);
@@ -286,6 +366,7 @@ export class TerminalScreen {
286
366
  }
287
367
  async handleKey(text, key = {}) {
288
368
  if (this.closed || this.suspended) return;
369
+ if (this.setup) { await this.handleSetupKey(text, key); return; }
289
370
  const name = key.name;
290
371
  if (this.selecting) {
291
372
  if (name === 'f5' || name === 'escape' || (key.ctrl && name === 'c')) this.setSelection(false);
@@ -312,10 +393,10 @@ export class TerminalScreen {
312
393
  else if (name === 'pagedown' || name === 'down') this.overlay.scroll += 3;
313
394
  else if (name === 'tab' && !key.shift) {
314
395
  const tool = this.paletteTool();
315
- if (!tool) { this.notice('Esa operación ya no está publicada. Vuelve al catálogo.', true); return; }
316
- if (tool.audience === 'human') { this.notice('Control humano: usa el comando correspondiente o /plugins run; no se pide al modelo.', true); return; }
317
- if (!this.preparedDraft) this.preparedDraft = { text: this.composer.text, cursor: this.composer.cursor, sessionId: this.metadata.sessionId };
318
- this.composer.setText(`Usa la herramienta ${safeText(tool.name)} para ${this.composer.text}`);
396
+ if (!tool) { this.notice(t("That operation is no longer published. Return to the catalog."), true); return; }
397
+ if (tool.audience === 'human') { this.notice(t("Human control: use the corresponding command or /plugins run; the model is not asked."), true); return; }
398
+ if (!this.preparedDraft) this.preparedDraft = { ...this.composer.snapshot(), sessionId: this.metadata.sessionId };
399
+ this.composer.setText(t("Use the tool {v0} to {v1}", {v0: safeText(tool.name), v1: this.composer.text}), undefined, this.composer.attachments.map(({start,end,...item})=>item));
319
400
  this.closeOverlay(); this.draftChanged(); return;
320
401
  }
321
402
  } else if (name === 'tab') { this.overlay.tab = this.overlay.tab === 'tool' ? 'command' : 'tool'; this.overlay.index = 0; }
@@ -363,7 +444,7 @@ export class TerminalScreen {
363
444
  if (key.ctrl && name === 'g') { await this.callbacks.onAction?.('editor'); return; }
364
445
  if (key.ctrl && name === 'd' && !this.composer.text) { await this.callbacks.onAction?.('exit'); return; }
365
446
  if (name === 'escape' && this.completion().items.length) { this.dismissedCompletion = this.completionSignature; this.schedule(); return; }
366
- if (name === 'escape' && this.preparedDraft) { const draft = this.preparedDraft; this.preparedDraft = null; this.composer.setText(draft.text, draft.cursor); this.draftChanged(); return; }
447
+ if (name === 'escape' && this.preparedDraft) { const draft = this.preparedDraft; this.preparedDraft = null; this.composer.setText(draft.text, draft.cursor, draft.attachments); this.draftChanged(); return; }
367
448
  if (name === 'escape') { if (this.editingQueue) await this.callbacks.onAction?.('queueEditCancel'); else if (this.busy) await this.callbacks.onAction?.('interrupt'); return; }
368
449
  if (name === 'pageup') { this.scroll += Math.max(5, (this.output.rows || 24) - 10); this.schedule(); return; }
369
450
  if (name === 'pagedown') { this.scroll = Math.max(0, this.scroll - Math.max(5, (this.output.rows || 24) - 10)); this.schedule(); return; }
@@ -379,15 +460,23 @@ export class TerminalScreen {
379
460
  }
380
461
  if (name === 'return' && !key.shift && !key.meta && completion.items.length) { this.acceptCompletion(); return; }
381
462
  if (name === 'return' && !key.shift && !key.meta) {
463
+ if (this.submitting) return;
382
464
  const value = this.composer.text; if (!value.trim()) return;
383
- if (!this.ready) { this.notice('Todavía conectando; el borrador se conserva.'); return; }
465
+ if (!this.ready) { this.notice(t("Still connecting; the draft is preserved.")); return; }
384
466
  if (this.overlay?.kind === 'help') this.closeOverlay();
385
467
  const draft = this.preparedDraft;
386
- this.composer.remember(value); this.composer.clear(); this.draftChanged();
468
+ let prepared = this.composer.snapshot(), submitted = false;
469
+ this.submitting = true;
387
470
  try {
388
- await this.callbacks.onSubmit?.(value);
389
- if (draft && draft.sessionId === this.metadata.sessionId && !this.composer.text) { this.preparedDraft = null; this.composer.setText(draft.text, draft.cursor); this.draftChanged(); }
390
- } catch (e) { if (!this.composer.text) this.composer.setText(value); this.preparedDraft = draft; this.notice(e.message, true); this.draftChanged(); }
471
+ const original = JSON.stringify(prepared), sessionId = this.metadata.sessionId;
472
+ if (this.callbacks.onPrepareSubmit) prepared = await this.callbacks.onPrepareSubmit(prepared) || prepared;
473
+ const current = JSON.stringify(this.composer.snapshot());
474
+ if (sessionId !== this.metadata.sessionId || (current !== original && current !== JSON.stringify(prepared))) throw new Error(t('The draft changed while preparing the image. Press Enter again to send the current draft.'));
475
+ this.composer.remember(prepared.text, prepared.attachments); this.composer.clear(); this.draftChanged(); submitted = true;
476
+ await this.callbacks.onSubmit?.(prepared.text, prepared.attachments);
477
+ if (draft && draft.sessionId === this.metadata.sessionId && !this.composer.text) { this.preparedDraft = null; this.composer.setText(draft.text, draft.cursor, draft.attachments); this.draftChanged(); }
478
+ } catch (e) { if (submitted && !this.composer.text) this.composer.setText(prepared.text, prepared.cursor, prepared.attachments); this.preparedDraft = draft; this.notice(e.message, true); this.draftChanged(); }
479
+ finally { this.submitting = false; }
391
480
  return;
392
481
  }
393
482
  this.composer.key(text, key, Math.max(2, (this.output.columns || 80) - 7)); this.draftChanged();
@@ -396,22 +485,22 @@ export class TerminalScreen {
396
485
  const output = [];
397
486
  if (!this.blocks.length) {
398
487
  const mark = brandingWelcome({ columns: Math.max(0, columns - 4), frame: this.brandFrame(), active: this.busy, env: this.env, stream: this.output });
399
- const intro = [tone.bold(' ¿Qué construimos hoy?'), '', ...wrap(' Escribe una tarea; / o F6 te muestran qué puedes hacer.', columns), '', tone.muted(' F6 descubrir · F2 sesiones · Shift+Tab modo')];
488
+ const intro = [tone.bold(t(" What are we building today?")), '', ...wrap(t(" Type a task; / or F6 show you what you can do."), columns), '', tone.muted(t(" F6 discover · F2 sessions · Shift+Tab mode"))];
400
489
  const branded = ['', ...mark.map(line => ' ' + line), '', ...intro];
401
490
  if (branded.length <= this.bodyHeight) output.push(...branded);
402
- else output.push('', tone.accent(' COMAL · sobre el comal se cocina'), '', ...intro);
491
+ else output.push('', tone.accent(t(" COMAL · on the comal one cooks")), '', ...intro);
403
492
  }
404
493
  for (const block of this.blocks) {
405
494
  const cacheKey = `${columns}:${this.expandedTools}:${block.text || ''}:${block.state || ''}:${block.output || ''}`;
406
495
  if (block.cacheKey === cacheKey) { output.push(...block.cached); continue; }
407
496
  let lines;
408
- if (block.role === 'user') lines = [tone.accent(' Tú'), ...wrap(safeText(block.text), columns - 4).map(line => ' ' + line), ''];
497
+ if (block.role === 'user') lines = [tone.accent(t(" You")), ...wrap(safeText(block.text), columns - 4).map(line => ' ' + line), ''];
409
498
  else if (block.role === 'assistant') lines = [tone.bold(' Comal'), ...this.markdown.render(block.text, columns - 4).map(line => ' ' + line), ''];
410
499
  else if (block.role === 'notice') lines = wrap((block.error ? tone.bad : tone.muted)(safeText(` ${block.error ? '✗' : '·'} ${block.text}`)), columns);
411
500
  else if (block.role === 'table') lines = [tone.accent(' ' + block.text), ...this.tables.render(block.data, columns - 4).map(line => ' ' + line), ''];
412
501
  else if (block.role === 'subagent') {
413
502
  const icon = ['running','queued','waiting'].includes(block.runner_state || block.state) ? this.spinner() : block.work_state==='checks_verified' ? '✓' : block.state==='paused' ? '◇' : block.state==='completed' ? '·' : '✗';
414
- lines = [' ' + tone.accent(clip(safeText(`${icon} Subagente ${block.id.slice(0, 8)} · ${block.subagent_type} · ${block.description}`), columns - 4)), ' ' + tone.muted(clip(safeText(`${block.runner_state || block.state} · ${block.turn_state || block.status || block.state} · ${block.work_state || 'unjudged'} · calls ${block.turns || 0}/${block.max_turns || 6}${block.lastTool ? ' · ' + block.lastTool : ''}`), columns - 6))];
503
+ lines = [' ' + tone.accent(clip(safeText(t("{v0} Subagent {v1} · {v2} · {v3}", {v0: icon, v1: block.id.slice(0, 8), v2: block.subagent_type, v3: block.description})), columns - 4)), ' ' + tone.muted(clip(safeText(`${block.runner_state || block.state} · ${block.turn_state || block.status || block.state} · ${block.work_state || 'unjudged'} · calls ${block.turns || 0}/${block.max_turns || 6}${block.lastTool ? ' · ' + block.lastTool : ''}`), columns - 6))];
415
504
  if (this.expandedTools || ['failed', 'cancelled', 'interrupted','paused'].includes(block.state)) lines.push(...wrap(safeText(block.error || block.result || block.text || ''), columns - 6).slice(0, 15).map(line => ' ' + line));
416
505
  }
417
506
  else {
@@ -432,62 +521,63 @@ export class TerminalScreen {
432
521
  if (this.overlay?.kind === 'palette' && !this.permission) {
433
522
  if (this.overlay.detail) {
434
523
  const tool = this.paletteTool();
435
- if (!tool) return [tone.warning(' Operación retirada del catálogo'), tone.muted(' Esc vuelve; no se ejecutó nada.')];
436
- const lines = [tone.accent(' ' + safeText(tool.name)), ...wrap(safeText(tool.description), columns - 4).map(line => ' ' + line), '', tone.muted(` Audiencia: ${safeText(tool.audience || 'any')} · origen: ${safeText(tool.origin?.plugin || tool.origin?.kind || 'núcleo/MCP')}`), tone.muted(' Consultar no autoriza. La ejecución conserva el juicio de la casa.'), ...wrap(safeText('Efectos declarados: ' + Object.entries(tool.effects || {}).map(([key,value]) => `${key}=${value}`).join(' · ')), columns - 4).map(line => ' ' + tone.muted(line)), '', tone.bold(' Argumentos · * obligatorio')];
437
- for (const [name, schema] of Object.entries(tool.input_schema?.properties || {})) lines.push(...wrap(safeText(`${tool.input_schema?.required?.includes(name) ? '* ' : ' '}${name} · ${schema.type || 'valor'}${schema.enum ? ' · ' + schema.enum.join(' | ') : ''}${schema.description ? ' · ' + schema.description : ''}`), columns - 6).map(line => ' ' + line));
438
- if (!Object.keys(tool.input_schema?.properties || {}).length) lines.push(tone.muted(' Sin campos declarados.'));
439
- lines.push('', tone.muted(tool.audience === 'human' ? ' Control humano · Esc vuelve al catálogo' : ' Tab prepara una petición · no envía · Esc vuelve'));
524
+ if (!tool) return [tone.warning(t(" Operation removed from the catalog")), tone.muted(t(" Esc returns; nothing was executed."))];
525
+ const lines = [tone.accent(' ' + safeText(tool.name)), ...wrap(safeText(tool.description), columns - 4).map(line => ' ' + line), '', tone.muted(t(" Audience: {v0} · source: {v1}", {v0: safeText(tool.audience || 'any'), v1: safeText(tool.origin?.plugin || tool.origin?.kind || t("core/MCP"))})), tone.muted(t(" Consulting does not authorize. Execution preserves the house's judgment.")), ...wrap(safeText(t("Declared effects: ") + Object.entries(tool.effects || {}).map(([key,value]) => `${key}=${value}`).join(' · ')), columns - 4).map(line => ' ' + tone.muted(line)), '', tone.bold(t(" Arguments · * required"))];
526
+ for (const [name, schema] of Object.entries(tool.input_schema?.properties || {})) lines.push(...wrap(safeText(`${tool.input_schema?.required?.includes(name) ? '* ' : ' '}${name} · ${schema.type || t("value")}${schema.enum ? ' · ' + schema.enum.join(' | ') : ''}${schema.description ? ' · ' + schema.description : ''}`), columns - 6).map(line => ' ' + line));
527
+ if (!Object.keys(tool.input_schema?.properties || {}).length) lines.push(tone.muted(t(" No fields declared.")));
528
+ lines.push('', tone.muted(tool.audience === 'human' ? t(" Human control · Esc returns to catalog") : t(" Tab prepares a request · does not send · Esc returns")));
440
529
  this.overlay.scroll = Math.min(this.overlay.scroll, Math.max(0, lines.length - height)); return lines.slice(this.overlay.scroll, this.overlay.scroll + height);
441
530
  }
442
531
  const items = this.paletteItems(); this.overlay.index = Math.max(0, Math.min(this.overlay.index, items.length - 1));
443
532
  const visible = Math.max(1, height - 5), start = Math.max(0, this.overlay.index - visible + 1);
444
- return [tone.accent(` Descubrir · ${this.overlay.tab === 'tool' ? 'HERRAMIENTAS' : 'COMANDOS'} · ${items.length}`), tone.muted(' Tab cambia sección · ↑/↓ elige · Enter prepara/inspecciona · Esc vuelve'), ` Buscar: ${safeText(this.overlay.filter) || tone.muted('escribe para filtrar')}`, '', ...items.slice(start, start + visible).map((item, i) => {
533
+ return [tone.accent(t(" Discover · {v0} · {v1}", {v0: this.overlay.tab === 'tool' ? t("TOOLS") : t("COMMANDS"), v1: items.length})), tone.muted(t(" Tab changes section · ↑/↓ chooses · Enter prepares/inspects · Esc returns")), t(" Search: {v0}", {v0: safeText(this.overlay.filter) || tone.muted(t("type to filter"))}), '', ...items.slice(start, start + visible).map((item, i) => {
445
534
  const selected = start + i === this.overlay.index, labelWidth = Math.max(10, Math.floor(columns * .36));
446
535
  const label = ` ${selected ? '❯' : ' '} ${safeText(item.label)}`;
447
536
  const line = columns >= 55 ? pad(clip(label, labelWidth), labelWidth) + ' ' + safeText(item.description) : label;
448
537
  return (selected ? tone.accent : String)(clip(line, columns));
449
- }), ...(!items.length ? [tone.muted(' Sin coincidencias. Cambia de sección con Tab.')] : [])];
538
+ }), ...(!items.length ? [tone.muted(t(" No matches. Change section with Tab."))] : [])];
450
539
  }
451
540
  if (this.overlay?.kind === 'board' && !this.permission) {
452
- const board = this.work, lines = [tone.accent(` Trabajo · plan v${board?.plan?.version || 0}`), ...wrap(safeText(board?.plan?.objective || 'Sin plan. Pide una tarea con pasos y comprobaciones.'), columns - 4).map(line => ' ' + line), tone.muted(' /work resume continúa · rueda/PgUp/PgDn desplaza · Esc vuelve'), ''];
453
- const labels = { pending: 'PENDIENTE', in_progress: 'EN CURSO', blocked: 'BLOQUEADO', done: 'HECHO · checks registrados', needs_verification: 'POR VERIFICAR' };
454
- for (const [status, label] of Object.entries(labels)) { const cards = board?.cards.filter(card => card.status === status) || []; if (!cards.length) continue; lines.push(tone.accent(` ${label} (${cards.length})`)); for (const card of cards) { lines.push(...wrap(safeText(`${card.id} · ${card.title}`), columns - 6).map(line => ' ' + line)); if (card.held_by?.length || card.reason) lines.push(...wrap(safeText(card.reason || `Espera: ${card.held_by.join(', ')}`), columns - 8).map(line => ' ' + tone.muted(line))); } lines.push(''); }
455
- if (board?.next_action) lines.push(...wrap(safeText('Siguiente: ' + board.next_action), columns - 4).map(line => ' ' + line));
541
+ const board = this.work, lines = [tone.accent(t(" Work · plan v{v0}", {v0: board?.plan?.version || 0})), ...wrap(safeText(board?.plan?.objective || t("No plan. Ask for a task with steps and checks.")), columns - 4).map(line => ' ' + line), tone.muted(t(" /work resume continues · wheel/PgUp/PgDn scrolls · Esc returns")), ''];
542
+ const labels = { pending: t("PENDING"), in_progress: t("IN PROGRESS"), blocked: t("BLOCKED"), done: t("DONE · checks recorded"), needs_verification: t("TO VERIFY") };
543
+ for (const [status, label] of Object.entries(labels)) { const cards = board?.cards.filter(card => card.status === status) || []; if (!cards.length) continue; lines.push(tone.accent(` ${label} (${cards.length})`)); for (const card of cards) { lines.push(...wrap(safeText(`${card.id} · ${card.title}`), columns - 6).map(line => ' ' + line)); if (card.held_by?.length || card.reason) lines.push(...wrap(safeText(card.reason || t("Waiting: {v0}", {v0: card.held_by.join(', ')})), columns - 8).map(line => ' ' + tone.muted(line))); } lines.push(''); }
544
+ if (board?.next_action) lines.push(...wrap(safeText(t("Next: ") + board.next_action), columns - 4).map(line => ' ' + line));
456
545
  this.overlay.scroll = Math.min(this.overlay.scroll, Math.max(0, lines.length - height)); return lines.slice(this.overlay.scroll, this.overlay.scroll + height);
457
546
  }
458
547
  if (this.permission) {
459
548
  const permission = this.permission, details = wrap(safeText(JSON.stringify(permission.input, null, 2)), columns - 6);
460
549
  permission.scroll = Math.min(permission.scroll, Math.max(0, details.length - height + 5));
461
- return [tone.warning(` Autorizar ${permission.name}`), tone.muted(` Detalle ${permission.scroll + 1}/${details.length} · PgUp/PgDn para revisar`), '', ...details.slice(permission.scroll, permission.scroll + height - 5).map(line => ' ' + line), '', ` ${permission.allowed ? tone.good('❯ Sí') : ' Sí'} ${!permission.allowed ? tone.warning('❯ No') : ' No'} · Enter confirma · y/n · Esc deniega`];
550
+ return [tone.warning(t(" Allow {v0}", {v0: permission.name})), tone.muted(t(" Detail {v0}/{v1} · PgUp/PgDn to review", {v0: permission.scroll + 1, v1: details.length})), '', ...details.slice(permission.scroll, permission.scroll + height - 5).map(line => ' ' + line), '', t(" {v0} {v1} · Enter confirms · y/n · Esc denies", {v0: permission.allowed ? tone.good(t("❯ Yes")) : t(" Yes"), v1: !permission.allowed ? tone.warning('❯ No') : ' No'})];
462
551
  }
463
552
  if (this.overlay.kind === 'help') {
464
- const lines = [tone.muted(' Ayuda · composer activo · escribe abajo · PgUp/PgDn desplaza · Esc/F1 cierra'), '', ...this.markdown.render(HELP, columns - 4).map(line => ' ' + line)];
553
+ const lines = [tone.muted(t(" Help · composer active · type below · PgUp/PgDn scrolls · Esc/F1 closes")), '', ...this.markdown.render(t(HELP), columns - 4).map(line => ' ' + line)];
465
554
  this.overlay.scroll = Math.min(this.overlay.scroll, Math.max(0, lines.length - height));
466
555
  return lines.slice(this.overlay.scroll, this.overlay.scroll + height);
467
556
  }
468
557
  if (this.overlay.kind === 'sessions') {
469
558
  const items = this.filteredSessions(); const visible = Math.max(1, height - 5), start = Math.max(0, this.overlay.index - visible + 1);
470
- return [tone.accent(' Sesiones · ' + this.overlay.items.length), ` Buscar: ${this.overlay.filter || tone.muted('escribe para filtrar')}`, this.overlay.error ? tone.bad(' ' + this.overlay.error) : tone.muted(' ↑/↓ elegir · Enter reanuda con historial · Esc vuelve'), '', ...items.slice(start, start + visible).map((item, i) => {
471
- const selected = start + i === this.overlay.index, title = `${item.invalid ? '✗ ' : ''}${item.name || item.prompt || '(vacía)'}`;
559
+ return [tone.accent(t(" Sessions · ") + this.overlay.items.length), t(" Search: {v0}", {v0: this.overlay.filter || tone.muted(t("type to filter"))}), this.overlay.error ? tone.bad(' ' + this.overlay.error) : tone.muted(t(" ↑/↓ choose · Enter resumes with history · Esc returns")), '', ...items.slice(start, start + visible).map((item, i) => {
560
+ const selected = start + i === this.overlay.index, title = `${item.invalid ? '✗ ' : ''}${item.name || item.prompt || t("(empty)")}`;
472
561
  const left = pad(safeText(`${selected ? '❯' : ' '} ${title}`), Math.max(8, columns - 40));
473
562
  const right = safeText(`${item.id.slice(0, 8)} ${item.messages} msgs ${item.model}`);
474
563
  return ' ' + (selected ? tone.accent : String)(clip(left + right, columns - 4));
475
- }), ...(!items.length ? [tone.muted(' No hay coincidencias.')] : [])];
564
+ }), ...(!items.length ? [tone.muted(t(" No matches."))] : [])];
476
565
  }
477
- const status = this.queue.paused ? 'pausada' : 'activa';
478
- const lines = [tone.accent(` Cola de mensajes · ${status}`), tone.muted(' e editar · d quitar · Ctrl+↑/↓ ordenar · Espacio pausar/reanudar · Esc volver'), ''];
479
- if (this.queue.running) lines.push(tone.muted(' En ejecución: ' + clip(safeText(this.queue.running.text), columns - 20)), '');
566
+ const status = this.queue.paused ? t("paused") : t("active");
567
+ const lines = [tone.accent(t(" Message queue · {v0}", {v0: status})), tone.muted(t(" e edit · d remove · Ctrl+↑/↓ reorder · Space pause/resume · Esc return")), ''];
568
+ if (this.queue.running) lines.push(tone.muted(t(" Running: ") + clip(safeText(this.queue.running.text), columns - 20)), '');
480
569
  this.queue.items.forEach((item, index) => { lines.push((index === this.overlay.index ? tone.accent : String)(` ${index === this.overlay.index ? '❯' : ' '} ${index + 1}. ${clip(safeText(item.text).replace(/\n/g, ' ↵ '), columns - 9)}`)); });
481
- if (!this.queue.items.length) lines.push(tone.muted(' Cola vacía. Envía un mensaje mientras el agente trabaja.'));
482
- if (this.queue.interrupted?.length) lines.push('', tone.warning(` ${this.queue.interrupted.length} mensaje(s) interrumpidos; no se reenviaron. R reencola el primero.`));
570
+ if (!this.queue.items.length) lines.push(tone.muted(t(" Empty queue. Send a message while the agent works.")));
571
+ if (this.queue.interrupted?.length) lines.push('', tone.warning(t(" {v0} interrupted message(s); not resent. R requeues the first.", {v0: this.queue.interrupted.length})));
483
572
  return lines.slice(0, height);
484
573
  }
485
574
  render(selectionEntry = false) {
486
575
  if (this.closed || this.suspended || (this.selecting && !selectionEntry)) return;
487
576
  const columns = Math.max(1, (this.output.columns || 80) - 1), rows = this.output.rows || 24;
488
- if (columns < 23 || rows < 10) { this.draw([clip('Amplía el terminal a 24×10', columns)], null, columns, rows); return; }
577
+ if (columns < 23 || rows < 10) { this.draw([clip(t("Enlarge the terminal to 24×10"), columns)], null, columns, rows); return; }
578
+ if (this.setup) { this.renderSetup(columns, rows); return; }
489
579
  const composer = this.composer.layout(columns - 6), composerHeight = Math.min(6, rows - 8, Math.max(2, composer.lines.length));
490
- const location = ` ${clip(safeText(this.metadata.cwd || ''), Math.max(4, columns - 27))} · ${safeText(this.metadata.sessionId?.slice(0, 8) || 'conectando')}`;
580
+ const location = ` ${clip(safeText(this.metadata.cwd || ''), Math.max(4, columns - 27))} · ${safeText(this.metadata.sessionId?.slice(0, 8) || t("connecting"))}`;
491
581
  const report = this.metadata.modelCapabilities;
492
582
  const budget = this.metadata.contextBudget;
493
583
  const capacity = budget?.contextWindow;
@@ -495,13 +585,13 @@ export class TerminalScreen {
495
585
  const capacityLabel = `Ctx:${amount}${amount === '?' ? '' : budget.kind === 'fallback' ? '?' : budget.kind === 'configured' || budget.source === 'comal_limit' ? '~' : ''}`;
496
586
  const indicators = [`E:${safeText(this.metadata.effort || 'auto')}`, capacityLabel];
497
587
  if (this.metadata.workflow) indicators.push(safeText(this.metadata.workflow));
498
- for (const [key, label] of [['thinking', 'think'], ['vision', 'visión'], ['video', 'video'], ['web_search', 'web']]) {
588
+ for (const [key, label] of [['thinking', 'think'], ['vision', t("vision")], ['video', 'video'], ['web_search', 'web']]) {
499
589
  if (report?.capabilities?.[key] === true) indicators.push(label + (report.provenance?.[key]?.kind === 'configured' ? '~' : ''));
500
590
  }
501
591
  if (report && Object.values(report.capabilities).some(value => value === null)) indicators.push('caps?');
502
592
  const modelLabel = clip(' ' + indicators.join(' · '), Math.max(8, Math.floor(columns / 2)));
503
593
  const locationLine = columns >= 50 ? pad(clip(location, columns - width(modelLabel) - 1), columns - width(modelLabel)) + modelLabel : location;
504
- const header = [tone.bold(' COMAL') + (this.metadata.name ? tone.accent(safeText(` @${this.metadata.name}`)) : '') + tone.muted(safeText(` ${this.metadata.model || 'modelo local'} · ${this.metadata.authProvider || this.metadata.provider || 'openai'} · `)) + tone.accent(this.mode), tone.muted(locationLine), tone.muted('─'.repeat(columns))];
594
+ const header = [tone.bold(' COMAL') + (this.metadata.name ? tone.accent(safeText(` @${this.metadata.name}`)) : '') + tone.muted(safeText(` ${this.metadata.model || t("local model")} · ${this.metadata.authProvider || this.metadata.provider || 'openai'} · `)) + tone.accent(this.mode), tone.muted(locationLine), tone.muted('─'.repeat(columns))];
505
595
  const completions = this.completion();
506
596
  const availableBody = Math.max(1, rows - header.length - composerHeight - 4);
507
597
  const completionHeight = completions.items.length ? Math.min(5, Math.max(0, availableBody - 1)) : 0;
@@ -518,19 +608,19 @@ export class TerminalScreen {
518
608
  }
519
609
  while (body.length < bodyHeight) body.push(''); body = body.slice(0, bodyHeight);
520
610
  if (sideWidth) {
521
- const side = [tone.accent(` Cola · ${this.queue.items.length}${this.queue.paused ? ' pausada' : ''}`), tone.muted(' F4 gestionar'), ''];
522
- if (this.queue.running) side.push(tone.muted(' En ejecución'), ...wrap(safeText(this.queue.running.text), sideWidth - 2).slice(0, 2).map(line => ' ' + line), '');
611
+ const side = [tone.accent(t(" Queue · {v0}{v1}", {v0: this.queue.items.length, v1: this.queue.paused ? t(" paused") : ''})), tone.muted(t(" F4 manage")), ''];
612
+ if (this.queue.running) side.push(tone.muted(t(" Running")), ...wrap(safeText(this.queue.running.text), sideWidth - 2).slice(0, 2).map(line => ' ' + line), '');
523
613
  this.queue.items.slice(0, 6).forEach((item, i) => side.push(tone.accent(` ${i + 1}. `) + clip(safeText(item.text).replace(/\n/g, ' ↵ '), sideWidth - 5), ''));
524
614
  body = body.map((line, i) => pad(line, bodyWidth) + tone.muted(' │') + clip(side[i] || '', sideWidth));
525
615
  }
526
616
  const elapsed = this.startedAt ? ((Date.now() - this.startedAt) / 1000).toFixed(1) : '0.0';
527
617
  const active = this.busy || !this.ready;
528
- const contextLabel = this.context ? ` · ctx ${this.context.estimated===false?'':'~'}${this.context.percent}%${this.context.accounting_method==='measured_prefix_estimated_tail'?' · base medida':''}${Number.isSafeInteger(this.cacheReadTokens)?' · caché '+this.cacheReadTokens.toLocaleString():''}` : '';
529
- const status = (active ? `${brandingStatus({frame:this.brandFrame(),active:true,env:this.env,stream:this.output})} ${this.spinner()} ${this.phase} · ${elapsed}s` : `● Listo · ${this.usage.toLocaleString()} tokens`) + contextLabel;
530
- const measure = this.goal?.status === 'active' ? `goal calls ${this.goal.calls}/${this.goal.limits.maxCalls} ${progressBar(this.goal.calls, this.goal.limits.maxCalls, 12)}` : this.busy ? `calls ${this.turn}/${this.maxTurns} ${progressBar(this.turn, this.maxTurns, 12)}` : `${this.queue.items.length} en cola${this.queue.paused ? ' · pausada' : ''}`;
531
- const workLabel = this.work?.plan ? ` · trabajo ${this.work.counts.done}/${this.work.cards.length}${this.work.current ? ' · ' + this.work.current.id : ''}` : '';
532
- const bar = tone.accent(' ' + status + workLabel) + ' ' + tone.muted(measure) + (this.scroll ? tone.warning(' · historial ↑') : '');
533
- let composerHeading = this.editingQueue ? tone.warning(' Editando mensaje en cola · Enter guarda · Esc cancela') : tone.muted(` Composer · ${this.busy ? 'Enter encola' : 'Enter envía'} · Alt+Enter multilínea · Ctrl+G editor`);
618
+ const contextLabel = this.context ? ` · ctx ${this.context.estimated===false?'':'~'}${this.context.percent}%${this.context.accounting_method==='measured_prefix_estimated_tail'?t(" · measured baseline"):''}${Number.isSafeInteger(this.cacheReadTokens)?t(" · cache ")+formatNumber(this.cacheReadTokens):''}` : '';
619
+ const status = (active ? `${brandingStatus({frame:this.brandFrame(),active:true,env:this.env,stream:this.output})} ${this.spinner()} ${this.phase} · ${elapsed}s` : t("● Ready · {v0} tokens", {v0: formatNumber(this.usage)})) + contextLabel;
620
+ const measure = this.goal?.status === 'active' ? `goal calls ${this.goal.calls}/${this.goal.limits.maxCalls} ${progressBar(this.goal.calls, this.goal.limits.maxCalls, 12)}` : this.busy ? `calls ${this.turn}/${this.maxTurns} ${progressBar(this.turn, this.maxTurns, 12)}` : t("{v0} in queue{v1}", {v0: this.queue.items.length, v1: this.queue.paused ? t(" · paused") : ''});
621
+ const workLabel = this.work?.plan ? t(" · work {v0}/{v1}{v2}", {v0: this.work.counts.done, v1: this.work.cards.length, v2: this.work.current ? ' · ' + this.work.current.id : ''}) : '';
622
+ const bar = tone.accent(' ' + status + workLabel) + ' ' + tone.muted(measure) + (this.scroll ? tone.warning(t(" · history ↑")) : '');
623
+ let composerHeading = this.editingQueue ? tone.warning(t(" Editing queued message · Enter saves · Esc cancels")) : tone.muted(t(" Composer · {v0} · Alt+Enter multiline · Ctrl+G editor", {v0: this.busy ? t("Enter queues") : t("Enter sends")}));
534
624
  const sessionName = safeText(this.metadata.name || '').replace(/[\r\n]+/g, ' ').trim();
535
625
  if (sessionName) {
536
626
  const labelWidth = Math.min(Math.floor(columns / 2), columns - 12);
@@ -541,10 +631,10 @@ export class TerminalScreen {
541
631
  const footer = [clip(bar, columns), tone.muted('─'.repeat(columns)), composerHeading];
542
632
  const offset = Math.max(0, composer.cursorRow - composerHeight + 1);
543
633
  const inputLines = Array.from({ length: composerHeight }, (_, i) => tone.muted(i === 0 ? ' ❯ ' : ' │ ') + (composer.lines[offset + i] || ''));
544
- const hints = this.selecting ? tone.warning(' SELECCIÓN · Arrastra y copia desde tu terminal · F5/Esc vuelve') : tone.muted(' ' + (completions.items.length ? 'Tab o Enter completa · ↑/↓ elige · Esc cierra · no envía' : (this.overlay?.kind === 'help' ? 'Ayuda abierta · composer activo · PgUp/PgDn desplaza · Esc/F1 cierra' : this.preparedDraft ? 'Acción preparada · Esc recupera tu borrador · Enter envía y lo restaura' : contextualTip(this.composer.text, this.discoveryContext()))));
634
+ const hints = this.selecting ? tone.warning(t(" SELECTION · Drag and copy from your terminal · F5/Esc returns")) : this.attachmentStatus ? (['unsupported','invalid'].includes(this.attachmentStatus.state) ? tone.warning : tone.muted)(' ' + safeText(this.attachmentStatus.message)) : tone.muted(' ' + (completions.items.length ? t("Tab or Enter completes · ↑/↓ chooses · Esc closes · does not send") : (this.overlay?.kind === 'help' ? t("Help open · composer active · PgUp/PgDn scrolls · Esc/F1 closes") : this.preparedDraft ? t("Action prepared · Esc recovers your draft · Enter sends and restores it") : contextualTip(this.composer.text, this.discoveryContext()))));
545
635
  const suggestionLines = [];
546
636
  if (completionHeight) {
547
- suggestionLines.push(tone.muted(` Sugerencias · ${completions.items.length} · Tab completa`));
637
+ suggestionLines.push(tone.muted(t(" Suggestions · {v0} · Tab completes", {v0: completions.items.length})));
548
638
  const visible = completionHeight - 1, start = Math.max(0, this.completionIndex - visible + 1);
549
639
  for (const [i,item] of completions.items.slice(start, start + visible).entries()) {
550
640
  const selected = start + i === this.completionIndex;
@@ -578,6 +668,7 @@ export class TerminalScreen {
578
668
  resume() { this.input.setRawMode?.(true); this.input.resume(); this.suspended = false; this.previous = []; this.output.write('\x1b[?1049h\x1b[?2004h' + MOUSE_ON); this.render(); }
579
669
  close() {
580
670
  if (this.closed) return; this.closed = true;
671
+ this.clearSetup();
581
672
  this.permission?.finish(false); clearInterval(this.timer); clearTimeout(this.renderTimer); this.decoder?.close();
582
673
  if (this.onData) this.input.off('data', this.onData); if (this.onResize) this.output.off('resize', this.onResize);
583
674
  if (this.started) { this.input.setRawMode?.(this.originalRaw); this.input.pause(); if (!this.suspended) this.output.write('\x1b[0m' + MOUSE_OFF + '\x1b[?2004l\x1b[?25h\x1b[?1049l'); }