@prestyj/cli 4.2.45 → 4.2.56

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