@mragentix/cli 4.2.39 → 4.2.53

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 (223) hide show
  1. package/README.md +15 -6
  2. package/defaults/agents/bee.md +22 -0
  3. package/defaults/agents/db-manager.md +31 -0
  4. package/defaults/agents/deployer.md +35 -0
  5. package/defaults/agents/devops.md +36 -0
  6. package/defaults/agents/owl.md +24 -0
  7. package/defaults/agents/payments.md +29 -0
  8. package/defaults/agents/workspace.md +37 -0
  9. package/defaults/skills/act.md +65 -0
  10. package/defaults/skills/docker.md +84 -0
  11. package/defaults/skills/doppler.md +89 -0
  12. package/defaults/skills/drizzle.md +81 -0
  13. package/defaults/skills/firebase.md +83 -0
  14. package/defaults/skills/gh.md +91 -0
  15. package/defaults/skills/gws.md +72 -0
  16. package/defaults/skills/netlify.md +69 -0
  17. package/defaults/skills/prisma.md +62 -0
  18. package/defaults/skills/railway.md +69 -0
  19. package/defaults/skills/stripe.md +69 -0
  20. package/defaults/skills/supabase.md +84 -0
  21. package/defaults/skills/turso.md +78 -0
  22. package/defaults/skills/vercel.md +72 -0
  23. package/defaults/skills/wrangler.md +105 -0
  24. package/dist/cli.js +5 -5
  25. package/dist/cli.js.map +1 -1
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +4 -63
  28. package/dist/config.js.map +1 -1
  29. package/dist/core/agent-session.d.ts +1 -1
  30. package/dist/core/agent-session.d.ts.map +1 -1
  31. package/dist/core/agent-session.js +7 -7
  32. package/dist/core/agent-session.js.map +1 -1
  33. package/dist/core/agents.d.ts +9 -4
  34. package/dist/core/agents.d.ts.map +1 -1
  35. package/dist/core/agents.js +53 -8
  36. package/dist/core/agents.js.map +1 -1
  37. package/dist/core/auto-update.js +1 -1
  38. package/dist/core/auto-update.js.map +1 -1
  39. package/dist/core/compaction/compactor.d.ts +1 -1
  40. package/dist/core/compaction/compactor.d.ts.map +1 -1
  41. package/dist/core/compaction/compactor.js +1 -1
  42. package/dist/core/compaction/compactor.js.map +1 -1
  43. package/dist/core/compaction/compactor.test.js +2 -2
  44. package/dist/core/compaction/compactor.test.js.map +1 -1
  45. package/dist/core/compaction/token-estimator.d.ts +1 -1
  46. package/dist/core/compaction/token-estimator.d.ts.map +1 -1
  47. package/dist/core/event-bus.d.ts +1 -1
  48. package/dist/core/event-bus.d.ts.map +1 -1
  49. package/dist/core/extensions/types.d.ts +1 -1
  50. package/dist/core/extensions/types.d.ts.map +1 -1
  51. package/dist/core/index.d.ts +2 -3
  52. package/dist/core/index.d.ts.map +1 -1
  53. package/dist/core/index.js +2 -3
  54. package/dist/core/index.js.map +1 -1
  55. package/dist/core/mcp/client.d.ts +1 -1
  56. package/dist/core/mcp/client.d.ts.map +1 -1
  57. package/dist/core/mcp/defaults.d.ts +1 -1
  58. package/dist/core/mcp/defaults.d.ts.map +1 -1
  59. package/dist/core/model-registry.d.ts +1 -1
  60. package/dist/core/model-registry.d.ts.map +1 -1
  61. package/dist/core/process-manager.d.ts.map +1 -1
  62. package/dist/core/process-manager.js +2 -7
  63. package/dist/core/process-manager.js.map +1 -1
  64. package/dist/core/session-manager.d.ts +1 -1
  65. package/dist/core/session-manager.d.ts.map +1 -1
  66. package/dist/core/skills.d.ts +7 -1
  67. package/dist/core/skills.d.ts.map +1 -1
  68. package/dist/core/skills.js +53 -7
  69. package/dist/core/skills.js.map +1 -1
  70. package/dist/core/slash-commands.d.ts.map +1 -1
  71. package/dist/core/slash-commands.js +1 -97
  72. package/dist/core/slash-commands.js.map +1 -1
  73. package/dist/core/telegram.d.ts +19 -0
  74. package/dist/core/telegram.d.ts.map +1 -1
  75. package/dist/core/telegram.js +22 -0
  76. package/dist/core/telegram.js.map +1 -1
  77. package/dist/core/voice-transcriber.d.ts +33 -0
  78. package/dist/core/voice-transcriber.d.ts.map +1 -0
  79. package/dist/core/voice-transcriber.js +113 -0
  80. package/dist/core/voice-transcriber.js.map +1 -0
  81. package/dist/core/voice-transcriber.test.d.ts +2 -0
  82. package/dist/core/voice-transcriber.test.d.ts.map +1 -0
  83. package/dist/core/voice-transcriber.test.js +88 -0
  84. package/dist/core/voice-transcriber.test.js.map +1 -0
  85. package/dist/interactive.js +6 -6
  86. package/dist/interactive.js.map +1 -1
  87. package/dist/modes/json-mode.d.ts +1 -1
  88. package/dist/modes/json-mode.d.ts.map +1 -1
  89. package/dist/modes/json-mode.js +1 -1
  90. package/dist/modes/json-mode.js.map +1 -1
  91. package/dist/modes/print-mode.d.ts +1 -1
  92. package/dist/modes/print-mode.d.ts.map +1 -1
  93. package/dist/modes/print-mode.js +1 -1
  94. package/dist/modes/print-mode.js.map +1 -1
  95. package/dist/modes/rpc-mode.d.ts +1 -1
  96. package/dist/modes/rpc-mode.d.ts.map +1 -1
  97. package/dist/modes/rpc-mode.js +1 -1
  98. package/dist/modes/rpc-mode.js.map +1 -1
  99. package/dist/modes/serve-mode.d.ts +1 -1
  100. package/dist/modes/serve-mode.d.ts.map +1 -1
  101. package/dist/modes/serve-mode.js +41 -4
  102. package/dist/modes/serve-mode.js.map +1 -1
  103. package/dist/session.d.ts +1 -1
  104. package/dist/session.d.ts.map +1 -1
  105. package/dist/session.js +1 -8
  106. package/dist/session.js.map +1 -1
  107. package/dist/system-prompt.js +2 -2
  108. package/dist/system-prompt.js.map +1 -1
  109. package/dist/tools/bash.d.ts +1 -2
  110. package/dist/tools/bash.d.ts.map +1 -1
  111. package/dist/tools/bash.js +1 -1
  112. package/dist/tools/bash.js.map +1 -1
  113. package/dist/tools/edit.d.ts +1 -1
  114. package/dist/tools/edit.d.ts.map +1 -1
  115. package/dist/tools/enter-plan.d.ts +1 -1
  116. package/dist/tools/enter-plan.d.ts.map +1 -1
  117. package/dist/tools/exit-plan.d.ts +1 -1
  118. package/dist/tools/exit-plan.d.ts.map +1 -1
  119. package/dist/tools/exit-plan.js +2 -3
  120. package/dist/tools/exit-plan.js.map +1 -1
  121. package/dist/tools/find.d.ts +1 -1
  122. package/dist/tools/find.d.ts.map +1 -1
  123. package/dist/tools/grep.d.ts +1 -1
  124. package/dist/tools/grep.d.ts.map +1 -1
  125. package/dist/tools/index.d.ts +5 -2
  126. package/dist/tools/index.d.ts.map +1 -1
  127. package/dist/tools/index.js.map +1 -1
  128. package/dist/tools/ls.d.ts +1 -1
  129. package/dist/tools/ls.d.ts.map +1 -1
  130. package/dist/tools/plan-mode.test.js +6 -6
  131. package/dist/tools/plan-mode.test.js.map +1 -1
  132. package/dist/tools/read.d.ts +1 -1
  133. package/dist/tools/read.d.ts.map +1 -1
  134. package/dist/tools/skill.d.ts +1 -1
  135. package/dist/tools/skill.d.ts.map +1 -1
  136. package/dist/tools/subagent.d.ts +1 -1
  137. package/dist/tools/subagent.d.ts.map +1 -1
  138. package/dist/tools/task-output.d.ts +1 -1
  139. package/dist/tools/task-output.d.ts.map +1 -1
  140. package/dist/tools/task-stop.d.ts +1 -1
  141. package/dist/tools/task-stop.d.ts.map +1 -1
  142. package/dist/tools/tasks.d.ts +1 -1
  143. package/dist/tools/tasks.d.ts.map +1 -1
  144. package/dist/tools/tasks.js +1 -1
  145. package/dist/tools/tasks.js.map +1 -1
  146. package/dist/tools/web-fetch.d.ts +1 -1
  147. package/dist/tools/web-fetch.d.ts.map +1 -1
  148. package/dist/tools/web-fetch.js +1 -1
  149. package/dist/tools/web-fetch.js.map +1 -1
  150. package/dist/tools/write.d.ts +1 -1
  151. package/dist/tools/write.d.ts.map +1 -1
  152. package/dist/tools/write.js +3 -1
  153. package/dist/tools/write.js.map +1 -1
  154. package/dist/types.d.ts +1 -1
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/ui/App.d.ts +9 -3
  157. package/dist/ui/App.d.ts.map +1 -1
  158. package/dist/ui/App.js +49 -121
  159. package/dist/ui/App.js.map +1 -1
  160. package/dist/ui/activity-phrases.d.ts +10 -0
  161. package/dist/ui/activity-phrases.d.ts.map +1 -0
  162. package/dist/ui/activity-phrases.js +227 -0
  163. package/dist/ui/activity-phrases.js.map +1 -0
  164. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  165. package/dist/ui/components/ActivityIndicator.js +1 -225
  166. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  167. package/dist/ui/components/Banner.d.ts +1 -1
  168. package/dist/ui/components/Banner.d.ts.map +1 -1
  169. package/dist/ui/components/Banner.js +7 -4
  170. package/dist/ui/components/Banner.js.map +1 -1
  171. package/dist/ui/components/InputArea.d.ts.map +1 -1
  172. package/dist/ui/components/InputArea.js +453 -39
  173. package/dist/ui/components/InputArea.js.map +1 -1
  174. package/dist/ui/components/ModelSelector.d.ts +1 -1
  175. package/dist/ui/components/ModelSelector.d.ts.map +1 -1
  176. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  177. package/dist/ui/components/PlanOverlay.js +7 -3
  178. package/dist/ui/components/PlanOverlay.js.map +1 -1
  179. package/dist/ui/components/SkillsOverlay.d.ts.map +1 -1
  180. package/dist/ui/components/SkillsOverlay.js +6 -2
  181. package/dist/ui/components/SkillsOverlay.js.map +1 -1
  182. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  183. package/dist/ui/components/TaskOverlay.js +8 -4
  184. package/dist/ui/components/TaskOverlay.js.map +1 -1
  185. package/dist/ui/hooks/useAgentLoop.d.ts +2 -2
  186. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  187. package/dist/ui/hooks/useAgentLoop.js +3 -2
  188. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  189. package/dist/ui/hooks/useSessionManager.d.ts +2 -2
  190. package/dist/ui/hooks/useSessionManager.d.ts.map +1 -1
  191. package/dist/ui/hooks/useTerminalSize.d.ts +12 -2
  192. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  193. package/dist/ui/hooks/useTerminalSize.js +27 -6
  194. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  195. package/dist/ui/hooks/useTerminalTitle.d.ts +8 -1
  196. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  197. package/dist/ui/hooks/useTerminalTitle.js +20 -23
  198. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  199. package/dist/ui/login.d.ts +1 -1
  200. package/dist/ui/login.d.ts.map +1 -1
  201. package/dist/ui/login.js +5 -1
  202. package/dist/ui/login.js.map +1 -1
  203. package/dist/ui/plan-overlay.test.d.ts +2 -0
  204. package/dist/ui/plan-overlay.test.d.ts.map +1 -0
  205. package/dist/ui/plan-overlay.test.js +327 -0
  206. package/dist/ui/plan-overlay.test.js.map +1 -0
  207. package/dist/ui/render.d.ts +2 -2
  208. package/dist/ui/render.d.ts.map +1 -1
  209. package/dist/ui/render.js +1 -1
  210. package/dist/ui/render.js.map +1 -1
  211. package/dist/ui/sessions.d.ts.map +1 -1
  212. package/dist/ui/sessions.js +5 -1
  213. package/dist/ui/sessions.js.map +1 -1
  214. package/package.json +17 -14
  215. package/LICENSE +0 -21
  216. package/dist/core/branding.d.ts +0 -11
  217. package/dist/core/branding.d.ts.map +0 -1
  218. package/dist/core/branding.js +0 -11
  219. package/dist/core/branding.js.map +0 -1
  220. package/dist/core/update.d.ts +0 -34
  221. package/dist/core/update.d.ts.map +0 -1
  222. package/dist/core/update.js +0 -231
  223. package/dist/core/update.js.map +0 -1
