@prestyj/cli 4.2.46 → 4.2.62

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 (172) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +129 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.js +5 -5
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +11 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/compaction/compactor.d.ts +8 -4
  10. package/dist/core/compaction/compactor.d.ts.map +1 -1
  11. package/dist/core/compaction/compactor.js +203 -27
  12. package/dist/core/compaction/compactor.js.map +1 -1
  13. package/dist/core/compaction/compactor.test.js +6 -4
  14. package/dist/core/compaction/compactor.test.js.map +1 -1
  15. package/dist/core/compaction/token-estimator.test.js +1 -1
  16. package/dist/core/compaction/token-estimator.test.js.map +1 -1
  17. package/dist/core/model-registry.js +5 -5
  18. package/dist/core/model-registry.js.map +1 -1
  19. package/dist/core/slash-commands.d.ts.map +1 -1
  20. package/dist/core/slash-commands.js +54 -0
  21. package/dist/core/slash-commands.js.map +1 -1
  22. package/dist/core/telegram.d.ts +19 -0
  23. package/dist/core/telegram.d.ts.map +1 -1
  24. package/dist/core/telegram.js +25 -0
  25. package/dist/core/telegram.js.map +1 -1
  26. package/dist/core/voice-transcriber.d.ts +33 -0
  27. package/dist/core/voice-transcriber.d.ts.map +1 -0
  28. package/dist/core/voice-transcriber.js +113 -0
  29. package/dist/core/voice-transcriber.js.map +1 -0
  30. package/dist/core/voice-transcriber.test.d.ts +2 -0
  31. package/dist/core/voice-transcriber.test.d.ts.map +1 -0
  32. package/dist/core/voice-transcriber.test.js +88 -0
  33. package/dist/core/voice-transcriber.test.js.map +1 -0
  34. package/dist/interactive.d.ts.map +1 -1
  35. package/dist/interactive.js +1 -0
  36. package/dist/interactive.js.map +1 -1
  37. package/dist/modes/serve-mode.d.ts.map +1 -1
  38. package/dist/modes/serve-mode.js +37 -0
  39. package/dist/modes/serve-mode.js.map +1 -1
  40. package/dist/tools/bash.d.ts.map +1 -1
  41. package/dist/tools/bash.js +4 -7
  42. package/dist/tools/bash.js.map +1 -1
  43. package/dist/tools/edit-diff.test.d.ts +2 -0
  44. package/dist/tools/edit-diff.test.d.ts.map +1 -0
  45. package/dist/tools/edit-diff.test.js +67 -0
  46. package/dist/tools/edit-diff.test.js.map +1 -0
  47. package/dist/tools/edit.d.ts.map +1 -1
  48. package/dist/tools/edit.js +4 -1
  49. package/dist/tools/edit.js.map +1 -1
  50. package/dist/tools/edit.test.d.ts +2 -0
  51. package/dist/tools/edit.test.d.ts.map +1 -0
  52. package/dist/tools/edit.test.js +93 -0
  53. package/dist/tools/edit.test.js.map +1 -0
  54. package/dist/tools/overflow.d.ts +6 -0
  55. package/dist/tools/overflow.d.ts.map +1 -0
  56. package/dist/tools/overflow.js +13 -0
  57. package/dist/tools/overflow.js.map +1 -0
  58. package/dist/tools/path-utils.test.d.ts +2 -0
  59. package/dist/tools/path-utils.test.d.ts.map +1 -0
  60. package/dist/tools/path-utils.test.js +49 -0
  61. package/dist/tools/path-utils.test.js.map +1 -0
  62. package/dist/tools/plan-mode.test.js.map +1 -1
  63. package/dist/tools/read.d.ts.map +1 -1
  64. package/dist/tools/read.js +6 -2
  65. package/dist/tools/read.js.map +1 -1
  66. package/dist/tools/read.test.d.ts +2 -0
  67. package/dist/tools/read.test.d.ts.map +1 -0
  68. package/dist/tools/read.test.js +81 -0
  69. package/dist/tools/read.test.js.map +1 -0
  70. package/dist/tools/truncate-utils.test.d.ts +2 -0
  71. package/dist/tools/truncate-utils.test.d.ts.map +1 -0
  72. package/dist/tools/truncate-utils.test.js +93 -0
  73. package/dist/tools/truncate-utils.test.js.map +1 -0
  74. package/dist/tools/truncate.d.ts +6 -4
  75. package/dist/tools/truncate.d.ts.map +1 -1
  76. package/dist/tools/truncate.js +12 -10
  77. package/dist/tools/truncate.js.map +1 -1
  78. package/dist/tools/write.test.js +65 -52
  79. package/dist/tools/write.test.js.map +1 -1
  80. package/dist/ui/App.d.ts +8 -1
  81. package/dist/ui/App.d.ts.map +1 -1
  82. package/dist/ui/App.js +62 -15
  83. package/dist/ui/App.js.map +1 -1
  84. package/dist/ui/activity-phrases.d.ts +10 -0
  85. package/dist/ui/activity-phrases.d.ts.map +1 -0
  86. package/dist/ui/activity-phrases.js +227 -0
  87. package/dist/ui/activity-phrases.js.map +1 -0
  88. package/dist/ui/components/ActivityIndicator.d.ts +3 -2
  89. package/dist/ui/components/ActivityIndicator.d.ts.map +1 -1
  90. package/dist/ui/components/ActivityIndicator.js +25 -244
  91. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  92. package/dist/ui/components/AnimationContext.d.ts +9 -2
  93. package/dist/ui/components/AnimationContext.d.ts.map +1 -1
  94. package/dist/ui/components/AnimationContext.js +46 -7
  95. package/dist/ui/components/AnimationContext.js.map +1 -1
  96. package/dist/ui/components/AssistantMessage.d.ts +2 -1
  97. package/dist/ui/components/AssistantMessage.d.ts.map +1 -1
  98. package/dist/ui/components/AssistantMessage.js +2 -2
  99. package/dist/ui/components/AssistantMessage.js.map +1 -1
  100. package/dist/ui/components/Banner.d.ts.map +1 -1
  101. package/dist/ui/components/Banner.js +10 -1
  102. package/dist/ui/components/Banner.js.map +1 -1
  103. package/dist/ui/components/CompactionNotice.d.ts.map +1 -1
  104. package/dist/ui/components/CompactionNotice.js +2 -1
  105. package/dist/ui/components/CompactionNotice.js.map +1 -1
  106. package/dist/ui/components/Footer.d.ts.map +1 -1
  107. package/dist/ui/components/Footer.js +27 -17
  108. package/dist/ui/components/Footer.js.map +1 -1
  109. package/dist/ui/components/InputArea.d.ts.map +1 -1
  110. package/dist/ui/components/InputArea.js +585 -60
  111. package/dist/ui/components/InputArea.js.map +1 -1
  112. package/dist/ui/components/Markdown.d.ts +9 -4
  113. package/dist/ui/components/Markdown.d.ts.map +1 -1
  114. package/dist/ui/components/Markdown.js +74 -17
  115. package/dist/ui/components/Markdown.js.map +1 -1
  116. package/dist/ui/components/PlanApproval.d.ts.map +1 -1
  117. package/dist/ui/components/PlanApproval.js +17 -1
  118. package/dist/ui/components/PlanApproval.js.map +1 -1
  119. package/dist/ui/components/PlanBanner.d.ts.map +1 -1
  120. package/dist/ui/components/PlanBanner.js +18 -1
  121. package/dist/ui/components/PlanBanner.js.map +1 -1
  122. package/dist/ui/components/PlanOverlay.d.ts.map +1 -1
  123. package/dist/ui/components/PlanOverlay.js +43 -12
  124. package/dist/ui/components/PlanOverlay.js.map +1 -1
  125. package/dist/ui/components/SkillsOverlay.d.ts.map +1 -1
  126. package/dist/ui/components/SkillsOverlay.js +5 -1
  127. package/dist/ui/components/SkillsOverlay.js.map +1 -1
  128. package/dist/ui/components/Spinner.d.ts.map +1 -1
  129. package/dist/ui/components/Spinner.js +2 -1
  130. package/dist/ui/components/Spinner.js.map +1 -1
  131. package/dist/ui/components/StreamingArea.d.ts +2 -1
  132. package/dist/ui/components/StreamingArea.d.ts.map +1 -1
  133. package/dist/ui/components/StreamingArea.js +4 -4
  134. package/dist/ui/components/StreamingArea.js.map +1 -1
  135. package/dist/ui/components/SubAgentPanel.d.ts.map +1 -1
  136. package/dist/ui/components/SubAgentPanel.js +2 -1
  137. package/dist/ui/components/SubAgentPanel.js.map +1 -1
  138. package/dist/ui/components/TaskOverlay.d.ts.map +1 -1
  139. package/dist/ui/components/TaskOverlay.js +5 -1
  140. package/dist/ui/components/TaskOverlay.js.map +1 -1
  141. package/dist/ui/components/ThinkingBlock.d.ts +2 -1
  142. package/dist/ui/components/ThinkingBlock.d.ts.map +1 -1
  143. package/dist/ui/components/ThinkingBlock.js +2 -2
  144. package/dist/ui/components/ThinkingBlock.js.map +1 -1
  145. package/dist/ui/components/ThinkingIndicator.d.ts.map +1 -1
  146. package/dist/ui/components/ThinkingIndicator.js +2 -1
  147. package/dist/ui/components/ThinkingIndicator.js.map +1 -1
  148. package/dist/ui/components/ToolExecution.d.ts +1 -0
  149. package/dist/ui/components/ToolExecution.d.ts.map +1 -1
  150. package/dist/ui/components/ToolExecution.js +7 -4
  151. package/dist/ui/components/ToolExecution.js.map +1 -1
  152. package/dist/ui/components/index.d.ts +1 -1
  153. package/dist/ui/components/index.d.ts.map +1 -1
  154. package/dist/ui/components/index.js +1 -1
  155. package/dist/ui/components/index.js.map +1 -1
  156. package/dist/ui/hooks/useAgentLoop.d.ts +8 -1
  157. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  158. package/dist/ui/hooks/useAgentLoop.js +69 -80
  159. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  160. package/dist/ui/hooks/useTerminalSize.d.ts +7 -7
  161. package/dist/ui/hooks/useTerminalSize.d.ts.map +1 -1
  162. package/dist/ui/hooks/useTerminalSize.js +25 -12
  163. package/dist/ui/hooks/useTerminalSize.js.map +1 -1
  164. package/dist/ui/hooks/useTerminalTitle.d.ts +8 -1
  165. package/dist/ui/hooks/useTerminalTitle.d.ts.map +1 -1
  166. package/dist/ui/hooks/useTerminalTitle.js +21 -23
  167. package/dist/ui/hooks/useTerminalTitle.js.map +1 -1
  168. package/dist/ui/plan-overlay.test.d.ts +2 -0
  169. package/dist/ui/plan-overlay.test.d.ts.map +1 -0
  170. package/dist/ui/plan-overlay.test.js +327 -0
  171. package/dist/ui/plan-overlay.test.js.map +1 -0
  172. package/package.json +6 -4
