@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,23 +1,88 @@
1
- import { graphemes, safeText, width } from './text.mjs';
1
+ import { t } from '../i18n/index.mjs';
2
+ import { clip, graphemes, safeText, width } from './text.mjs';
3
+
4
+ const normalize = text => safeText(String(text).replace(/\r\n?/g, '\n'), { preserveTabs: true });
5
+ const IMAGE_COUNT = 2;
6
+ const imageLabel = /^\[Image ([1-9]\d*)\]$/;
2
7
 
3
8
  export class Composer {
4
- constructor(text = '') { this.history = []; this.historyIndex = -1; this.scratch = ''; this.setText(text); }
9
+ constructor(text = '') { this.history = []; this.historyAttachments = []; this.historyIndex = -1; this.scratch = ''; this.nextImage = 1; this.setText(text); }
5
10
  get text() { return this.buffer.join(''); }
6
- setText(text, cursor) {
7
- this.buffer = graphemes(safeText(String(text).replace(/\r\n?/g, '\n'), { preserveTabs: true }));
11
+ // Positions are UTF-16 offsets in display text, not filesystem paths. Only
12
+ // explicit host metadata may restore an attachment; a typed label is text.
13
+ get attachments() {
14
+ const result = []; let start = 0;
15
+ for (const atom of this.buffer) {
16
+ const descriptor = this.attachmentMap.get(atom);
17
+ if (descriptor) result.push({ ...structuredClone(descriptor), start, end: start + atom.length });
18
+ start += atom.length;
19
+ }
20
+ return result;
21
+ }
22
+ get attachmentDescriptors() { return this.attachments; }
23
+ snapshot() { return { text: this.text, cursor: this.cursor, attachments: this.attachments }; }
24
+ setText(text, cursor, attachments = []) {
25
+ const clean = normalize(text); this.attachmentMap = new Map();
26
+ const spans = [];
27
+ for (const candidate of attachments || []) {
28
+ if (!candidate || typeof candidate !== 'object' || !imageLabel.test(candidate.label || '') || spans.length >= IMAGE_COUNT || this.attachmentMap.has(candidate.label)) continue;
29
+ const start = Number.isInteger(candidate.start) ? candidate.start : clean.indexOf(candidate.label);
30
+ const end = start + candidate.label.length;
31
+ if (start < 0 || clean.slice(start, end) !== candidate.label || spans.some(span => start < span.end && end > span.start)) continue;
32
+ // Refuse offsets inside another grapheme, including joined emoji.
33
+ const boundaries = new Set([0]); let offset = 0;
34
+ for (const atom of graphemes(clean)) { offset += atom.length; boundaries.add(offset); }
35
+ if (!boundaries.has(start) || !boundaries.has(end)) continue;
36
+ const { start: _start, end: _end, ...descriptor } = structuredClone(candidate);
37
+ this.attachmentMap.set(candidate.label, descriptor); spans.push({ start, end, label: candidate.label });
38
+ this.nextImage = Math.max(this.nextImage, Number(imageLabel.exec(candidate.label)[1]) + 1);
39
+ }
40
+ spans.sort((a, b) => a.start - b.start); this.buffer = []; let offset = 0;
41
+ for (const span of spans) { this.buffer.push(...graphemes(clean.slice(offset, span.start)), span.label); offset = span.end; }
42
+ this.buffer.push(...graphemes(clean.slice(offset)));
8
43
  this.cursor = Math.max(0, Math.min(cursor ?? this.buffer.length, this.buffer.length));
9
44
  }
45
+ addAttachment(descriptor, range) {
46
+ if (!descriptor || typeof descriptor !== 'object') throw new Error(t("Invalid image attachment."));
47
+ if (this.attachments.length >= IMAGE_COUNT) throw new Error(t("Attach up to two images per message."));
48
+ const source = this.text;
49
+ const start = range?.start ?? this.buffer.slice(0, this.cursor).join('').length;
50
+ const end = range?.end ?? start;
51
+ let offset = 0; const boundaries = new Map([[0, 0]]);
52
+ for (let i = 0; i < this.buffer.length; i++) { offset += this.buffer[i].length; boundaries.set(offset, i + 1); }
53
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start > end || !boundaries.has(start) || !boundaries.has(end)) throw new Error(t("Invalid image attachment position."));
54
+ let label = descriptor.label;
55
+ if (!imageLabel.test(label || '') || source.includes(label)) {
56
+ do { label = `[Image ${this.nextImage++}]`; } while (source.includes(label));
57
+ } else this.nextImage = Math.max(this.nextImage, Number(imageLabel.exec(label)[1]) + 1);
58
+ const next = source.slice(0, start) + label + source.slice(end);
59
+ if (Buffer.byteLength(next) > 1024 * 1024) throw new Error(t("The composer supports up to 1 MiB."));
60
+ const first = boundaries.get(start), last = boundaries.get(end);
61
+ this.buffer.splice(first, last - first, label);
62
+ if (this.cursor >= last) this.cursor += 1 - (last - first);
63
+ else if (this.cursor > first) this.cursor = first + 1;
64
+ const { start: _start, end: _end, ...value } = structuredClone(descriptor);
65
+ this.attachmentMap.set(label, { ...value, label }); this.historyIndex = -1; this.pruneAttachments();
66
+ return this.attachments.find(attachment => attachment.label === label);
67
+ }
68
+ pruneAttachments() { for (const label of this.attachmentMap.keys()) if (!this.buffer.includes(label)) this.attachmentMap.delete(label); }
10
69
  insert(text) {
11
70
  const inserted = graphemes(safeText(String(text).replace(/\r\n?/g, '\n'), { preserveTabs: true }));
12
- if (Buffer.byteLength(this.text + inserted.join('')) > 1024 * 1024) throw new Error('El composer admite hasta 1 MiB.');
71
+ if (Buffer.byteLength(this.text + inserted.join('')) > 1024 * 1024) throw new Error(t("The composer supports up to 1 MiB."));
13
72
  this.buffer = this.buffer.slice(0, this.cursor).concat(inserted, this.buffer.slice(this.cursor)); this.cursor += inserted.length; this.historyIndex = -1;
14
73
  }
15
74
  clear() { this.setText(''); this.historyIndex = -1; }
16
- remember(text) { if (text.trim() && this.history.at(-1) !== text) this.history.push(text); this.history = this.history.slice(-100); this.historyIndex = -1; }
75
+ remember(text, attachments = text === this.text ? this.attachments : []) {
76
+ if (text.trim()) {
77
+ if (this.history.at(-1) !== text) { this.history.push(text); this.historyAttachments.push(structuredClone(attachments)); }
78
+ else this.historyAttachments[this.history.length - 1] = structuredClone(attachments);
79
+ }
80
+ this.history = this.history.slice(-100); this.historyAttachments = this.historyAttachments.slice(-100); this.historyIndex = -1;
81
+ }
17
82
  boundary(direction) {
18
83
  let cursor = this.cursor;
19
- if (direction < 0) { while (cursor > 0 && /\s/.test(this.buffer[cursor - 1])) cursor--; while (cursor > 0 && !/\s/.test(this.buffer[cursor - 1])) cursor--; }
20
- else { while (cursor < this.buffer.length && !/\s/.test(this.buffer[cursor])) cursor++; while (cursor < this.buffer.length && /\s/.test(this.buffer[cursor])) cursor++; }
84
+ if (direction < 0) { while (cursor > 0 && /^\s+$/.test(this.buffer[cursor - 1])) cursor--; while (cursor > 0 && !/^\s+$/.test(this.buffer[cursor - 1])) cursor--; }
85
+ else { while (cursor < this.buffer.length && !/^\s+$/.test(this.buffer[cursor])) cursor++; while (cursor < this.buffer.length && /^\s+$/.test(this.buffer[cursor])) cursor++; }
21
86
  return cursor;
22
87
  }
23
88
  lineStart() { let c = this.cursor; while (c > 0 && this.buffer[c - 1] !== '\n') c--; return c; }
@@ -39,9 +104,9 @@ export class Composer {
39
104
  }
40
105
  recall(direction) {
41
106
  if (!this.history.length) return;
42
- if (this.historyIndex === -1) { if (direction > 0) return; this.scratch = this.text; this.historyIndex = this.history.length; }
107
+ if (this.historyIndex === -1) { if (direction > 0) return; this.scratch = this.text; this.scratchAttachments = this.attachments; this.historyIndex = this.history.length; }
43
108
  this.historyIndex = Math.max(0, Math.min(this.history.length, this.historyIndex + direction));
44
- this.setText(this.historyIndex === this.history.length ? this.scratch : this.history[this.historyIndex]);
109
+ this.setText(this.historyIndex === this.history.length ? this.scratch : this.history[this.historyIndex], undefined, this.historyIndex === this.history.length ? this.scratchAttachments : this.historyAttachments[this.historyIndex]);
45
110
  }
46
111
  key(text, key = {}, columns) {
47
112
  const name = key.name;
@@ -58,11 +123,12 @@ export class Composer {
58
123
  else if (name === 'down' || (key.ctrl && name === 'n')) { if (key.ctrl || !this.vertical(1, columns)) this.recall(1); }
59
124
  else if ((name === 'return' && (key.meta || key.shift)) || name === 'linefeed' || (key.ctrl && name === 'j')) this.insert('\n');
60
125
  else if (!key.ctrl && !key.meta && text && !text.startsWith('\x1b')) this.insert(text);
126
+ this.pruneAttachments();
61
127
  }
62
128
  layout(columns) {
63
129
  columns = Math.max(2, columns); const lines = [''], positions = []; let row = 0, column = 0, cursorRow = 0, cursorColumn = 0;
64
130
  for (let i = 0; i <= this.buffer.length; i++) {
65
- const atom = this.buffer[i], visible = atom === '\t' ? ' ' : atom;
131
+ const atom = this.buffer[i], visible = this.attachmentMap.has(atom) ? clip(atom, columns, columns > 1 ? '…' : '') : atom === '\t' ? ' ' : atom;
66
132
  const size = atom && atom !== '\n' ? width(visible) : 0;
67
133
  if (atom && atom !== '\n' && column + size > columns) { lines.push(''); row++; column = 0; }
68
134
  positions.push({ index: i, row, column });
@@ -1,3 +1,4 @@
1
+ import { t } from '../i18n/index.mjs';
1
2
  import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -5,14 +6,18 @@ import { spawn } from 'node:child_process';
5
6
  import { once } from 'node:events';
6
7
  import { MessageQueue, QueueRunner } from './message-queue.mjs';
7
8
  import { shellCommands } from '../auto-guard.mjs';
9
+ import { findImageCandidates } from '../image-attachments.mjs';
10
+ import { Composer } from './composer.mjs';
11
+ import { isRemoteCommand } from '../remote-commands.mjs';
8
12
 
9
- const IMMEDIATE = new Set(['help', 'copy', 'select', 'status', 'context', 'tools', 'house', 'state', 'audit', 'board', 'tasks', 'work', 'operations', 'plugins', 'sessions', 'resume', 'clear', 'model', 'models', 'effort', 'capabilities', 'login', 'logout', 'permissions', 'rename', 'table', 'queue', 'agents', 'mcp','list-agents','inbox','providers','provider', 'exit', 'quit']);
13
+ const IMMEDIATE = new Set(['cloud', 'remote-control', 'rc', 'help', 'copy', 'select', 'status', 'context', 'tools', 'house', 'state', 'audit', 'board', 'tasks', 'work', 'operations', 'plugins', 'sessions', 'resume', 'clear', 'model', 'models', 'effort', 'capabilities', 'login', 'logout', 'permissions', 'rename', 'table', 'queue', 'agents', 'mcp','list-agents','inbox','providers','provider', 'exit', 'quit']);
10
14
 
11
15
  export class InteractiveController {
12
16
  constructor(ui, backend) { this.ui = ui; this.backend = backend; this.closed = false; this.switching = false; this.draftTimer = null; this.pendingDraft = null; }
13
17
  async start(session, initialPrompt = '') {
14
18
  this.attachSession(session);
15
- this.ui.configure({ onSubmit: text => this.submit(text), onAction: (name, arg) => this.action(name, arg), onDraft: text => this.scheduleDraft(text), onDiscover: () => this.refreshDiscovery() });
19
+ this.ui.configure({ onSubmit: (text, attachments) => this.submit(text, attachments), onPrepareSubmit: snapshot => this.prepareSubmit(snapshot), onAction: (name, arg) => this.action(name, arg), onDraft: (text, attachments) => this.scheduleDraft(text, attachments), onDiscover: () => this.refreshDiscovery(), onCapabilitiesChanged: () => this.previewImages() });
20
+ this.previewImages();
16
21
  if(this.backend.hasPeerMessages){this.peerTimer=setInterval(()=>this.kickPeers(),300);this.peerTimer.unref();}
17
22
  if (initialPrompt.trim()) await this.submit(initialPrompt);
18
23
  else if (session.goal?.status === 'active') await this.submit('/goal resume');
@@ -25,12 +30,13 @@ export class InteractiveController {
25
30
  const id = session.id;
26
31
  this.queue = new MessageQueue(session.queue, snapshot => this.backend.saveQueue(id, snapshot));
27
32
  this.runner = new QueueRunner(this.queue, async item => {
33
+ if(item.remote){await this.backend.validateRemote(item);if(item.text.trimStart().startsWith('/')&&!isRemoteCommand(item.text))throw new Error('This command is only available in the local terminal.');}
28
34
  this.ui.setBusy(true);
29
- try { return await this.backend.execute(item.text, { userIntent: item.text, shouldYield: () => this.queue.items.length > 0 }); }
35
+ try { return await this.backend.execute(item.text, { userIntent: item.text, attachments: item.attachments || [], shouldYield: () => this.queue.items.length > 0 }); }
30
36
  finally { this.ui.setBusy(false); }
31
37
  }, { blocked: () => Boolean(this.peerProcessing || this.ui.selecting || this.modalOverlay() || this.ui.editingQueue || this.switching || this.closed), onChange: () => { this.syncQueue(); this.refreshDiscovery(); }, onError: error => this.ui.notice(error.message, true) });
32
38
  this.syncQueue(); this.refreshDiscovery();
33
- if (this.queue.items.length || this.queue.interrupted.length) this.ui.notice('La cola recuperada está pausada. Revisa F4 antes de reanudar; los mensajes interrumpidos no se reenviaron.');
39
+ if (this.queue.items.length || this.queue.interrupted.length) this.ui.notice(t("The recovered queue is paused. Check F4 before resuming; interrupted messages were not resent."));
34
40
  }
35
41
  kickPeers(){
36
42
  if(this.peerProcessing || this.ui.busy || this.runner.processing || this.queue.paused || this.queue.items.length || this.modalOverlay() || this.ui.selecting || this.ui.editingQueue || this.switching || this.closed)return;
@@ -42,16 +48,80 @@ export class InteractiveController {
42
48
  })().catch(error=>this.ui.notice(error.message,true)).finally(()=>{this.peerProcessing=null;this.runner.kick();});
43
49
  }
44
50
  syncQueue() { this.ui.setQueue(this.queue.snapshot()); if (this.ui.overlay?.kind === 'queue') this.ui.overlay.index = Math.min(this.ui.overlay.index, Math.max(0, this.queue.items.length - 1)); }
45
- scheduleDraft(text) {
46
- clearTimeout(this.draftTimer); this.pendingDraft = { id: this.session.id, text };
51
+ scheduleDraft(text, attachments = this.ui.composer.attachments || []) {
52
+ clearTimeout(this.draftTimer); this.pendingDraft = { id: this.session.id, text, attachments };
47
53
  this.draftTimer = setTimeout(() => { this.flushDraft().catch(e => this.ui.notice(e.message, true)); }, 500);
54
+ clearTimeout(this.imageTimer);
55
+ if (this.backend.stageAttachment) this.imageTimer = setTimeout(() => this.previewImages(), 80);
48
56
  }
49
- async flushDraft() { clearTimeout(this.draftTimer); const pending = this.pendingDraft; this.pendingDraft = null; if (pending) await this.backend.saveDraft(pending.id, pending.text); }
50
- async submit(text) {
51
- if (this.closed || this.switching) throw new Error('La sesión está cambiando; conserva el mensaje y vuelve a enviarlo.');
57
+ async flushDraft() { clearTimeout(this.draftTimer); const pending = this.pendingDraft; this.pendingDraft = null; if (pending) await this.backend.saveDraft(pending.id, pending.text, pending.attachments); }
58
+ imageStatus(status) { this.ui.setAttachmentStatus?.(status); }
59
+ async normalizeImages(snapshot) {
60
+ if (!this.backend.stageAttachment) return snapshot;
61
+ const id = this.session.id, signature = JSON.stringify([id, snapshot.text, snapshot.attachments]);
62
+ if (this.imagePending?.signature === signature) return this.imagePending.promise;
63
+ const promise = (async () => {
64
+ const draft = new Composer(); draft.setText(snapshot.text, snapshot.cursor, snapshot.attachments);
65
+ const candidates = /^\/[a-z][\w-]*(?:\s|$)/i.test(snapshot.text.trimStart()) ? [] : findImageCandidates(snapshot.text, { cwd: this.backend.metadata().cwd });
66
+ for (const candidate of candidates.reverse()) {
67
+ if (draft.attachments.length >= 2) throw new Error(t('Attach up to two images per message.'));
68
+ if (this.closed || this.session.id !== id) throw new Error(t('The session changed; the image was not attached.'));
69
+ const descriptor = await this.backend.stageAttachment(candidate, id);
70
+ draft.addAttachment(descriptor, candidate);
71
+ }
72
+ return draft.snapshot();
73
+ })();
74
+ const pending = { signature, promise }; this.imagePending = pending;
75
+ try { return await promise; } finally { if (this.imagePending === pending) this.imagePending = null; }
76
+ }
77
+ async visionStatus(snapshot) {
78
+ const generation = this.visionGeneration = (this.visionGeneration || 0) + 1;
79
+ if (!snapshot.attachments?.length) { this.imageStatus(null); return true; }
80
+ const id = this.session.id, model = this.backend.metadata().model;
81
+ this.imageStatus({ state: 'checking', message: t('Checking vision support…') });
82
+ let capability;
83
+ try { capability = await this.backend.attachmentCapability?.(); }
84
+ catch { capability = { supported: false, source: 'unknown' }; }
85
+ if (this.closed || this.session.id !== id || this.backend.metadata().model !== model || this.visionGeneration !== generation) return false;
86
+ const supported = capability?.supported === true;
87
+ const message = supported ? t('{count} image(s) attached · Backspace removes a label', { count: snapshot.attachments.length })
88
+ : t('This model cannot receive images or vision is unverified. Switch model or remove the image before sending.');
89
+ if (this.ui.composer.text === snapshot.text) this.imageStatus({ state: supported ? 'ready' : 'unsupported', message });
90
+ return supported;
91
+ }
92
+ async previewImages() {
93
+ if (this.closed || this.switching || !this.backend.stageAttachment) return;
94
+ const id = this.session.id, snapshot = this.ui.composer.snapshot(), stamp = JSON.stringify(snapshot);
95
+ try {
96
+ if (findImageCandidates(snapshot.text, { cwd: this.backend.metadata().cwd }).length) this.imageStatus({ state: 'checking', message: t('Attaching image…') });
97
+ const prepared = await this.normalizeImages(snapshot);
98
+ if (this.closed || this.session.id !== id || JSON.stringify(this.ui.composer.snapshot()) !== stamp) return;
99
+ if (prepared.text !== snapshot.text) {
100
+ this.ui.composer.setText(prepared.text, prepared.cursor, prepared.attachments);
101
+ this.scheduleDraft(prepared.text, prepared.attachments); this.ui.schedule?.();
102
+ }
103
+ await this.visionStatus(prepared);
104
+ } catch (error) { if (!this.closed && this.session.id === id && this.ui.composer.text === snapshot.text) this.imageStatus({ state: 'invalid', message: error.message }); }
105
+ }
106
+ async prepareSubmit(snapshot) {
107
+ const id = this.session.id, prepared = await this.normalizeImages(snapshot);
108
+ if (this.closed || this.switching || this.session.id !== id) throw new Error(t('The session changed; the message was not sent.'));
109
+ if (JSON.stringify(this.ui.composer.snapshot()) === JSON.stringify(snapshot) && prepared.text !== snapshot.text) {
110
+ this.ui.composer.setText(prepared.text, prepared.cursor, prepared.attachments); this.scheduleDraft(prepared.text, prepared.attachments); this.ui.schedule?.();
111
+ }
112
+ if (!await this.visionStatus(prepared)) throw new Error(t('Image not sent: the current model does not support verified vision.'));
113
+ if (prepared.attachments?.length && prepared.text.trimStart().startsWith('/')) throw new Error(t('Use a plain message with images; commands do not accept image attachments.'));
114
+ return prepared;
115
+ }
116
+ async submit(text, attachments) {
117
+ if (this.closed || this.switching) throw new Error(t("The session is changing; keep the message and resend it."));
118
+ if (attachments === undefined && this.backend.stageAttachment) {
119
+ const prepared = await this.prepareSubmit({ text, attachments: [], cursor: undefined }); text = prepared.text; attachments = prepared.attachments;
120
+ }
121
+ attachments ||= [];
52
122
  if (this.ui.editingQueue) {
53
- const editing = this.ui.editingQueue; await this.queue.update(editing.id, text); this.ui.editingQueue = null;
54
- this.ui.setDraft(editing.draft); this.scheduleDraft(editing.draft); await this.queue.setPaused(editing.wasPaused); this.syncQueue(); this.runner.kick(); return;
123
+ const editing = this.ui.editingQueue; await this.queue.update(editing.id, text, attachments); this.ui.editingQueue = null;
124
+ this.ui.setDraft(editing.draft, editing.attachments); this.scheduleDraft(editing.draft, editing.attachments); await this.queue.setPaused(editing.wasPaused); this.syncQueue(); this.runner.kick(); return;
55
125
  }
56
126
  const trimmed = text.trim(), match = /^\/(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
57
127
  const mcpMutation = match?.[1] === 'mcp' && match[2] && !/^(?:status|list|config)(?:\s|$)/.test(match[2]);
@@ -65,21 +135,30 @@ export class InteractiveController {
65
135
  if (this.ui.busy && arg !== 'status') { await this.queue.setPaused(true); this.backend.interrupt(); await this.runner.processing; }
66
136
  await this.backend.goalControl(arg); this.syncQueue(); return;
67
137
  }
68
- await this.queue.enqueue(text); this.syncQueue(); this.runner.kick();
138
+ await this.queue.enqueue(text, attachments); this.syncQueue(); this.runner.kick();
139
+ }
140
+ async requireIdle() { if (this.peerProcessing || this.ui.busy || this.queue.running || this.runner.processing) throw new Error(t("Cancel the turn with Esc or Ctrl+C before switching session or model.")); }
141
+ async admitRemote(input, remote) {
142
+ if(this.closed || this.switching)throw new Error('Local session unavailable.');
143
+ const accepted=await this.queue.enqueueRemote(input.text,remote);this.syncQueue();this.runner.kick();return accepted;
144
+ }
145
+ async stopRemote(turnId) {
146
+ if(this.closed || !this.backend.remoteTurnMatches(turnId))return false;
147
+ await this.queue.setPaused(true);if(!this.backend.remoteTurnMatches(turnId))return false;this.backend.interrupt();this.syncQueue();return true;
69
148
  }
70
- async requireIdle() { if (this.peerProcessing || this.ui.busy || this.queue.running || this.runner.processing) throw new Error('Cancela el turno con Esc o Ctrl+C antes de cambiar sesión o modelo.'); }
71
149
  async command(command, arg) {
72
150
  if (['exit', 'quit'].includes(command)) return this.exit();
73
151
  if (command === 'copy' || command === 'select') {
74
- if (arg.trim()) throw new Error(`Usa /${command} sin argumentos.`);
152
+ if (arg.trim()) throw new Error(t("Use /{v0} without arguments.", {v0: command}));
75
153
  if (command === 'copy') await this.ui.copyResponse(); else this.ui.setSelection();
76
154
  return;
77
155
  }
156
+ if (['rc','remote-control'].includes(command) && ['', 'start'].includes(arg.trim()))await this.requireIdle();
78
157
  if (command === 'help') { this.ui.showHelp(); return; }
79
158
  if (command === 'sessions' || (command === 'resume' && !arg)) { this.ui.showSessions(await this.backend.listSessions()); return; }
80
159
  if (command === 'resume' || command === 'clear') {
81
160
  await this.requireIdle(); this.switching = true;
82
- try { await this.flushDraft(); const next = await this.backend.activate(command === 'clear' ? null : arg); this.ui.overlay = null; this.ui.editingQueue = null; this.attachSession(next); }
161
+ try { await this.backend.detachRemote?.(); await this.flushDraft(); const next = await this.backend.activate(command === 'clear' ? null : arg); this.ui.overlay = null; this.ui.editingQueue = null; this.attachSession(next); }
83
162
  finally { this.switching = false; }
84
163
  return;
85
164
  }
@@ -88,14 +167,15 @@ export class InteractiveController {
88
167
  else if (arg === 'pause') await this.queue.setPaused(true);
89
168
  else if (arg === 'resume') { await this.queue.setPaused(false); this.ui.closeOverlay(); this.runner.kick(); }
90
169
  else if (arg === 'clear') await this.queue.clear();
91
- else if (/^edit \d+$/.test(arg)) { const item = this.queue.items[Number(arg.slice(5)) - 1]; if (!item) throw new Error('Número de mensaje inválido.'); await this.action('queueEdit', item.id); }
92
- else throw new Error('Usa /queue, /queue pause|resume|clear o /queue edit N.');
170
+ else if (/^edit \d+$/.test(arg)) { const item = this.queue.items[Number(arg.slice(5)) - 1]; if (!item) throw new Error(t("Invalid message number.")); await this.action('queueEdit', item.id); }
171
+ else throw new Error(t("Use /queue, /queue pause|resume|clear or /queue edit N."));
93
172
  this.syncQueue(); return;
94
173
  }
174
+ if(command==='work'&&(arg.trim()==='recover'||arg.trim().startsWith('review '))){await this.requireIdle();const output=await this.backend.control(command,arg);if(output)this.ui.notice(output);return;}
95
175
  if (['board', 'tasks', 'work'].includes(command)) { this.ui.showBoard(JSON.parse(await this.backend.control('board', ''))); return; }
96
- if (['model', 'effort', 'provider', 'login', 'logout'].includes(command) && (arg.trim() || ['login', 'logout'].includes(command))) await this.requireIdle();
97
- if (command === 'table') { if (!arg) throw new Error('Usa /table archivo.csv|tsv|json|md.'); const table = await this.backend.readTable(arg); this.ui.addTable(table.text, table.format, arg); return; }
98
- const changing=['model','effort','provider','login','logout'].includes(command) && (arg.trim() || ['login','logout'].includes(command));
176
+ if (['cloud', 'model', 'effort', 'provider', 'login', 'logout'].includes(command) && (arg.trim() || ['login', 'logout'].includes(command))) await this.requireIdle();
177
+ if (command === 'table') { if (!arg) throw new Error(t("Use /table file.csv|tsv|json|md.")); const table = await this.backend.readTable(arg); this.ui.addTable(table.text, table.format, arg); return; }
178
+ const changing=(['cloud','model','effort','provider','login','logout'].includes(command) && (arg.trim() || ['login','logout'].includes(command))) || (['rc','remote-control'].includes(command) && ['', 'start'].includes(arg.trim()));
99
179
  if(changing)this.switching=true;
100
180
  try {
101
181
  const output=await this.backend.control(command,arg);if(output)this.ui.notice(output);
@@ -112,14 +192,14 @@ export class InteractiveController {
112
192
  if (name === 'resume') return this.command('resume', arg);
113
193
  if (name === 'queueKick') return this.runner.kick();
114
194
  if (name === 'queueEdit') {
115
- const item = this.queue.items.find(item => item.id === arg); if (!item) throw new Error('Ese mensaje ya se está ejecutando o salió de la cola.');
195
+ const item = this.queue.items.find(item => item.id === arg); if (!item) throw new Error(t("That message is already running or has left the queue."));
116
196
  const wasPaused = this.queue.paused; await this.queue.setPaused(true);
117
- this.ui.editingQueue = { id: arg, draft: this.ui.composer.text, wasPaused };
118
- this.ui.overlay = null; this.ui.setDraft(item.text); this.scheduleDraft(item.text); this.syncQueue(); return;
197
+ this.ui.editingQueue = { id: arg, draft: this.ui.composer.text, attachments: this.ui.composer.attachments, wasPaused };
198
+ this.ui.overlay = null; this.ui.setDraft(item.text, item.attachments); this.scheduleDraft(item.text, item.attachments); this.syncQueue(); return;
119
199
  }
120
200
  if (name === 'queueEditCancel') {
121
201
  const editing = this.ui.editingQueue; if (!editing) return;
122
- this.ui.editingQueue = null; this.ui.setDraft(editing.draft); this.scheduleDraft(editing.draft); await this.queue.setPaused(editing.wasPaused); this.syncQueue(); this.runner.kick(); return;
202
+ this.ui.editingQueue = null; this.ui.setDraft(editing.draft, editing.attachments); this.scheduleDraft(editing.draft, editing.attachments); await this.queue.setPaused(editing.wasPaused); this.syncQueue(); this.runner.kick(); return;
123
203
  }
124
204
  if (name === 'queueRemove') await this.queue.remove(arg);
125
205
  if (name === 'queueMove') { await this.queue.move(arg.id, arg.delta); if (this.ui.overlay?.kind === 'queue') this.ui.overlay.index = Math.max(0, this.queue.items.findIndex(item => item.id === arg.id)); }
@@ -129,21 +209,23 @@ export class InteractiveController {
129
209
  this.syncQueue(); this.runner.kick();
130
210
  }
131
211
  async externalEditor() {
212
+ const attachments = this.ui.composer.attachments || [];
132
213
  const commands = shellCommands(process.env.VISUAL || process.env.EDITOR || 'vi');
133
- if (!commands || commands.length !== 1 || !commands[0].length) throw new Error('$VISUAL/$EDITOR debe ser un ejecutable con argumentos, sin expansiones de shell.');
214
+ if (!commands || commands.length !== 1 || !commands[0].length) throw new Error(t("$VISUAL/$EDITOR must be an executable with arguments, without shell expansions."));
134
215
  const dir = await mkdtemp(path.join(os.tmpdir(), 'comal-composer-')), file = path.join(dir, 'message.md');
135
216
  await writeFile(file, this.ui.composer.text, { mode: 0o600 });
136
217
  const wasPaused = this.queue.paused; await this.queue.setPaused(true); this.syncQueue(); this.ui.suspend();
137
218
  try {
138
219
  const [command, ...args] = commands[0], child = spawn(command, [...args, file], { stdio: 'inherit' });
139
220
  const [code] = await once(child, 'close');
140
- if (code !== 0) throw new Error(`El editor terminó con código ${code}; el borrador anterior se conserva.`);
141
- const text = await readFile(file, 'utf8'); if (Buffer.byteLength(text) > 1024 * 1024) throw new Error('El editor produjo más de 1 MiB; el borrador anterior se conserva.');
142
- this.ui.setDraft(text); this.scheduleDraft(text);
221
+ if (code !== 0) throw new Error(t("The editor exited with code {v0}; the previous draft is kept.", {v0: code}));
222
+ const text = await readFile(file, 'utf8'); if (Buffer.byteLength(text) > 1024 * 1024) throw new Error(t("The editor produced more than 1 MiB; the previous draft is kept."));
223
+ const restored = attachments.map(({start,end,...attachment}) => attachment);
224
+ this.ui.setDraft(text, restored); this.scheduleDraft(text, this.ui.composer.attachments);
143
225
  } finally { await rm(dir, { recursive: true, force: true }); this.ui.resume(); await this.queue.setPaused(wasPaused); this.syncQueue(); this.runner.kick(); }
144
226
  }
145
227
  async exit() {
146
- if (this.closed) return; this.closed = true; clearInterval(this.peerTimer);this.runner.stopped = true; this.backend.interrupt();
228
+ if (this.closed) return; this.closed = true; clearInterval(this.peerTimer);clearTimeout(this.imageTimer);this.runner.stopped = true; this.backend.interrupt();
147
229
  this.ui.permission?.finish(false); await this.runner.processing;await this.peerProcessing; await this.flushDraft(); await this.queue.save(); this.ui.close();
148
230
  }
149
231
  }