@@ -6,8 +6,74 @@ import { useAnimationTick, deriveFrame } from "./AnimationContext.js";
6
6
  import { useTerminalSize } from "../hooks/useTerminalSize.js";
7
7
  import { extractImagePaths, readImageFile, getClipboardImage } from "../../utils/image.js";
8
8
  import { SlashCommandMenu, filterCommands } from "./SlashCommandMenu.js";
9
+ import { log } from "../../core/logger.js";
9
10
  const MAX_VISIBLE_LINES = 5;
10
11
  const PROMPT = "❯ ";
12
+ // SGR mouse sequence: ESC [ < button ; col ; row M/m
13
+ // M = press, m = release. Coordinates are 1-based.
14
+ // SGR mouse sequence (global) — used both to strip sequences from input data
15
+ // and to extract click coordinates. Must reset lastIndex before each use.
16
+ // eslint-disable-next-line no-control-regex
17
+ const SGR_MOUSE_RE_G = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
18
+ // Enable/disable escape sequences for SGR mouse tracking.
19
+ // ?1000h = basic click tracking, ?1006h = SGR extended mode (supports coords > 223).
20
+ const ENABLE_MOUSE = "\x1b[?1000h\x1b[?1006h";
21
+ const DISABLE_MOUSE = "\x1b[?1006l\x1b[?1000l";
22
+ // Option+Arrow escape sequences — terminals send these as raw input strings
23
+ // rather than setting key.meta + key.leftArrow reliably.
24
+ const OPTION_LEFT_SEQUENCES = new Set([
25
+ "\x1bb", // Meta+b (emacs style)
26
+ "\x1b[1;3D", // CSI 1;3 D (xterm with modifiers)
27
+ ]);
28
+ const OPTION_RIGHT_SEQUENCES = new Set([
29
+ "\x1bf", // Meta+f (emacs style)
30
+ "\x1b[1;3C", // CSI 1;3 C (xterm with modifiers)
31
+ ]);
32
+ /** Classify a character as word, punctuation, or space. */
33
+ function charClass(ch) {
34
+ if (/\s/.test(ch))
35
+ return "space";
36
+ if (/\w/.test(ch))
37
+ return "word";
38
+ return "punct";
39
+ }
40
+ /** Find the start of the previous word from `pos` in `text`. */
41
+ function prevWordBoundary(text, pos) {
42
+ if (pos <= 0)
43
+ return 0;
44
+ let i = pos - 1;
45
+ // Skip whitespace
46
+ while (i > 0 && charClass(text[i]) === "space")
47
+ i--;
48
+ if (i <= 0)
49
+ return 0;
50
+ // Skip through same character class (word or punct)
51
+ const cls = charClass(text[i]);
52
+ while (i > 0 && charClass(text[i - 1]) === cls)
53
+ i--;
54
+ return i;
55
+ }
56
+ /** Find the end of the next word from `pos` in `text`. */
57
+ function nextWordBoundary(text, pos) {
58
+ const len = text.length;
59
+ if (pos >= len)
60
+ return len;
61
+ let i = pos;
62
+ // Skip through current character class (word or punct)
63
+ const cls = charClass(text[i]);
64
+ while (i < len && charClass(text[i]) === cls)
65
+ i++;
66
+ // Skip whitespace
67
+ while (i < len && charClass(text[i]) === "space")
68
+ i++;
69
+ return i;
70
+ }
71
+ /** Get the normalized selection range [start, end] from anchor and cursor, or null. */
72
+ function getSelectionRange(anchor, cur) {
73
+ if (anchor === null || anchor === cur)
74
+ return null;
75
+ return [Math.min(anchor, cur), Math.max(anchor, cur)];
76
+ }
11
77
  // Border (1 each side) + padding (1 each side) = 4 characters of overhead