@@ -1,14 +1,89 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useState, useRef, useEffect, useMemo } from "react";
3
- import { Text, Box, useInput, useStdout, useStdin } from "ink";
3
+ import { Text, Box, useInput, useStdin } from "ink";
4
4
  import { useTheme } from "../theme/theme.js";
5
- import { useAnimationTick, deriveFrame } from "./AnimationContext.js";
5
+ import { useAnimationTick, useAnimationActive, deriveFrame } from "./AnimationContext.js";
6
+ import { useTerminalSize } from "../hooks/useTerminalSize.js";
6
7
  import { extractImagePaths, readImageFile, getClipboardImage } from "../../utils/image.js";
7
8
  import { SlashCommandMenu, filterCommands } from "./SlashCommandMenu.js";
9
+ import { log } from "../../core/logger.js";
8
10
  const MAX_VISIBLE_LINES = 5;
9
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
+ // Guard against stray SGR mouse sequences leaking into text input.
23
+ // Some terminals or multiplexers send these even without mouse tracking enabled.
24
+ function isMouseEscapeSequence(input) {
25
+ return input.includes("[<") && /\[<\d+;\d+;\d+[Mm]/.test(input);
26
+ }
27
+ // Option+Arrow escape sequences — terminals send these as raw input strings
28
+ // rather than setting key.meta + key.leftArrow reliably.
29
+ const OPTION_LEFT_SEQUENCES = new Set([
30
+ "\x1bb", // Meta+b (emacs style)
31
+ "\x1b[1;3D", // CSI 1;3 D (xterm with modifiers)
32
+ ]);
33
+ const OPTION_RIGHT_SEQUENCES = new Set([
34
+ "\x1bf", // Meta+f (emacs style)
35
+ "\x1b[1;3C", // CSI 1;3 C (xterm with modifiers)
36
+ ]);
37
+ /** Classify a character as word, punctuation, or space. */
38
+ function charClass(ch) {
39
+ if (/\s/.test(ch))
40
+ return "space";
41
+ if (/\w/.test(ch))
42
+ return "word";
43
+ return "punct";
44
+ }
45
+ /** Find the start of the previous word from `pos` in `text`. */
46
+ function prevWordBoundary(text, pos) {
47
+ if (pos <= 0)
48
+ return 0;
49
+ let i = pos - 1;
50
+ // Skip whitespace
51
+ while (i > 0 && charClass(text[i]) === "space")
52
+ i--;
53
+ if (i <= 0)
54
+ return 0;
55
+ // Skip through same character class (word or punct)
56
+ const cls = charClass(text[i]);
57
+ while (i > 0 && charClass(text[i - 1]) === cls)
58
+ i--;
59
+ return i;
60
+ }
61
+ /** Find the end of the next word from `pos` in `text`. */
62
+ function nextWordBoundary(text, pos) {
63
+ const len = text.length;
64
+ if (pos >= len)
65
+ return len;
66
+ let i = pos;
67
+ // Skip through current character class (word or punct)
68
+ const cls = charClass(text[i]);
69
+ while (i < len && charClass(text[i]) === cls)
70
+ i++;
71
+ // Skip whitespace
72
+ while (i < len && charClass(text[i]) === "space")
73
+ i++;
74
+ return i;
75
+ }
76
+ /** Get the normalized selection range [start, end] from anchor and cursor, or null. */
77
+ function getSelectionRange(anchor, cur) {
78
+ if (anchor === null || anchor === cur)
79
+ return null;
80
+ return [Math.min(anchor, cur), Math.max(anchor, cur)];
81
+ }
10
82
  // Border (1 each side) + padding (1 each side) = 4 characters of overhead
11
83
  const BOX_OVERHEAD = 4;
84
+ // Minimum content width to prevent zero/negative values that cause infinite
85
+ // re-render loops when Ink tries to wrap text wider than available space.
86
+ const MIN_CONTENT_WIDTH = 10;
12
87
  /**
13
88
  * Split text into visual lines based on terminal width.
14
89
  * Accounts for the prompt prefix, border, and padding.
@@ -37,9 +112,7 @@ function wrapLine(text, contentWidth) {
37
112
  return lines;
38
113
  }
39
114
  function getVisualLines(text, columns) {
40
- const contentWidth = columns - PROMPT.length - BOX_OVERHEAD;
41
- if (contentWidth <= 0)
42
- return [text];
115
+ const contentWidth = Math.max(MIN_CONTENT_WIDTH, columns - PROMPT.length - BOX_OVERHEAD);
43
116
  if (text.length === 0)
44
117
  return [""];
45
118
  // Split on real newlines first, then wrap each
@@ -54,12 +127,14 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
54
127
  const theme = useTheme();
55
128
  const [value, setValue] = useState("");
56
129
  const [cursor, setCursor] = useState(0);
130
+ const cursorRef = useRef(cursor);
131
+ cursorRef.current = cursor;
132
+ const [selectionAnchor, setSelectionAnchor] = useState(null);
57
133
  const [images, setImages] = useState([]);
58
134
  const historyRef = useRef([]);
59
135
  const historyIndexRef = useRef(-1);
60
136
  const lastEscRef = useRef(0);
61
- const { stdout } = useStdout();
62
- const columns = stdout?.columns ?? 80;
137
+ const { columns } = useTerminalSize();
63
138
  const [menuIndex, setMenuIndex] = useState(0);
64
139
  const [pasteText, setPasteText] = useState(""); // accumulated pasted content
65
140
  const [pasteOffset, setPasteOffset] = useState(0); // where in value the paste starts
@@ -75,6 +150,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
75
150
  // Border color pulse (when idle/waiting for input)
76
151
  const borderPulseColors = useMemo(() => [theme.primary, theme.accent, theme.secondary, theme.accent], [theme.primary, theme.accent, theme.secondary]);
77
152
  // Derive border pulse and cursor blink from global animation tick
153
+ useAnimationActive();
78
154
  const tick = useAnimationTick();
79
155
  const borderFrame = disabled ? 0 : deriveFrame(tick, 800, borderPulseColors.length);
80
156
  // Cursor blink: ~530ms period → visible for ~500ms, hidden for ~500ms
@@ -141,7 +217,241 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
141
217
  internal_eventEmitter.removeListener("input", onInput);
142
218
  };
143
219
  }, [isActive, internal_eventEmitter]);
220
+ // --- Mouse click-to-position-cursor ---
221
+ // Store layout info in a ref so the mouse handler can map terminal
222
+ // coordinates to character offsets without re-subscribing on every change.
223
+ const layoutRef = useRef({
224
+ value: "",
225
+ displayLines: [""],
226
+ startLine: 0,
227
+ contentWidth: 10,
228
+ columns: 80,
229
+ hasImages: false,
230
+ });
231
+ // Self-calibrating anchor: the terminal row (1-based) of the LAST
232
+ // display line. We track the last line (not the first) because Ink
233
+ // grows the input box upward — the bottom stays at a stable position
234
+ // while the top moves up as lines are added. Calibrated on any
235
+ // single-line click (unambiguous), then remains valid as text wraps.
236
+ const lastLineRowRef = useRef(-1);
237
+ // Enable SGR mouse tracking and intercept mouse sequences before Ink's
238
+ // useInput sees them (which would insert the raw escape text). We wrap
239
+ // the internal event emitter's `emit` so mouse data is consumed here and
240
+ // never forwarded to Ink's input handler.
241
+ const mouseEmitRef = useRef({ original: null });
242
+ // Track whether input has text so we can toggle mouse tracking.
243
+ // Only enable mouse tracking when there's text to navigate — when the input
244
+ // is empty, click-to-cursor is useless and disabling tracking lets the
245
+ // terminal handle CMD+click for opening links natively.
246
+ const hasInputTextRef = useRef(value.length > 0);
247
+ useEffect(() => {
248
+ if (!isActive || !internal_eventEmitter)
249
+ return;
250
+ // Only enable mouse tracking if there's text — when empty, let the
251
+ // terminal handle clicks natively (e.g., CMD+click to open links).
252
+ if (hasInputTextRef.current) {
253
+ process.stdout.write(ENABLE_MOUSE);
254
+ }
255
+ // Safety: ensure mouse tracking is disabled even on crash/SIGINT/unexpected exit
256
+ // so the terminal isn't left in a broken state sending escape sequences on every click.
257
+ const onProcessExit = () => process.stdout.write(DISABLE_MOUSE);
258
+ process.on("exit", onProcessExit);
259
+ const originalEmit = internal_eventEmitter.emit.bind(internal_eventEmitter);
260
+ mouseEmitRef.current.original = originalEmit;
261
+ // Scroll passthrough: when a scroll event is detected, temporarily disable
262
+ // mouse tracking so the terminal handles scroll natively (scrollback buffer).
263
+ // Re-enable after a short idle period so click-to-cursor continues to work.
264
+ let scrollTimer = null;
265
+ let mouseDisabled = false;
266
+ const reenableMouse = () => {
267
+ if (mouseDisabled && hasInputTextRef.current) {
268
+ process.stdout.write(ENABLE_MOUSE);
269
+ mouseDisabled = false;
270
+ }
271
+ };
272
+ const pauseMouseForScroll = () => {
273
+ if (!mouseDisabled) {
274
+ process.stdout.write(DISABLE_MOUSE);
275
+ mouseDisabled = true;
276
+ }
277
+ if (scrollTimer)
278
+ clearTimeout(scrollTimer);
279
+ scrollTimer = setTimeout(reenableMouse, 300);
280
+ };
281
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
282
+ internal_eventEmitter.emit = (event, ...args) => {
283
+ if (event === "input" && typeof args[0] === "string") {
284
+ const data = args[0];
285
+ // Strip all SGR mouse sequences from the data
286
+ const stripped = data.replace(SGR_MOUSE_RE_G, "");
287
+ // Process each mouse sequence for click handling
288
+ let match;
289
+ SGR_MOUSE_RE_G.lastIndex = 0;
290
+ while ((match = SGR_MOUSE_RE_G.exec(data)) !== null) {
291
+ const btnCode = parseInt(match[1], 10);
292
+ const termCol = parseInt(match[2], 10);
293
+ const termRow = parseInt(match[3], 10);
294
+ const isPress = match[4] === "M";
295
+ // Decode SGR button code with bitmask:
296
+ // bits 0-1: button (0=left, 1=middle, 2=right, 3=release)
297
+ // bit 2 (4): shift held
298
+ // bit 3 (8): meta/alt held (CMD on macOS)
299
+ // bit 4 (16): control held
300
+ // bit 5 (32): motion event
301
+ // bit 6 (64): scroll wheel
302
+ const button = btnCode & 3;
303
+ const hasModifier = (btnCode & 0b11100) !== 0; // shift, meta, or ctrl
304
+ const isMotion = (btnCode & 32) !== 0;
305
+ const isScroll = (btnCode & 64) !== 0;
306
+ // On scroll: disable mouse tracking so the terminal handles it natively,
307
+ // then re-enable after idle so click-to-cursor keeps working.
308
+ if (isScroll) {
309
+ pauseMouseForScroll();
310
+ continue;
311
+ }
312
+ // When modifier keys are held (CMD+click, Ctrl+click, Shift+click),
313
+ // temporarily disable mouse tracking so the terminal can handle
314
+ // the click natively (e.g., opening links with CMD+click).
315
+ if (hasModifier) {
316
+ pauseMouseForScroll();
317
+ continue;
318
+ }
319
+ // Only handle left-click press (button 0), not motion or release
320
+ if (button !== 0 || isMotion || !isPress)
321
+ continue;
322
+ const layout = layoutRef.current;
323
+ if (!layout.value && layout.displayLines.length <= 1 && !layout.displayLines[0])
324
+ continue;
325
+ const numDisplayLines = layout.displayLines.length;
326
+ // Calibrate on single-line click (unambiguous — the clicked row
327
+ // IS the last (and only) display line's terminal row).
328
+ if (numDisplayLines === 1) {
329
+ lastLineRowRef.current = termRow;
330
+ }
331
+ // Determine which display line was clicked by computing from the
332
+ // last line's row (stable because Ink grows the box upward).
333
+ let clickedDisplayLine;
334
+ if (lastLineRowRef.current > 0) {
335
+ const firstLineRow = lastLineRowRef.current - numDisplayLines + 1;
336
+ clickedDisplayLine = termRow - firstLineRow;
337
+ // If calibration is stale (click outside valid range), recalibrate.
338
+ if (clickedDisplayLine < 0) {
339
+ lastLineRowRef.current = termRow + (numDisplayLines - 1);
340
+ clickedDisplayLine = 0;
341
+ }
342
+ else if (clickedDisplayLine >= numDisplayLines) {
343
+ lastLineRowRef.current = termRow;
344
+ clickedDisplayLine = numDisplayLines - 1;
345
+ }
346
+ }
347
+ else {
348
+ // Not calibrated yet — assume click is on the last display line.
349
+ // This calibrates correctly because the bottom is stable in Ink.
350
+ lastLineRowRef.current = termRow;
351
+ clickedDisplayLine = numDisplayLines - 1;
352
+ }
353
+ log("INFO", "mouse", "click", {
354
+ termRow,
355
+ termCol,
356
+ lastLineRow: lastLineRowRef.current,
357
+ clickedDisplayLine,
358
+ numDisplayLines,
359
+ });
360
+ // Clamp to valid range
361
+ if (clickedDisplayLine < 0)
362
+ clickedDisplayLine = 0;
363
+ if (clickedDisplayLine >= numDisplayLines)
364
+ clickedDisplayLine = numDisplayLines - 1;
365
+ // Column within the text: subtract border(1) + padding(1) + prompt(2) = 4
366
+ const textCol = termCol - 1 - 4;
367
+ const line = layout.displayLines[clickedDisplayLine];
368
+ const col = Math.max(0, Math.min(textCol, line.length));
369
+ // Convert display line + col to absolute character offset
370
+ const { value: val, startLine: sl, contentWidth: cw } = layout;
371
+ const hardLines = val.split("\n");
372
+ let charOffset = 0;
373
+ let vlIndex = 0;
374
+ let found = false;
375
+ for (let h = 0; h < hardLines.length; h++) {
376
+ const hardLine = hardLines[h];
377
+ const wrapped = wrapLine(hardLine, cw > 0 ? cw : val.length + 1);
378
+ let hardLinePos = 0;
379
+ for (let w = 0; w < wrapped.length; w++) {
380
+ if (vlIndex === sl + clickedDisplayLine) {
381
+ setCursor(Math.min(charOffset + col, val.length));
382
+ setSelectionAnchor(null);
383
+ found = true;
384
+ break;
385
+ }
386
+ charOffset += wrapped[w].length;
387
+ hardLinePos += wrapped[w].length;
388
+ // Account for the space consumed by word-wrap break
389
+ if (w < wrapped.length - 1 &&
390
+ hardLinePos < hardLine.length &&
391
+ hardLine[hardLinePos] === " ") {
392
+ charOffset++;
393
+ hardLinePos++;
394
+ }
395
+ vlIndex++;
396
+ }
397
+ if (found)
398
+ break;
399
+ charOffset++; // newline
400
+ }
401
+ }
402
+ // Forward non-mouse data (if any remains) to Ink
403
+ if (stripped) {
404
+ return originalEmit("input", stripped);
405
+ }
406
+ return true; // swallowed entirely
407
+ }
408
+ return originalEmit(event, ...args);
409
+ };
410
+ return () => {
411
+ if (scrollTimer)
412
+ clearTimeout(scrollTimer);
413
+ process.stdout.write(DISABLE_MOUSE);
414
+ process.removeListener("exit", onProcessExit);
415
+ // Restore original emit
416
+ if (mouseEmitRef.current.original) {
417
+ internal_eventEmitter.emit = mouseEmitRef.current.original;
418
+ mouseEmitRef.current.original = null;
419
+ }
420
+ };
421
+ }, [isActive, internal_eventEmitter]);
422
+ // Toggle mouse tracking based on input text: disable when empty so the
423
+ // terminal handles CMD+click for links natively, enable when there's text
424
+ // so click-to-cursor works.
425
+ useEffect(() => {
426
+ const hasText = value.length > 0;
427
+ if (hasText !== hasInputTextRef.current) {
428
+ hasInputTextRef.current = hasText;
429
+ if (isActive) {
430
+ process.stdout.write(hasText ? ENABLE_MOUSE : DISABLE_MOUSE);
431
+ }
432
+ }
433
+ }, [value, isActive]);
434
+ // Helper: delete selected text and return new value + cursor position.
435
+ // Returns null if no selection is active.
436
+ const deleteSelection = () => {
437
+ const sel = getSelectionRange(selectionAnchor, cursor);
438
+ if (!sel)
439
+ return null;
440
+ const [start, end] = sel;
441
+ return { newValue: value.slice(0, start) + value.slice(end), newCursor: start };
442
+ };
443
+ // Helper: clear all input state (used on submit / Ctrl+C / Escape)
444
+ const clearInput = () => {
445
+ setValue("");
446
+ setCursor(0);
447
+ setSelectionAnchor(null);
448
+ setImages([]);
449
+ setPasteText("");
450
+ };
144
451
  useInput((input, key) => {
452
+ // Filter out stray mouse escape sequences so they don't get inserted as text
453
+ if (isMouseEscapeSequence(input))
454
+ return;
145
455
  // Ctrl+T toggles task overlay — works even while agent is running
146
456
  if (key.ctrl && input === "t") {
147
457
  onToggleTasks?.();
@@ -166,8 +476,17 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
166
476
  // Submitted messages will be queued by the parent component.
167
477
  }
168
478
  if (key.return && (key.shift || key.meta)) {
169
- setValue((v) => v.slice(0, cursor) + "\n" + v.slice(cursor));
170
- setCursor((c) => c + 1);
479
+ // If there's a selection, replace it with the newline
480
+ const sel = deleteSelection();
481
+ if (sel) {
482
+ setValue(sel.newValue.slice(0, sel.newCursor) + "\n" + sel.newValue.slice(sel.newCursor));
483
+ setCursor(sel.newCursor + 1);
484
+ }
485
+ else {
486
+ setValue((v) => v.slice(0, cursor) + "\n" + v.slice(cursor));
487
+ setCursor((c) => c + 1);
488
+ }
489
+ setSelectionAnchor(null);
171
490
  return;
172
491
  }
173
492
  if (key.return) {
@@ -179,10 +498,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
179
498
  historyRef.current.push(cmd);
180
499
  historyIndexRef.current = -1;
181
500
  onSubmit(cmd, []);
182
- setValue("");
183
- setCursor(0);
184
- setImages([]);
185
- setPasteText("");
501
+ clearInput();
186
502
  return;
187
503
  }
188
504
  const trimmed = value.trim();
@@ -200,10 +516,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
200
516
  }
201
517
  : undefined;
202
518
  onSubmit(trimmed, [...images], paste);
203
- setValue("");
204
- setCursor(0);
205
- setImages([]);
206
- setPasteText("");
519
+ clearInput();
207
520
  }
208
521
  return;
209
522
  }
@@ -217,10 +530,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
217
530
  }
218
531
  if (key.ctrl && input === "c") {
219
532
  if (value || images.length > 0) {
220
- setValue("");
221
- setCursor(0);
222
- setImages([]);
223
- setPasteText("");
533
+ clearInput();
224
534
  }
225
535
  else {
226
536
  onAbort();
@@ -230,16 +540,53 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
230
540
  if (key.ctrl && input === "d") {
231
541
  process.exit(0);
232
542
  }
233
- // Home / End
543
+ // Ctrl+W delete previous word (or selection)
544
+ if (key.ctrl && input === "w") {
545
+ const sel = deleteSelection();
546
+ if (sel) {
547
+ setValue(sel.newValue);
548
+ setCursor(sel.newCursor);
549
+ }
550
+ else if (cursor > 0) {
551
+ const boundary = prevWordBoundary(value, cursor);
552
+ setValue((v) => v.slice(0, boundary) + v.slice(cursor));
553
+ setCursor(boundary);
554
+ }
555
+ setSelectionAnchor(null);
556
+ return;
557
+ }
558
+ // Home / End — Shift extends selection
234
559
  if (key.ctrl && input === "a") {
560
+ if (key.shift) {
561
+ if (selectionAnchor === null)
562
+ setSelectionAnchor(cursor);
563
+ }
564
+ else {
565
+ setSelectionAnchor(null);
566
+ }
235
567
  setCursor(0);
236
568
  return;
237
569
  }
238
570
  if (key.ctrl && input === "e") {
571
+ if (key.shift) {
572
+ if (selectionAnchor === null)
573
+ setSelectionAnchor(cursor);
574
+ }
575
+ else {
576
+ setSelectionAnchor(null);
577
+ }
239
578
  setCursor(value.length);
240
579
  return;
241
580
  }
242
581
  if (key.backspace || key.delete) {
582
+ // If selection active, delete the selection
583
+ const sel = deleteSelection();
584
+ if (sel) {
585
+ setValue(sel.newValue);
586
+ setCursor(sel.newCursor);
587
+ setSelectionAnchor(null);
588
+ return;
589
+ }
243
590
  if (cursor > 0) {
244
591
  setValue((v) => v.slice(0, cursor - 1) + v.slice(cursor));
245
592
  setCursor((c) => c - 1);
@@ -247,6 +594,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
247
594
  else if (!value && images.length > 0) {
248
595
  setImages((prev) => prev.slice(0, -1));
249
596
  }
597
+ setSelectionAnchor(null);
250
598
  return;
251
599
  }
252
600
  if (key.upArrow) {
@@ -255,6 +603,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
255
603
  setMenuIndex((i) => Math.max(0, i - 1));
256
604
  return;
257
605
  }
606
+ setSelectionAnchor(null);
258
607
  const history = historyRef.current;
259
608
  if (history.length === 0)
260
609
  return;
@@ -272,6 +621,7 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
272
621
  setMenuIndex((i) => Math.min(filteredCommands.length - 1, i + 1));
273
622
  return;
274
623
  }
624
+ setSelectionAnchor(null);
275
625
  const history = historyRef.current;
276
626
  if (historyIndexRef.current === -1) {
277
627
  if (onDownAtEnd)
@@ -292,12 +642,15 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
292
642
  return;
293
643
  }
294
644
  if (key.escape) {
645
+ // First escape clears selection, second clears input (double-tap)
646
+ if (selectionAnchor !== null) {
647
+ setSelectionAnchor(null);
648
+ lastEscRef.current = Date.now();
649
+ return;
650
+ }
295
651
  const now = Date.now();
296
652
  if ((value || images.length > 0) && now - lastEscRef.current < 400) {
297
- setValue("");
298
- setCursor(0);
299
- setImages([]);
300
- setPasteText("");
653
+ clearInput();
301
654
  }
302
655
  lastEscRef.current = now;
303
656
  return;
@@ -313,30 +666,108 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
313
666
  const cmd = "/" + selected.name;
314
667
  setValue(cmd);
315
668
  setCursor(cmd.length);
669
+ setSelectionAnchor(null);
670
+ }
671
+ return;
672
+ }
673
+ // Option+Arrow word jump via raw escape sequences — many terminals send
674
+ // these as input strings rather than setting key.meta + arrow reliably.
675
+ if (OPTION_LEFT_SEQUENCES.has(input)) {
676
+ if (selectionAnchor !== null) {
677
+ const sel = getSelectionRange(selectionAnchor, cursor);
678
+ if (sel)
679
+ setCursor(sel[0]);
680
+ setSelectionAnchor(null);
681
+ }
682
+ else {
683
+ setCursor(prevWordBoundary(value, cursor));
316
684
  }
317
685
  return;
318
686
  }
687
+ if (OPTION_RIGHT_SEQUENCES.has(input)) {
688
+ if (selectionAnchor !== null) {
689
+ const sel = getSelectionRange(selectionAnchor, cursor);
690
+ if (sel)
691
+ setCursor(sel[1]);
692
+ setSelectionAnchor(null);
693
+ }
694
+ else {
695
+ setCursor(nextWordBoundary(value, cursor));
696
+ }
697
+ return;
698
+ }
699
+ // Arrow keys — Shift extends selection, Meta/Option jumps words
319
700
  if (key.leftArrow) {
320
- if (cursor > 0)
701
+ if (key.shift) {
702
+ if (selectionAnchor === null)
703
+ setSelectionAnchor(cursor);
704
+ }
705
+ else if (selectionAnchor !== null) {
706
+ // Collapse selection to the left edge
707
+ const sel = getSelectionRange(selectionAnchor, cursor);
708
+ if (sel)
709
+ setCursor(sel[0]);
710
+ setSelectionAnchor(null);
711
+ return;
712
+ }
713
+ if (key.meta) {
714
+ setCursor(prevWordBoundary(value, cursor));
715
+ }
716
+ else if (cursor > 0) {
321
717
  setCursor((c) => c - 1);
718
+ }
719
+ if (!key.shift)
720
+ setSelectionAnchor(null);
322
721
  return;
323
722
  }
324
723
  if (key.rightArrow) {
325
- if (cursor < value.length)
724
+ if (key.shift) {
725
+ if (selectionAnchor === null)
726
+ setSelectionAnchor(cursor);
727
+ }
728
+ else if (selectionAnchor !== null) {
729
+ // Collapse selection to the right edge
730
+ const sel = getSelectionRange(selectionAnchor, cursor);
731
+ if (sel)
732
+ setCursor(sel[1]);
733
+ setSelectionAnchor(null);
734
+ return;
735
+ }
736
+ if (key.meta) {
737
+ setCursor(nextWordBoundary(value, cursor));
738
+ }
739
+ else if (cursor < value.length) {
326
740
  setCursor((c) => c + 1);
741
+ }
742
+ if (!key.shift)
743
+ setSelectionAnchor(null);
327
744
  return;
328
745
  }
329
746
  if (input) {
330
747
  const normalized = input.replace(/\r\n?/g, "\n");
331
- setValue((v) => v.slice(0, cursor) + normalized + v.slice(cursor));
332
- setCursor((c) => c + normalized.length);
748
+ // If there's a selection, replace it with the typed input
749
+ const sel = deleteSelection();
750
+ if (sel) {
751
+ setValue(sel.newValue.slice(0, sel.newCursor) + normalized + sel.newValue.slice(sel.newCursor));
752
+ const newCur = sel.newCursor + normalized.length;
753
+ setCursor(newCur);
754
+ cursorRef.current = newCur;
755
+ setSelectionAnchor(null);
756
+ }
757
+ else {
758
+ const cur = cursorRef.current;
759
+ setValue((v) => v.slice(0, cur) + normalized + v.slice(cur));
760
+ setCursor(cur + normalized.length);
761
+ cursorRef.current = cur + normalized.length;
762
+ }
333
763
  // Detect paste: Ink delivers pasted text as input.length > 1
334
764
  // For large pastes, Ink may split into multiple chunks, so we
335
765
  // accumulate and debounce to capture the full paste.
336
766
  if (input.length > 1) {
767
+ const pasteStart = sel ? sel.newCursor : cursorRef.current - normalized.length;
337
768
  setPasteText((prev) => {
338
769
  if (!prev)
339
- setPasteOffset(cursor); // record where paste starts on first chunk
770
+ setPasteOffset(pasteStart);
340
771
  return prev + normalized;
341
772
  });
342
773
  if (pasteTimerRef.current)
@@ -351,29 +782,31 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
351
782
  }, { isActive });
352
783
  // Calculate visual lines and cap at MAX_VISIBLE_LINES (scroll to cursor)
353
784
  const visualLines = getVisualLines(value, columns);
354
- const contentWidth = columns - PROMPT.length - BOX_OVERHEAD;
785
+ const contentWidth = Math.max(MIN_CONTENT_WIDTH, columns - PROMPT.length - BOX_OVERHEAD);
355
786
  // Find which visual line and column the cursor is on
356
787
  const cursorLineInfo = useMemo(() => {
357
788
  let pos = 0;
358
789
  const hardLines = value.split("\n");
359
790
  let visualLineIndex = 0;
360
791
  for (let h = 0; h < hardLines.length; h++) {
361
- const wrapped = wrapLine(hardLines[h], contentWidth > 0 ? contentWidth : value.length + 1);
792
+ const hardLine = hardLines[h];
793
+ const wrapped = wrapLine(hardLine, contentWidth);
794
+ let hardLinePos = 0; // track position within the original hard line
362
795
  for (let w = 0; w < wrapped.length; w++) {
363
796
  const lineLen = wrapped[w].length;
364
797
  const lineStart = pos;
365
798
  const lineEnd = pos + lineLen;
366
- // Cursor is on this visual line if it falls within [lineStart, lineEnd]
367
- // For the last wrapped segment of a hard line, also include the newline position
368
- const isLastWrap = w === wrapped.length - 1;
369
- const effectiveEnd = isLastWrap ? lineEnd : lineEnd;
370
- if (cursor >= lineStart && cursor <= effectiveEnd) {
799
+ if (cursor >= lineStart && cursor <= lineEnd) {
371
800
  return { line: visualLineIndex, col: cursor - lineStart };
372
801
  }
373
802
  pos += lineLen;
803
+ hardLinePos += lineLen;
374
804
  // Account for the space consumed by word-wrap break
375
- if (!isLastWrap) {
376
- // wrapped lines don't consume extra chars unless word-broken
805
+ if (w < wrapped.length - 1 &&
806
+ hardLinePos < hardLine.length &&
807
+ hardLine[hardLinePos] === " ") {
808
+ pos++;
809
+ hardLinePos++;
377
810
  }
378
811
  visualLineIndex++;
379
812
  }
@@ -397,6 +830,13 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
397
830
  }
398
831
  const displayLines = visualLines.slice(startLine, startLine + MAX_VISIBLE_LINES);
399
832
  const cursorDisplayLine = cursorLineInfo.line - startLine;
833
+ // Keep layout ref in sync for mouse click handler
834
+ layoutRef.current.value = value;
835
+ layoutRef.current.displayLines = displayLines;
836
+ layoutRef.current.startLine = startLine;
837
+ layoutRef.current.contentWidth = contentWidth;
838
+ layoutRef.current.columns = columns;
839
+ layoutRef.current.hasImages = images.length > 0;
400
840
  // Determine if the input starts with a slash command and find command boundary
401
841
  const isCommand = value.startsWith("/");
402
842
  // Command portion ends at first space (e.g., "/research" in "/research some args")
@@ -405,7 +845,9 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
405
845
  ? value.length
406
846
  : value.indexOf(" ")
407
847
  : 0;
408
- return (_jsxs(Box, { flexDirection: "column", 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
848
+ // Active selection range (absolute character offsets)
849
+ const selection = getSelectionRange(selectionAnchor, cursor);
850
+ 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
409
851
  .map((img, i) => img.kind === "text" ? `[File: ${img.fileName}]` : `[Image #${i + 1}]`)
410
852
  .join(" ") }) })), (() => {
411
853
  if (pasteText && value) {
@@ -433,41 +875,124 @@ export function InputArea({ onSubmit, onAbort, disabled = false, isActive = true
433
875
  const col = cursorLineInfo.col;
434
876
  // Calculate the absolute character offset where this display line starts
435
877
  let lineStartOffset = 0;
436
- for (let j = 0; j < startLine + i; j++) {
437
- lineStartOffset += visualLines[j].length;
438
- }
439
878
  const hardLines = value.split("\n");
440
879
  let offset = 0;
441
880
  let vlIndex = 0;
442
881
  for (let h = 0; h < hardLines.length && vlIndex <= startLine + i; h++) {
443
- const wrapped = wrapLine(hardLines[h], contentWidth > 0 ? contentWidth : value.length + 1);
882
+ const hardLine = hardLines[h];
883
+ const cw = contentWidth > 0 ? contentWidth : value.length + 1;
884
+ const wrapped = wrapLine(hardLine, cw);
885
+ let hardLinePos = 0;
444
886
  for (let w = 0; w < wrapped.length && vlIndex <= startLine + i; w++) {
445
887
  if (vlIndex === startLine + i) {
446
888
  lineStartOffset = offset;
447
889
  }
448
890
  offset += wrapped[w].length;
891
+ hardLinePos += wrapped[w].length;
892
+ // Account for the space consumed by word-wrap break
893
+ if (w < wrapped.length - 1 &&
894
+ hardLinePos < hardLine.length &&
895
+ hardLine[hardLinePos] === " ") {
896
+ offset++;
897
+ hardLinePos++;
898
+ }
449
899
  vlIndex++;
450
900
  }
451
901
  offset++; // newline
452
902
  }
453
- // Determine color for each character based on whether it's in the command portion
454
- const renderSegments = (text, textStartOffset) => {
455
- if (!isCommand || textStartOffset >= commandEndIndex) {
456
- return _jsx(Text, { color: theme.text, children: text });
457
- }
458
- const cmdChars = Math.min(text.length, commandEndIndex - textStartOffset);
903
+ const lineEndOffset = lineStartOffset + line.length;
904
+ // Render a text segment with command coloring and optional selection highlight
905
+ const renderSegment = (text, absOffset, opts) => {
906
+ if (!text)
907
+ return null;
908
+ const inCmd = isCommand && absOffset < commandEndIndex;
909
+ const cmdChars = inCmd ? Math.min(text.length, commandEndIndex - absOffset) : 0;
910
+ const inv = opts?.inverse ?? false;
459
911
  if (cmdChars >= text.length) {
460
- return (_jsx(Text, { color: theme.commandColor, bold: true, children: text }));
912
+ return (_jsx(Text, { color: theme.commandColor, bold: true, inverse: inv, children: text }));
913
+ }
914
+ if (cmdChars > 0) {
915
+ 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) })] }));
461
916
  }
462
- 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) })] }));
917
+ return (_jsx(Text, { color: theme.text, inverse: inv, children: text }));
463
918
  };
464
- const before = showCursor ? line.slice(0, col) : line;
465
- const charUnderCursor = showCursor ? (col < line.length ? line[col] : " ") : "";
466
- const after = showCursor ? line.slice(col + (col < line.length ? 1 : 0)) : "";
467
- const cursorCharOffset = lineStartOffset + col;
468
- const cursorInCommand = isCommand && cursorCharOffset < commandEndIndex;
469
- 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 &&
470
- renderSegments(after, lineStartOffset + col + (col < line.length ? 1 : 0))] }, i));
919
+ // Build segments for: [before-sel] [selected] [cursor] [after-sel]
920
+ // considering that cursor and selection can overlap on this line
921
+ const segments = [];
922
+ let pos = 0; // position within `line`
923
+ // Determine selection overlap with this line (in line-local coords)
924
+ const selLocalStart = selection
925
+ ? Math.max(0, selection[0] - lineStartOffset)
926
+ : line.length;
927
+ const selLocalEnd = selection
928
+ ? Math.min(line.length, selection[1] - lineStartOffset)
929
+ : line.length;
930
+ const hasSelOnLine = selection !== null && selection[0] < lineEndOffset && selection[1] > lineStartOffset;
931
+ if (hasSelOnLine) {
932
+ // Text before selection
933
+ if (selLocalStart > 0) {
934
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(0, selLocalStart), lineStartOffset) }, "pre"));
935
+ pos = selLocalStart;
936
+ }
937
+ // Selected text — render with inverse, but split around cursor if needed
938
+ if (showCursor && col >= selLocalStart && col < selLocalEnd) {
939
+ // Cursor is inside the selection
940
+ if (col > pos) {
941
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos, col), lineStartOffset + pos, {
942
+ inverse: true,
943
+ }) }, "sel-before"));
944
+ }
945
+ // Cursor character (blinks within selection)
946
+ const cursorChar = col < line.length ? line[col] : " ";
947
+ const cursorAbs = lineStartOffset + col;
948
+ const curInCmd = isCommand && cursorAbs < commandEndIndex;
949
+ segments.push(_jsx(Text, { color: curInCmd ? theme.commandColor : theme.text, bold: curInCmd, inverse: cursorVisible, children: cursorChar }, "cursor"));
950
+ const afterCursorPos = col + (col < line.length ? 1 : 0);
951
+ if (afterCursorPos < selLocalEnd) {
952
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(afterCursorPos, selLocalEnd), lineStartOffset + afterCursorPos, { inverse: true }) }, "sel-after"));
953
+ }
954
+ pos = selLocalEnd;
955
+ }
956
+ else {
957
+ // Cursor not on this selection portion — render entire selection inverse
958
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos, selLocalEnd), lineStartOffset + pos, {
959
+ inverse: true,
960
+ }) }, "sel"));
961
+ pos = selLocalEnd;
962
+ }
963
+ // Cursor after selection on this line
964
+ if (showCursor && col >= selLocalEnd) {
965
+ // Text between selection end and cursor
966
+ if (col > pos) {
967
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos, col), lineStartOffset + pos) }, "mid"));
968
+ }
969
+ const cursorChar = col < line.length ? line[col] : " ";
970
+ const cursorAbs = lineStartOffset + col;
971
+ const curInCmd = isCommand && cursorAbs < commandEndIndex;
972
+ segments.push(_jsx(Text, { color: curInCmd ? theme.commandColor : theme.text, bold: curInCmd, inverse: cursorVisible, children: cursorChar }, "cursor"));
973
+ pos = col + (col < line.length ? 1 : 0);
974
+ }
975
+ // Text after selection (and cursor)
976
+ if (pos < line.length) {
977
+ segments.push(_jsx(React.Fragment, { children: renderSegment(line.slice(pos), lineStartOffset + pos) }, "post"));
978
+ }
979
+ }
980
+ else {
981
+ // No selection on this line — original cursor-only rendering
982
+ const before = showCursor ? line.slice(0, col) : line;
983
+ const charUnderCursor = showCursor ? (col < line.length ? line[col] : " ") : "";
984
+ const after = showCursor ? line.slice(col + (col < line.length ? 1 : 0)) : "";
985
+ const cursorCharOffset = lineStartOffset + col;
986
+ const cursorInCommand = isCommand && cursorCharOffset < commandEndIndex;
987
+ segments.push(_jsx(React.Fragment, { children: renderSegment(before, lineStartOffset) }, "before"));
988
+ if (showCursor) {
989
+ segments.push(_jsx(Text, { color: cursorInCommand ? theme.commandColor : theme.text, bold: cursorInCommand, inverse: cursorVisible, children: charUnderCursor }, "cursor"));
990
+ }
991
+ if (after) {
992
+ segments.push(_jsx(React.Fragment, { children: renderSegment(after, lineStartOffset + col + (col < line.length ? 1 : 0)) }, "after"));
993
+ }
994
+ }
995
+ return (_jsxs(Box, { children: [_jsx(Text, { color: disabled ? theme.textDim : theme.inputPrompt, bold: true, children: i === 0 ? PROMPT : " " }), segments] }, i));
471
996
  });
472
997
  })()] }), isSlashMode && filteredCommands.length > 0 && (_jsx(SlashCommandMenu, { commands: commands, filter: slashFilter, selectedIndex: menuIndex }))] }));
473
998
  }