12
78
  const BOX_OVERHEAD = 4;
13
79
  // Minimum content width to prevent zero/negative values that cause infinite
@@ -56,6 +122,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
56
122
  const theme = useTheme();
57
123
  const [value, setValue] = useState("");
58
124
  const [cursor, setCursor] = useState(0);
125
+ const [selectionAnchor, setSelectionAnchor] = useState(null);
59
126
  const [images, setImages] = useState([]);
60
127
  const historyRef = useRef([]);
61
128
  const historyIndexRef = useRef(-1);
@@ -142,6 +209,152 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
142
209
  internal_eventEmitter.removeListener("input", onInput);
143
210
  };
144
211
  }, [isActive, internal_eventEmitter]);
212
+ // --- Mouse click-to-position-cursor ---
213
+ // Store layout info in a ref so the mouse handler can map terminal
214
+ // coordinates to character offsets without re-subscribing on every change.
215
+ const layoutRef = useRef({
216
+ value: "",
217
+ displayLines: [""],
218
+ startLine: 0,
219
+ contentWidth: 10,
220
+ columns: 80,
221
+ hasImages: false,
222
+ });
223
+ // Self-calibrating anchor: the terminal row (1-based) of the first
224
+ // display line. Set from the first single-line click (unambiguous).
225
+ // Ink rewrites from the same starting row on each render, so this
226
+ // value stays correct as text wraps to additional lines below.
227
+ const firstLineRowRef = useRef(-1);
228
+ // Enable SGR mouse tracking and intercept mouse sequences before Ink's
229
+ // useInput sees them (which would insert the raw escape text). We wrap
230
+ // the internal event emitter's `emit` so mouse data is consumed here and
231
+ // never forwarded to Ink's input handler.
232
+ const mouseEmitRef = useRef({ original: null });
233
+ useEffect(() => {
234
+ if (!isActive || !internal_eventEmitter)
235
+ return;
236
+ process.stdout.write(ENABLE_MOUSE);
237
+ // Safety: ensure mouse tracking is disabled even on crash/SIGINT/unexpected exit
238
+ // so the terminal isn't left in a broken state sending escape sequences on every click.
239
+ const onProcessExit = () => process.stdout.write(DISABLE_MOUSE);
240
+ process.on("exit", onProcessExit);
241
+ const originalEmit = internal_eventEmitter.emit.bind(internal_eventEmitter);
242
+ mouseEmitRef.current.original = originalEmit;
243
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
244
+ internal_eventEmitter.emit = (event, ...args) => {
245
+ if (event === "input" && typeof args[0] === "string") {
246
+ const data = args[0];
247
+ // Strip all SGR mouse sequences from the data
248
+ const stripped = data.replace(SGR_MOUSE_RE_G, "");
249
+ // Process each mouse sequence for click handling
250
+ let match;
251
+ SGR_MOUSE_RE_G.lastIndex = 0;
252
+ while ((match = SGR_MOUSE_RE_G.exec(data)) !== null) {
253
+ const btnCode = parseInt(match[1], 10);
254
+ const termCol = parseInt(match[2], 10);
255
+ const termRow = parseInt(match[3], 10);
256
+ const isPress = match[4] === "M";
257
+ // Decode SGR button code with bitmask:
258
+ // bits 0-1: button (0=left, 1=middle, 2=right, 3=release)
259
+ // bit 5 (32): motion event
260
+ // bit 6 (64): scroll wheel
261
+ const button = btnCode & 3;
262
+ const isMotion = (btnCode & 32) !== 0;
263
+ // Only handle left-click press (button 0), not motion or scroll
264
+ if (button !== 0 || isMotion || !isPress)
265
+ continue;
266
+ const layout = layoutRef.current;
267
+ if (!layout.value && layout.displayLines.length <= 1 && !layout.displayLines[0])
268
+ continue;
269
+ const numDisplayLines = layout.displayLines.length;
270
+ // Calibrate on the first single-line click: the clicked row
271
+ // IS the first (and only) display line's terminal row.
272
+ if (firstLineRowRef.current < 0 && numDisplayLines === 1) {
273
+ firstLineRowRef.current = termRow;
274
+ }
275
+ // Determine which display line was clicked
276
+ let clickedDisplayLine;
277
+ if (firstLineRowRef.current > 0) {
278
+ clickedDisplayLine = termRow - firstLineRowRef.current;
279
+ }
280
+ else {
281
+ // Not calibrated yet (multi-line before first click) — default to line 0
282
+ clickedDisplayLine = 0;
283
+ }
284
+ log("INFO", "mouse", "click", {
285
+ termRow,
286
+ termCol,
287
+ firstLineRow: firstLineRowRef.current,
288
+ clickedDisplayLine,
289
+ numDisplayLines,
290
+ });
291
+ // Clamp to valid range
292
+ if (clickedDisplayLine < 0)
293
+ clickedDisplayLine = 0;
294
+ if (clickedDisplayLine >= numDisplayLines)
295
+ clickedDisplayLine = numDisplayLines - 1;
296
+ // Column within the text: subtract border(1) + padding(1) + prompt(2) = 4
297
+ const textCol = termCol - 1 - 4;
298
+ const line = layout.displayLines[clickedDisplayLine];
299
+ const col = Math.max(0, Math.min(textCol, line.length));
300
+ // Convert display line + col to absolute character offset
301
+ const { value: val, startLine: sl, contentWidth: cw } = layout;
302
+ const hardLines = val.split("\n");
303
+ let charOffset = 0;
304
+ let vlIndex = 0;
305
+ let found = false;
306
+ for (let h = 0; h < hardLines.length; h++) {
307
+ const wrapped = wrapLine(hardLines[h], cw > 0 ? cw : val.length + 1);
308
+ for (let w = 0; w < wrapped.length; w++) {
309
+ if (vlIndex === sl + clickedDisplayLine) {
310
+ setCursor(Math.min(charOffset + col, val.length));
311
+ setSelectionAnchor(null);
312
+ found = true;
313
+ break;
314
+ }
315
+ charOffset += wrapped[w].length;
316
+ vlIndex++;
317
+ }
318
+ if (found)
319
+ break;
320
+ charOffset++; // newline
321
+ }
322
+ }
323
+ // Forward non-mouse data (if any remains) to Ink
324
+ if (stripped) {
325
+ return originalEmit("input", stripped);
326
+ }
327
+ return true; // swallowed entirely
328
+ }
329
+ return originalEmit(event, ...args);
330
+ };
331
+ return () => {
332
+ process.stdout.write(DISABLE_MOUSE);
333
+ process.removeListener("exit", onProcessExit);
334
+ // Restore original emit
335
+ if (mouseEmitRef.current.original) {
336
+ internal_eventEmitter.emit = mouseEmitRef.current.original;
337
+ mouseEmitRef.current.original = null;
338
+ }
339
+ };
340
+ }, [isActive, internal_eventEmitter]);
341
+ // Helper: delete selected text and return new value + cursor position.
342
+ // Returns null if no selection is active.
343
+ const deleteSelection = () => {
344
+ const sel = getSelectionRange(selectionAnchor, cursor);
345
+ if (!sel)
346
+ return null;
347
+ const [start, end] = sel;
348
+ return { newValue: value.slice(0, start) + value.slice(end), newCursor: start };
349
+ };
350
+ // Helper: clear all input state (used on submit / Ctrl+C / Escape)
351
+ const clearInput = () => {
352
+ setValue("");
353
+ setCursor(0);
354
+ setSelectionAnchor(null);
355
+ setImages([]);
356
+ setPasteText("");
357
+ };
145
358
  useInput((input, key) => {
146
359
  // Ctrl+T toggles task overlay — works even while agent is running
147
360
  if (key.ctrl && input === "t") {
@@ -167,8 +380,17 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
167
380
  // Submitted messages will be queued by the parent component.
168
381
  }
169
382
  if (key.return && (key.shift || key.meta)) {
170
- setValue((v) => v.slice(0, cursor) + "\n" + v.slice(cursor));
171
- setCursor((c) => c + 1);
383
+ // If there's a selection, replace it with the newline
384
+ const sel = deleteSelection();
385
+ if (sel) {
386
+ setValue(sel.newValue.slice(0, sel.newCursor) + "\n" + sel.newValue.slice(sel.newCursor));
387
+ setCursor(sel.newCursor + 1);
388
+ }
389
+ else {
390
+ setValue((v) => v.slice(0, cursor) + "\n" + v.slice(cursor));
391
+ setCursor((c) => c + 1);
392
+ }
393
+ setSelectionAnchor(null);
172
394
  return;
173
395
  }
174
396
  if (key.return) {
@@ -180,10 +402,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
180
402
  historyRef.current.push(cmd);
181
403
  historyIndexRef.current = -1;
182
404
  onSubmit(cmd, []);
183
- setValue("");
184
- setCursor(0);
185
- setImages([]);
186
- setPasteText("");
405
+ clearInput();
187
406
  return;
188
407
  }
189
408
  const trimmed = value.trim();
@@ -201,10 +420,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
201
420
  }
202
421
  : undefined;
203
422
  onSubmit(trimmed, [...images], paste);
204
- setValue("");
205
- setCursor(0);
206
- setImages([]);
207
- setPasteText("");
423
+ clearInput();
208
424
  }
209
425
  return;
210
426
  }
@@ -218,10 +434,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
218
434
  }
219
435
  if (key.ctrl && input === "c") {
220
436
  if (value || images.length > 0) {
221
- setValue("");
222
- setCursor(0);
223
- setImages([]);
224
- setPasteText("");
437
+ clearInput();
225
438
  }
226
439
  else {
227
440
  onAbort();
@@ -231,16 +444,53 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
231
444
  if (key.ctrl && input === "d") {
232
445
  process.exit(0);
233
446
  }
234
- // Home / End
447
+ // Ctrl+W delete previous word (or selection)
448
+ if (key.ctrl && input === "w") {
449
+ const sel = deleteSelection();
450
+ if (sel) {
451
+ setValue(sel.newValue);
452
+ setCursor(sel.newCursor);
453
+ }
454
+ else if (cursor > 0) {
455
+ const boundary = prevWordBoundary(value, cursor);
456
+ setValue((v) => v.slice(0, boundary) + v.slice(cursor));
457
+ setCursor(boundary);
458
+ }
459
+ setSelectionAnchor(null);
460
+ return;
461
+ }
462
+ // Home / End — Shift extends selection
235
463
  if (key.ctrl && input === "a") {
464
+ if (key.shift) {
465
+ if (selectionAnchor === null)
466
+ setSelectionAnchor(cursor);
467
+ }
468
+ else {
469
+ setSelectionAnchor(null);
470
+ }
236
471
  setCursor(0);
237
472
  return;
238
473
  }
239
474
  if (key.ctrl && input === "e") {
475
+ if (key.shift) {
476
+ if (selectionAnchor === null)
477
+ setSelectionAnchor(cursor);
478
+ }
479
+ else {
480
+ setSelectionAnchor(null);
481
+ }
240
482
  setCursor(value.length);
241
483
  return;
242
484
  }
243
485
  if (key.backspace || key.delete) {
486
+ // If selection active, delete the selection
487
+ const sel = deleteSelection();
488
+ if (sel) {
489
+ setValue(sel.newValue);
490
+ setCursor(sel.newCursor);
491
+ setSelectionAnchor(null);
492
+ return;
493
+ }
244
494
  if (cursor > 0) {
245
495
  setValue((v) => v.slice(0, cursor - 1) + v.slice(cursor));
246
496
  setCursor((c) => c - 1);
@@ -248,6 +498,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
248
498
  else if (!value && images.length > 0) {
249
499
  setImages((prev) => prev.slice(0, -1));
250
500
  }
501
+ setSelectionAnchor(null);
251
502
  return;
252
503
  }
253
504
  if (key.upArrow) {
@@ -256,6 +507,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
256
507
  setMenuIndex((i) => Math.max(0, i - 1));
257
508
  return;
258
509
  }
510
+ setSelectionAnchor(null);
259
511
  const history = historyRef.current;
260
512
  if (history.length === 0)
261
513
  return;
@@ -273,6 +525,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
273
525
  setMenuIndex((i) => Math.min(filteredCommands.length - 1, i + 1));
274
526
  return;
275
527
  }
528
+ setSelectionAnchor(null);
276
529
  const history = historyRef.current;
277
530
  if (historyIndexRef.current === -1) {
278
531
  if (onDownAtEnd)
@@ -293,12 +546,15 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
293
546
  return;
294
547
  }
295
548
  if (key.escape) {
549
+ // First escape clears selection, second clears input (double-tap)
550
+ if (selectionAnchor !== null) {
551
+ setSelectionAnchor(null);
552
+ lastEscRef.current = Date.now();
553
+ return;
554
+ }
296
555
  const now = Date.now();
297
556
  if ((value || images.length > 0) && now - lastEscRef.current < 400) {
298
- setValue("");
299
- setCursor(0);
300
- setImages([]);
301
- setPasteText("");
557
+ clearInput();
302
558
  }
303
559
  lastEscRef.current = now;
304
560
  return;
@@ -314,30 +570,104 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
314
570
  const cmd = "/" + selected.name;
315
571
  setValue(cmd);
316
572
  setCursor(cmd.length);
573
+ setSelectionAnchor(null);
317
574
  }
318
575
  return;
319
576
  }
577
+ // Option+Arrow word jump via raw escape sequences — many terminals send
578
+ // these as input strings rather than setting key.meta + arrow reliably.
579
+ if (OPTION_LEFT_SEQUENCES.has(input)) {
580
+ if (selectionAnchor !== null) {
581
+ const sel = getSelectionRange(selectionAnchor, cursor);
582
+ if (sel)
583
+ setCursor(sel[0]);
584
+ setSelectionAnchor(null);
585
+ }
586
+ else {
587
+ setCursor(prevWordBoundary(value, cursor));
588
+ }
589
+ return;
590
+ }
591
+ if (OPTION_RIGHT_SEQUENCES.has(input)) {
592
+ if (selectionAnchor !== null) {
593
+ const sel = getSelectionRange(selectionAnchor, cursor);
594
+ if (sel)
595
+ setCursor(sel[1]);
596
+ setSelectionAnchor(null);
597
+ }
598
+ else {
599
+ setCursor(nextWordBoundary(value, cursor));
600
+ }
601
+ return;
602
+ }
603
+ // Arrow keys — Shift extends selection, Meta/Option jumps words
320
604
  if (key.leftArrow) {
321
- if (cursor > 0)
605
+ if (key.shift) {
606
+ if (selectionAnchor === null)
607
+ setSelectionAnchor(cursor);
608
+ }
609
+ else if (selectionAnchor !== null) {
610
+ // Collapse selection to the left edge
611
+ const sel = getSelectionRange(selectionAnchor, cursor);
612
+ if (sel)
613
+ setCursor(sel[0]);
614
+ setSelectionAnchor(null);
615
+ return;
616
+ }
617
+ if (key.meta) {
618
+ setCursor(prevWordBoundary(value, cursor));
619
+ }
620
+ else if (cursor > 0) {
322
621
  setCursor((c) => c - 1);
622
+ }
623
+ if (!key.shift)
624
+ setSelectionAnchor(null);
323
625
  return;
324
626
  }
325
627
  if (key.rightArrow) {
326
- if (cursor < value.length)
628
+ if (key.shift) {
629
+ if (selectionAnchor === null)
630
+ setSelectionAnchor(cursor);
631
+ }
632
+ else if (selectionAnchor !== null) {
633
+ // Collapse selection to the right edge
634
+ const sel = getSelectionRange(selectionAnchor, cursor);
635
+ if (sel)
636
+ setCursor(sel[1]);
637
+ setSelectionAnchor(null);
638
+ return;
639
+ }
640
+ if (key.meta) {
641
+ setCursor(nextWordBoundary(value, cursor));
642
+ }
643
+ else if (cursor < value.length) {
327
644
  setCursor((c) => c + 1);
645
+ }
646
+ if (!key.shift)
647
+ setSelectionAnchor(null);
328
648
  return;
329
649
  }
330
650
  if (input) {
331
651
  const normalized = input.replace(/\r\n?/g, "\n");
332
- setValue((v) => v.slice(0, cursor) + normalized + v.slice(cursor));
333
- setCursor((c) => c + normalized.length);
652
+ // If there's a selection, replace it with the typed input
653
+ const sel = deleteSelection();
654
+ if (sel) {
655
+ setValue(sel.newValue.slice(0, sel.newCursor) + normalized + sel.newValue.slice(sel.newCursor));
656
+ setCursor(sel.newCursor + normalized.length);
657
+ setSelectionAnchor(null);
658
+ }
659
+ else {
660
+ setValue((v) => v.slice(0, cursor) + normalized + v.slice(cursor));
661
+ setCursor((c) => c + normalized.length);
662
+ }
334
663
  // Detect paste: Ink delivers pasted text as input.length > 1
335
664
  // For large pastes, Ink may split into multiple chunks, so we
336
665
  // accumulate and debounce to capture the full paste.
337
666
  if (input.length > 1) {
667
+ const pasteStart = sel ? sel.newCursor : cursor;
338
668
  setPasteText((prev) => {
339
669
  if (!prev)
340
- setPasteOffset(cursor); // record where paste starts on first chunk
670
+ setPasteOffset(pasteStart);
341
671
  return prev + normalized;
342
672
  });
343
673
  if (pasteTimerRef.current)
@@ -398,6 +728,13 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
398
728
  }
399
729
  const displayLines = visualLines.slice(startLine, startLine + MAX_VISIBLE_LINES);
400
730
  const cursorDisplayLine = cursorLineInfo.line - startLine;
731
+ // Keep layout ref in sync for mouse click handler
732
+ layoutRef.current.value = value;
733
+ layoutRef.current.displayLines = displayLines;
734
+ layoutRef.current.startLine = startLine;
735
+ layoutRef.current.contentWidth = contentWidth;
736
+ layoutRef.current.columns = columns;
737
+ layoutRef.current.hasImages = images.length > 0;
401
738
  // Determine if the input starts with a slash command and find command boundary
402
739
  const isCommand = value.startsWith("/");
403
740
  // Command portion ends at first space (e.g., "/research" in "/research some args")
@@ -406,6 +743,8 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
406
743
  ? value.length
407
744
  : value.indexOf(" ")
408
745
  : 0;
746
+ // Active selection range (absolute character offsets)
747
+ const selection = getSelectionRange(selectionAnchor, cursor);
409
748
  return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: disabled ? theme.textDim : borderPulseColors[borderFrame], paddingLeft: 1, paddingRight: 1, children: [images.length > 0 && (_jsx(Box, { children: _jsx(Text, { color: theme.accent, children: images
410
749
  .map((img, i) => img.kind === "text" ? `[File: ${img.fileName}]` : `[Image #${i + 1}]`)
411
750
  .join(" ") }) })), (() => {
@@ -451,24 +790,99 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
451
790
  }
452
791
  offset++; // newline
453
792
  }
454
- // Determine color for each character based on whether it's in the command portion
455
- const renderSegments = (text, textStartOffset) => {
456
- if (!isCommand || textStartOffset >= commandEndIndex) {
457
- return _jsx(Text, { color: theme.text, children: text });
458
- }
459
- const cmdChars = Math.min(text.length, commandEndIndex - textStartOffset);
793
+ const lineEndOffset = lineStartOffset + line.length;
794
+ // Render a text segment with command coloring and optional selection highlight
795
+ const renderSegment = (text, absOffset, opts) => {
796
+ if (!text)
797
+ return null;
798
+ const inCmd = isCommand && absOffset < commandEndIndex;
799
+ const cmdChars = inCmd ? Math.min(text.length, commandEndIndex - absOffset) : 0;
800
+ const inv = opts?.inverse ?? false;
460
801
  if (cmdChars >= text.length) {
461
- return (_jsx(Text, { color: theme.commandColor, bold: true, children: text }));
802
+ return (_jsx(Text, { color: theme.commandColor, bold: true, inverse: inv, children: text }));
803
+ }
804
+ if (cmdChars > 0) {
805
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.commandColor, bold: true, inverse: inv, children: text.slice(0, cmdChars) }), _jsx(Text, { color: theme.text, inverse: inv, children: text.slice(cmdChars) })] }));
462
806
  }
463
- return (_jsxs(_Fragment, { children: [_jsx(Text, { color: theme.commandColor, bold: true, children: text.slice(0, cmdChars) }), _jsx(Text, { color: theme.text, children: text.slice(cmdChars) })] }));
807
+ return (_jsx(Text, { color: theme.text, inverse: inv, children: text }));
464
808
  };
465
- const before = showCursor ? line.slice(0, col) : line;
466
- const charUnderCursor = showCursor ? (col < line.length ? line[col] : " ") : "";
467
- const after = showCursor ? line.slice(col + (col < line.length ? 1 : 0)) : "";
468
- const cursorCharOffset = lineStartOffset + col;
469
- const cursorInCommand = isCommand && cursorCharOffset < commandEndIndex;
470
- return (_jsxs(Box, { children: [_jsx(Text, { color: disabled ? theme.textDim : theme.inputPrompt, bold: true, children: i === 0 ? PROMPT : " " }), renderSegments(before, lineStartOffset), showCursor && (_jsx(Text, { color: cursorInCommand ? theme.commandColor : theme.text, bold: cursorInCommand, inverse: cursorVisible, children: charUnderCursor })), after &&
471
- renderSegments(after, lineStartOffset + col + (col < line.length ? 1 : 0))] }, i));
809
+ // Build segments for: [before-sel] [selected] [cursor] [after-sel]
810
+ // considering that cursor and selection can overlap on this line
811
+ const segments = [];
812
+ let pos = 0; // position within `line`
813
+ // Determine selection overlap with this line (in line-local coords)
814
+ const selLocalStart = selection
815
+ ? Math.max(0, selection[0] - lineStartOffset)
816
+ : line.length;
817
+ const selLocalEnd = selection
818
+ ? Math.min(line.length, selection[1] - lineStartOffset)
819
+ : line.length;
820
+ const hasSelOnLine = selection !== null && selection[0] < lineEndOffset && selection[1] > lineStartOffset;
821
+ if (hasSelOnLine) {
822
+ // Text before selection
823
+ if (selLocalStart > 0) {
824
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(0, selLocalStart), lineStartOffset) }, "pre"));
825
+ pos = selLocalStart;
826
+ }
827
+ // Selected text — render with inverse, but split around cursor if needed
828
+ if (showCursor && col >= selLocalStart && col < selLocalEnd) {
829
+ // Cursor is inside the selection
830
+ if (col > pos) {
831
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos, col), lineStartOffset + pos, {
832
+ inverse: true,
833
+ }) }, "sel-before"));
834
+ }
835
+ // Cursor character (blinks within selection)
836
+ const cursorChar = col < line.length ? line[col] : " ";
837
+ const cursorAbs = lineStartOffset + col;
838
+ const curInCmd = isCommand && cursorAbs < commandEndIndex;
839
+ segments.push(_jsx(Text, { color: curInCmd ? theme.commandColor : theme.text, bold: curInCmd, inverse: cursorVisible, children: cursorChar }, "cursor"));
840
+ const afterCursorPos = col + (col < line.length ? 1 : 0);
841
+ if (afterCursorPos < selLocalEnd) {
842
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(afterCursorPos, selLocalEnd), lineStartOffset + afterCursorPos, { inverse: true }) }, "sel-after"));
843
+ }
844
+ pos = selLocalEnd;
845
+ }
846
+ else {
847
+ // Cursor not on this selection portion — render entire selection inverse
848
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos, selLocalEnd), lineStartOffset + pos, {
849
+ inverse: true,
850
+ }) }, "sel"));
851
+ pos = selLocalEnd;
852
+ }
853
+ // Cursor after selection on this line
854
+ if (showCursor && col >= selLocalEnd) {
855
+ // Text between selection end and cursor
856
+ if (col > pos) {
857
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos, col), lineStartOffset + pos) }, "mid"));
858
+ }
859
+ const cursorChar = col < line.length ? line[col] : " ";
860
+ const cursorAbs = lineStartOffset + col;
861
+ const curInCmd = isCommand && cursorAbs < commandEndIndex;
862
+ segments.push(_jsx(Text, { color: curInCmd ? theme.commandColor : theme.text, bold: curInCmd, inverse: cursorVisible, children: cursorChar }, "cursor"));
863
+ pos = col + (col < line.length ? 1 : 0);
864
+ }
865
+ // Text after selection (and cursor)
866
+ if (pos < line.length) {
867
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos), lineStartOffset + pos) }, "post"));
868
+ }
869
+ }
870
+ else {
871
+ // No selection on this line — original cursor-only rendering
872
+ const before = showCursor ? line.slice(0, col) : line;
873
+ const charUnderCursor = showCursor ? (col < line.length ? line[col] : " ") : "";
874
+ const after = showCursor ? line.slice(col + (col < line.length ? 1 : 0)) : "";
875
+ const cursorCharOffset = lineStartOffset + col;
876
+ const cursorInCommand = isCommand && cursorCharOffset < commandEndIndex;
877
+ segments.push(_jsx(React.Fragment, { children: renderSegment(before, lineStartOffset) }, "before"));
878
+ if (showCursor) {
879
+ segments.push(_jsx(Text, { color: cursorInCommand ? theme.commandColor : theme.text, bold: cursorInCommand, inverse: cursorVisible, children: charUnderCursor }, "cursor"));
880
+ }
881
+ if (after) {
882
+ segments.push(_jsx(React.Fragment, { children: renderSegment(after, lineStartOffset + col + (col < line.length ? 1 : 0)) }, "after"));
883
+ }
884
+ }
885
+ return (_jsxs(Box, { children: [_jsx(Text, { color: disabled ? theme.textDim : theme.inputPrompt, bold: true, children: i === 0 ? PROMPT : " " }), segments] }, i));
472
886
  });
473
887
  })()] }), isSlashMode && filteredCommands.length > 0 && (_jsx(SlashCommandMenu, { commands: commands, filter: slashFilter, selectedIndex: menuIndex }))] }));
474
888
  }