@namzu/cli 14.2.1 → 15.0.0

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 (164) hide show
  1. package/README.md +123 -11
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +33 -27
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/acp.js +1 -1
  6. package/dist/commands/acp.js.map +1 -1
  7. package/dist/commands/doctor.d.ts.map +1 -1
  8. package/dist/commands/doctor.js +2 -0
  9. package/dist/commands/doctor.js.map +1 -1
  10. package/dist/commands/login.d.ts +2 -0
  11. package/dist/commands/login.d.ts.map +1 -1
  12. package/dist/commands/login.js +45 -7
  13. package/dist/commands/login.js.map +1 -1
  14. package/dist/commands/prior-messages.d.ts.map +1 -1
  15. package/dist/commands/prior-messages.js +13 -4
  16. package/dist/commands/prior-messages.js.map +1 -1
  17. package/dist/commands/run.d.ts.map +1 -1
  18. package/dist/commands/run.js +22 -9
  19. package/dist/commands/run.js.map +1 -1
  20. package/dist/commands/{stubs.d.ts → serve.d.ts} +1 -10
  21. package/dist/commands/serve.d.ts.map +1 -0
  22. package/dist/commands/{stubs.js → serve.js} +1 -24
  23. package/dist/commands/serve.js.map +1 -0
  24. package/dist/commands/skills.d.ts +19 -0
  25. package/dist/commands/skills.d.ts.map +1 -0
  26. package/dist/commands/skills.js +107 -0
  27. package/dist/commands/skills.js.map +1 -0
  28. package/dist/commands/upgrade.d.ts +25 -0
  29. package/dist/commands/upgrade.d.ts.map +1 -0
  30. package/dist/commands/upgrade.js +182 -0
  31. package/dist/commands/upgrade.js.map +1 -0
  32. package/dist/context/runtime-message.d.ts +4 -0
  33. package/dist/context/runtime-message.d.ts.map +1 -0
  34. package/dist/context/runtime-message.js +20 -0
  35. package/dist/context/runtime-message.js.map +1 -0
  36. package/dist/integrations/mcp/servers.d.ts +7 -0
  37. package/dist/integrations/mcp/servers.d.ts.map +1 -1
  38. package/dist/integrations/mcp/servers.js +33 -8
  39. package/dist/integrations/mcp/servers.js.map +1 -1
  40. package/dist/integrations/sessions/store.d.ts +8 -0
  41. package/dist/integrations/sessions/store.d.ts.map +1 -1
  42. package/dist/integrations/sessions/store.js +15 -0
  43. package/dist/integrations/sessions/store.js.map +1 -1
  44. package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
  45. package/dist/integrations/sessions/transcript-export.js +10 -1
  46. package/dist/integrations/sessions/transcript-export.js.map +1 -1
  47. package/dist/integrations/sessions/turn-evidence.d.ts.map +1 -1
  48. package/dist/integrations/sessions/turn-evidence.js +9 -1
  49. package/dist/integrations/sessions/turn-evidence.js.map +1 -1
  50. package/dist/integrations/subagents/runtime.d.ts +2 -0
  51. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  52. package/dist/integrations/subagents/runtime.js +86 -13
  53. package/dist/integrations/subagents/runtime.js.map +1 -1
  54. package/dist/integrations/updates.d.ts +7 -1
  55. package/dist/integrations/updates.d.ts.map +1 -1
  56. package/dist/integrations/updates.js +86 -23
  57. package/dist/integrations/updates.js.map +1 -1
  58. package/dist/tui/App.d.ts +12 -1
  59. package/dist/tui/App.d.ts.map +1 -1
  60. package/dist/tui/App.js +1211 -208
  61. package/dist/tui/App.js.map +1 -1
  62. package/dist/tui/ChoicePicker.d.ts +2 -1
  63. package/dist/tui/ChoicePicker.d.ts.map +1 -1
  64. package/dist/tui/ChoicePicker.js +10 -6
  65. package/dist/tui/ChoicePicker.js.map +1 -1
  66. package/dist/tui/Composer.d.ts +33 -6
  67. package/dist/tui/Composer.d.ts.map +1 -1
  68. package/dist/tui/Composer.js +569 -48
  69. package/dist/tui/Composer.js.map +1 -1
  70. package/dist/tui/CopyPicker.d.ts.map +1 -1
  71. package/dist/tui/CopyPicker.js +3 -4
  72. package/dist/tui/CopyPicker.js.map +1 -1
  73. package/dist/tui/EditPromptPicker.d.ts.map +1 -1
  74. package/dist/tui/EditPromptPicker.js +3 -4
  75. package/dist/tui/EditPromptPicker.js.map +1 -1
  76. package/dist/tui/LiveActivity.d.ts +3 -0
  77. package/dist/tui/LiveActivity.d.ts.map +1 -1
  78. package/dist/tui/LiveActivity.js +4 -1
  79. package/dist/tui/LiveActivity.js.map +1 -1
  80. package/dist/tui/Markdown.d.ts +3 -1
  81. package/dist/tui/Markdown.d.ts.map +1 -1
  82. package/dist/tui/Markdown.js +11 -9
  83. package/dist/tui/Markdown.js.map +1 -1
  84. package/dist/tui/PermissionOverlay.d.ts +11 -6
  85. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  86. package/dist/tui/PermissionOverlay.js +13 -20
  87. package/dist/tui/PermissionOverlay.js.map +1 -1
  88. package/dist/tui/Picker.d.ts.map +1 -1
  89. package/dist/tui/Picker.js +87 -20
  90. package/dist/tui/Picker.js.map +1 -1
  91. package/dist/tui/ResumePicker.d.ts.map +1 -1
  92. package/dist/tui/ResumePicker.js +6 -1
  93. package/dist/tui/ResumePicker.js.map +1 -1
  94. package/dist/tui/TextPrompt.d.ts +21 -0
  95. package/dist/tui/TextPrompt.d.ts.map +1 -0
  96. package/dist/tui/TextPrompt.js +139 -0
  97. package/dist/tui/TextPrompt.js.map +1 -0
  98. package/dist/tui/Transcript.d.ts +3 -1
  99. package/dist/tui/Transcript.d.ts.map +1 -1
  100. package/dist/tui/Transcript.js +4 -4
  101. package/dist/tui/Transcript.js.map +1 -1
  102. package/dist/tui/agent.d.ts +39 -20
  103. package/dist/tui/agent.d.ts.map +1 -1
  104. package/dist/tui/agent.js +115 -74
  105. package/dist/tui/agent.js.map +1 -1
  106. package/dist/tui/exit-summary.d.ts +6 -0
  107. package/dist/tui/exit-summary.d.ts.map +1 -0
  108. package/dist/tui/exit-summary.js +9 -0
  109. package/dist/tui/exit-summary.js.map +1 -0
  110. package/dist/tui/external-editor.d.ts +25 -0
  111. package/dist/tui/external-editor.d.ts.map +1 -0
  112. package/dist/tui/external-editor.js +169 -0
  113. package/dist/tui/external-editor.js.map +1 -0
  114. package/dist/tui/index.d.ts.map +1 -1
  115. package/dist/tui/index.js +19 -6
  116. package/dist/tui/index.js.map +1 -1
  117. package/dist/tui/log-pane.d.ts +13 -8
  118. package/dist/tui/log-pane.d.ts.map +1 -1
  119. package/dist/tui/log-pane.js +23 -29
  120. package/dist/tui/log-pane.js.map +1 -1
  121. package/dist/tui/login-prompt.d.ts +2 -0
  122. package/dist/tui/login-prompt.d.ts.map +1 -1
  123. package/dist/tui/login-prompt.js +8 -1
  124. package/dist/tui/login-prompt.js.map +1 -1
  125. package/dist/tui/mentions.d.ts +19 -0
  126. package/dist/tui/mentions.d.ts.map +1 -1
  127. package/dist/tui/mentions.js +96 -5
  128. package/dist/tui/mentions.js.map +1 -1
  129. package/dist/tui/open-browser.d.ts.map +1 -1
  130. package/dist/tui/open-browser.js +27 -9
  131. package/dist/tui/open-browser.js.map +1 -1
  132. package/dist/tui/permission-review.d.ts +59 -0
  133. package/dist/tui/permission-review.d.ts.map +1 -0
  134. package/dist/tui/permission-review.js +219 -0
  135. package/dist/tui/permission-review.js.map +1 -0
  136. package/dist/tui/selection-window.d.ts +18 -0
  137. package/dist/tui/selection-window.d.ts.map +1 -0
  138. package/dist/tui/selection-window.js +37 -0
  139. package/dist/tui/selection-window.js.map +1 -0
  140. package/dist/tui/slashCommands.d.ts +48 -8
  141. package/dist/tui/slashCommands.d.ts.map +1 -1
  142. package/dist/tui/slashCommands.js +129 -47
  143. package/dist/tui/slashCommands.js.map +1 -1
  144. package/dist/tui/terminal-hyperlinks.d.ts +20 -0
  145. package/dist/tui/terminal-hyperlinks.d.ts.map +1 -0
  146. package/dist/tui/terminal-hyperlinks.js +70 -0
  147. package/dist/tui/terminal-hyperlinks.js.map +1 -0
  148. package/dist/tui/types.d.ts +2 -0
  149. package/dist/tui/types.d.ts.map +1 -1
  150. package/dist/tui/use-selection-index.d.ts +8 -0
  151. package/dist/tui/use-selection-index.d.ts.map +1 -0
  152. package/dist/tui/use-selection-index.js +13 -0
  153. package/dist/tui/use-selection-index.js.map +1 -0
  154. package/dist/tui/workspace-review.d.ts +15 -0
  155. package/dist/tui/workspace-review.d.ts.map +1 -0
  156. package/dist/tui/workspace-review.js +67 -0
  157. package/dist/tui/workspace-review.js.map +1 -0
  158. package/dist/version.d.ts +10 -0
  159. package/dist/version.d.ts.map +1 -0
  160. package/dist/version.js +23 -0
  161. package/dist/version.js.map +1 -0
  162. package/package.json +7 -6
  163. package/dist/commands/stubs.d.ts.map +0 -1
  164. package/dist/commands/stubs.js.map +0 -1
@@ -1,16 +1,213 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Box, Text, useInput } from 'ink';
2
+ import { Box, Text, useInput, useStdout } from 'ink';
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
4
  import { readClipboardImage } from '../integrations/clipboard/image.js';
5
+ import { activeFileMention, matchMentionableFiles } from './mentions.js';
5
6
  import { matchSlashCommands } from './slashCommands.js';
7
+ import { terminalDisplayText } from './terminal-display.js';
6
8
  import { theme } from './theme.js';
7
- const MAX_SUGGESTIONS = 6;
9
+ const MIN_SUGGESTIONS = 6;
10
+ const MAX_SUGGESTIONS = 12;
11
+ const SUGGESTION_FURNITURE_ROWS = 12;
8
12
  // A single keypress longer than this (with no newline) is treated as a paste.
9
13
  const PASTE_THRESHOLD = 80;
10
- export function Composer({ disabled = false, onSubmit, history, userCommands = [], hidden = false, escapeInterrupts = false, onNotice, onEditPrevious, draftToRestore = null, onDraftRestored, }) {
11
- const [value, setValue] = useState('');
12
- const [historyIndex, setHistoryIndex] = useState(-1);
14
+ // A recalled or branch-restored prompt can be much larger than anything the
15
+ // operator typed one key at a time. Keep the exact source in state, but never
16
+ // hand an unbounded string to Ink's wrapping engine. The bounded view follows
17
+ // the real cursor rather than assuming every edit happens at the end.
18
+ const COMPOSER_DISPLAY_CODE_UNITS = 2048;
19
+ const COMPOSER_DISPLAY_LINES = 8;
20
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
21
+ /**
22
+ * Visible rows in a completion menu for this terminal height.
23
+ *
24
+ * Each option is budgeted as two rows because a description may wrap. The
25
+ * composer, borders, footer and status area retain twelve rows before the
26
+ * menu grows. A short/unknown terminal keeps the established six-row window;
27
+ * a tall terminal uses the space it actually has, capped so the menu never
28
+ * takes over the whole conversation view.
29
+ */
30
+ export function suggestionWindowSize(rows) {
31
+ if (rows === undefined || !Number.isFinite(rows))
32
+ return MIN_SUGGESTIONS;
33
+ return Math.min(MAX_SUGGESTIONS, Math.max(MIN_SUGGESTIONS, Math.floor((rows - SUGGESTION_FURNITURE_ROWS) / 2)));
34
+ }
35
+ function graphemeBoundaries(source) {
36
+ const boundaries = [0];
37
+ for (const segment of graphemeSegmenter.segment(source)) {
38
+ const end = segment.index + segment.segment.length;
39
+ if (end > (boundaries[boundaries.length - 1] ?? -1))
40
+ boundaries.push(end);
41
+ }
42
+ return boundaries;
43
+ }
44
+ function previousGraphemeBoundary(source, cursor) {
45
+ let previous = 0;
46
+ for (const boundary of graphemeBoundaries(source)) {
47
+ if (boundary >= cursor)
48
+ break;
49
+ previous = boundary;
50
+ }
51
+ return previous;
52
+ }
53
+ function nextGraphemeBoundary(source, cursor) {
54
+ for (const boundary of graphemeBoundaries(source)) {
55
+ if (boundary > cursor)
56
+ return boundary;
57
+ }
58
+ return source.length;
59
+ }
60
+ function lineStart(source, cursor) {
61
+ return cursor <= 0 ? 0 : source.lastIndexOf('\n', cursor - 1) + 1;
62
+ }
63
+ function lineEnd(source, cursor) {
64
+ const newline = source.indexOf('\n', cursor);
65
+ return newline < 0 ? source.length : newline;
66
+ }
67
+ function graphemeColumn(source, start, cursor) {
68
+ let column = 0;
69
+ for (const _segment of graphemeSegmenter.segment(source.slice(start, cursor)))
70
+ column += 1;
71
+ return column;
72
+ }
73
+ function cursorAtGraphemeColumn(source, start, end, column) {
74
+ let current = 0;
75
+ for (const segment of graphemeSegmenter.segment(source.slice(start, end))) {
76
+ if (current === column)
77
+ return start + segment.index;
78
+ current += 1;
79
+ }
80
+ return end;
81
+ }
82
+ function verticalCursor(source, cursor, direction, preferredColumn) {
83
+ const currentStart = lineStart(source, cursor);
84
+ const currentEnd = lineEnd(source, cursor);
85
+ const column = preferredColumn ?? graphemeColumn(source, currentStart, cursor);
86
+ if (direction === -1) {
87
+ if (currentStart === 0)
88
+ return null;
89
+ const previousEnd = currentStart - 1;
90
+ const previousStart = lineStart(source, previousEnd);
91
+ return {
92
+ cursor: cursorAtGraphemeColumn(source, previousStart, previousEnd, column),
93
+ column,
94
+ };
95
+ }
96
+ if (currentEnd === source.length)
97
+ return null;
98
+ const nextStart = currentEnd + 1;
99
+ return {
100
+ cursor: cursorAtGraphemeColumn(source, nextStart, lineEnd(source, nextStart), column),
101
+ column,
102
+ };
103
+ }
104
+ function composerDisplayValue(source, cursor) {
105
+ let start = Math.max(0, cursor - Math.floor(COMPOSER_DISPLAY_CODE_UNITS / 2));
106
+ let end = Math.min(source.length, start + COMPOSER_DISPLAY_CODE_UNITS);
107
+ if (end === source.length)
108
+ start = Math.max(0, end - COMPOSER_DISPLAY_CODE_UNITS);
109
+ if (cursor > end) {
110
+ end = cursor;
111
+ start = Math.max(0, end - COMPOSER_DISPLAY_CODE_UNITS);
112
+ }
113
+ let afterBreaks = 0;
114
+ for (let index = cursor; index < end; index += 1) {
115
+ if (source.charCodeAt(index) !== 0x0a)
116
+ continue;
117
+ afterBreaks += 1;
118
+ if (afterBreaks === Math.min(3, COMPOSER_DISPLAY_LINES - 1)) {
119
+ end = index;
120
+ break;
121
+ }
122
+ }
123
+ let beforeBreaks = 0;
124
+ const beforeBudget = COMPOSER_DISPLAY_LINES - afterBreaks;
125
+ for (let index = cursor - 1; index >= start; index -= 1) {
126
+ if (source.charCodeAt(index) !== 0x0a)
127
+ continue;
128
+ beforeBreaks += 1;
129
+ if (beforeBreaks === beforeBudget) {
130
+ start = index + 1;
131
+ break;
132
+ }
133
+ }
134
+ // Only the view is rounded to complete grapheme boundaries. The exact source
135
+ // remains untouched even when the cap lands inside a combining sequence.
136
+ const boundaries = graphemeBoundaries(source);
137
+ start = boundaries.find((boundary) => boundary >= start) ?? source.length;
138
+ end = [...boundaries].reverse().find((boundary) => boundary <= end) ?? 0;
139
+ return {
140
+ before: terminalDisplayText(source.slice(start, cursor)),
141
+ after: terminalDisplayText(source.slice(cursor, end)),
142
+ leadingEllipsis: start > 0,
143
+ trailingEllipsis: end < source.length,
144
+ };
145
+ }
146
+ /**
147
+ * Terminal Ctrl+W / Unix word-rubout semantics.
148
+ *
149
+ * Punctuation is part of the word: `hello-world` disappears as one unit. The
150
+ * boundary is whitespace, matching the reference composer and the shell habit
151
+ * operators already bring to this key.
152
+ */
153
+ export function deletePreviousWord(source) {
154
+ return deletePreviousWordAt(source, source.length).value;
155
+ }
156
+ function previousWordBoundary(source, cursor) {
157
+ let start = cursor;
158
+ while (start > 0) {
159
+ const previous = previousGraphemeBoundary(source, start);
160
+ if (!/\s/u.test(source.slice(previous, start)))
161
+ break;
162
+ start = previous;
163
+ }
164
+ while (start > 0) {
165
+ const previous = previousGraphemeBoundary(source, start);
166
+ if (/\s/u.test(source.slice(previous, start)))
167
+ break;
168
+ start = previous;
169
+ }
170
+ return start;
171
+ }
172
+ function nextWordBoundary(source, cursor) {
173
+ let end = cursor;
174
+ while (end < source.length) {
175
+ const next = nextGraphemeBoundary(source, end);
176
+ if (!/\s/u.test(source.slice(end, next)))
177
+ break;
178
+ end = next;
179
+ }
180
+ while (end < source.length) {
181
+ const next = nextGraphemeBoundary(source, end);
182
+ if (/\s/u.test(source.slice(end, next)))
183
+ break;
184
+ end = next;
185
+ }
186
+ return end;
187
+ }
188
+ function deletePreviousWordAt(source, cursor) {
189
+ const start = previousWordBoundary(source, cursor);
190
+ return { value: source.slice(0, start) + source.slice(cursor), cursor: start };
191
+ }
192
+ function deleteNextWordAt(source, cursor) {
193
+ const end = nextWordBoundary(source, cursor);
194
+ return { value: source.slice(0, cursor) + source.slice(end), cursor };
195
+ }
196
+ export function Composer({ disabled = false, onSubmit, history, userCommands = [], mentionCandidates = [], hidden = false, escapeInterrupts = false, onNotice, onStepReasoningEffort, onExternalEdit, onEditPrevious, draftToRestore = null, onDraftRestored, }) {
197
+ const { stdout } = useStdout();
198
+ const [value, setValueState] = useState('');
199
+ const valueRef = useRef('');
200
+ const [cursor, setCursorState] = useState(0);
201
+ const cursorRef = useRef(0);
202
+ const [, setHistoryIndexState] = useState(-1);
203
+ const historyIndexRef = useRef(-1);
204
+ const historyDraftRef = useRef(null);
205
+ const [historySearch, setHistorySearchState] = useState(null);
206
+ const historySearchRef = useRef(null);
207
+ const verticalColumnRef = useRef(null);
208
+ const killBufferRef = useRef('');
13
209
  const [selected, setSelected] = useState(0);
210
+ const selectedRef = useRef(0);
14
211
  // Large pastes are held as attachments (shown as chips) instead of being
15
212
  // dumped into the input, then folded into the message on submit.
16
213
  const [pastes, setPastes] = useState([]);
@@ -19,29 +216,82 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
19
216
  const [attachments, setAttachments] = useState([]);
20
217
  const [editPreviousArmed, setEditPreviousArmed] = useState(false);
21
218
  const restoredTokenRef = useRef(null);
22
- const suggestions = matchSlashCommands(value, userCommands).slice(0, MAX_SUGGESTIONS);
23
- const showSuggestions = suggestions.length > 0;
24
- const selIdx = Math.min(selected, Math.max(0, suggestions.length - 1));
219
+ // Keep the complete match set. The six-row limit belongs to the rendered
220
+ // window, not to navigation: slicing here made every later command
221
+ // unreachable no matter how many times the operator pressed Down.
222
+ const commandSuggestions = matchSlashCommands(value, userCommands);
223
+ const activeMention = activeFileMention(value, cursor);
224
+ const fileSuggestions = activeMention
225
+ ? matchMentionableFiles(activeMention.query, mentionCandidates)
226
+ : [];
227
+ const suggestionKind = commandSuggestions.length > 0 ? 'command' : fileSuggestions.length > 0 ? 'file' : null;
228
+ const suggestionCount = suggestionKind === 'command' ? commandSuggestions.length : fileSuggestions.length;
229
+ const suggestionWindow = suggestionWindowSize(stdout.rows);
230
+ const selIdx = Math.min(selected, Math.max(0, suggestionCount - 1));
231
+ const suggestionStart = Math.min(Math.max(0, selIdx - suggestionWindow + 1), Math.max(0, suggestionCount - suggestionWindow));
232
+ const visibleCommandSuggestions = commandSuggestions.slice(suggestionStart, suggestionStart + suggestionWindow);
233
+ const visibleFileSuggestions = fileSuggestions.slice(suggestionStart, suggestionStart + suggestionWindow);
234
+ const displayValue = composerDisplayValue(value, cursor);
235
+ const commandColumnWidth = Math.min(24, Math.max(10, ...visibleCommandSuggestions.map((command) => command.name.length + 4)));
236
+ const setBuffer = useCallback((nextValue, nextCursor = nextValue.length) => {
237
+ valueRef.current = nextValue;
238
+ cursorRef.current = nextCursor;
239
+ setValueState(nextValue);
240
+ setCursorState(nextCursor);
241
+ }, []);
242
+ const setHistoryIndex = useCallback((next) => {
243
+ historyIndexRef.current = next;
244
+ setHistoryIndexState(next);
245
+ }, []);
246
+ const setHistorySearch = useCallback((next) => {
247
+ historySearchRef.current = next;
248
+ setHistorySearchState(next);
249
+ }, []);
250
+ const setSelectedIndex = useCallback((next) => {
251
+ const resolved = typeof next === 'function' ? next(selectedRef.current) : next;
252
+ selectedRef.current = resolved;
253
+ setSelected(resolved);
254
+ }, []);
255
+ const editBuffer = useCallback((nextValue, nextCursor = nextValue.length) => {
256
+ verticalColumnRef.current = null;
257
+ historyDraftRef.current = null;
258
+ setHistorySearch(null);
259
+ setHistoryIndex(-1);
260
+ setBuffer(nextValue, nextCursor);
261
+ }, [setBuffer, setHistoryIndex, setHistorySearch]);
25
262
  const reset = useCallback(() => {
26
- setValue('');
263
+ verticalColumnRef.current = null;
264
+ historyDraftRef.current = null;
265
+ setHistorySearch(null);
266
+ setBuffer('', 0);
27
267
  setHistoryIndex(-1);
28
- setSelected(0);
268
+ setSelectedIndex(0);
29
269
  setPastes([]);
30
270
  setAttachments([]);
31
271
  setEditPreviousArmed(false);
32
- }, []);
272
+ }, [setBuffer, setHistoryIndex, setHistorySearch, setSelectedIndex]);
33
273
  useEffect(() => {
34
274
  if (!draftToRestore || restoredTokenRef.current === draftToRestore.token)
35
275
  return;
36
276
  restoredTokenRef.current = draftToRestore.token;
37
- setValue(draftToRestore.text);
277
+ setBuffer(draftToRestore.text);
38
278
  setHistoryIndex(-1);
39
- setSelected(0);
279
+ setSelectedIndex(0);
280
+ verticalColumnRef.current = null;
281
+ historyDraftRef.current = null;
282
+ setHistorySearch(null);
40
283
  setPastes([]);
41
284
  setAttachments(draftToRestore.attachments ? [...draftToRestore.attachments] : []);
42
285
  setEditPreviousArmed(false);
43
286
  onDraftRestored?.(draftToRestore.token);
44
- }, [draftToRestore, onDraftRestored]);
287
+ }, [
288
+ draftToRestore,
289
+ onDraftRestored,
290
+ setBuffer,
291
+ setHistoryIndex,
292
+ setHistorySearch,
293
+ setSelectedIndex,
294
+ ]);
45
295
  useInput((input, key) => {
46
296
  // Hidden means the screen belongs to something else, so a keypress
47
297
  // aimed at that must not also land here.
@@ -57,28 +307,119 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
57
307
  // strength of a second flag happening to agree with it.
58
308
  if (disabled || hidden)
59
309
  return;
310
+ const liveCommandSuggestions = matchSlashCommands(valueRef.current, userCommands);
311
+ const liveMention = activeFileMention(valueRef.current, cursorRef.current);
312
+ const liveFileSuggestions = liveMention
313
+ ? matchMentionableFiles(liveMention.query, mentionCandidates)
314
+ : [];
315
+ const liveSuggestionKind = liveCommandSuggestions.length > 0
316
+ ? 'command'
317
+ : liveFileSuggestions.length > 0
318
+ ? 'file'
319
+ : null;
320
+ const liveSuggestionCount = liveSuggestionKind === 'command'
321
+ ? liveCommandSuggestions.length
322
+ : liveFileSuggestions.length;
323
+ const hasLiveSuggestions = liveSuggestionKind !== null;
324
+ const liveSelection = Math.min(selectedRef.current, Math.max(0, liveSuggestionCount - 1));
325
+ const acceptSuggestion = () => {
326
+ if (liveSuggestionKind === 'command') {
327
+ editBuffer(`/${liveCommandSuggestions[liveSelection]?.name ?? ''} `);
328
+ setSelectedIndex(0);
329
+ return true;
330
+ }
331
+ if (liveSuggestionKind === 'file' && liveMention) {
332
+ const path = liveFileSuggestions[liveSelection];
333
+ if (!path)
334
+ return false;
335
+ const replacement = `@${path} `;
336
+ editBuffer(valueRef.current.slice(0, liveMention.start) +
337
+ replacement +
338
+ valueRef.current.slice(liveMention.end), liveMention.start + replacement.length);
339
+ setSelectedIndex(0);
340
+ return true;
341
+ }
342
+ return false;
343
+ };
344
+ if (!hasLiveSuggestions &&
345
+ onStepReasoningEffort &&
346
+ ((key.shift && key.downArrow) || (key.meta && input === ','))) {
347
+ onStepReasoningEffort('lower');
348
+ return;
349
+ }
350
+ if (!hasLiveSuggestions &&
351
+ onStepReasoningEffort &&
352
+ ((key.shift && key.upArrow) || (key.meta && input === '.'))) {
353
+ onStepReasoningEffort('raise');
354
+ return;
355
+ }
60
356
  if (!key.escape && editPreviousArmed)
61
357
  setEditPreviousArmed(false);
358
+ const submit = (mode) => {
359
+ const message = [valueRef.current, ...pastes]
360
+ .map((s) => s.trim())
361
+ .filter(Boolean)
362
+ .join('\n\n');
363
+ if (message.length === 0 && attachments.length === 0)
364
+ return false;
365
+ const submittedAttachments = attachments.length > 0 ? attachments : undefined;
366
+ if (mode === 'queue')
367
+ onSubmit(message, submittedAttachments, mode);
368
+ else
369
+ onSubmit(message, submittedAttachments);
370
+ reset();
371
+ return true;
372
+ };
373
+ if ((key.return && (key.shift || key.meta)) || input === '\n') {
374
+ const position = cursorRef.current;
375
+ editBuffer(valueRef.current.slice(0, position) + '\n' + valueRef.current.slice(position), position + 1);
376
+ return;
377
+ }
378
+ if (key.ctrl && (input === 'r' || input === 's')) {
379
+ verticalColumnRef.current = null;
380
+ const browsedHistoryDraft = historyDraftRef.current;
381
+ historyDraftRef.current = null;
382
+ setHistoryIndex(-1);
383
+ let search = historySearchRef.current;
384
+ if (!search) {
385
+ const query = valueRef.current;
386
+ const normalizedQuery = query.toLowerCase();
387
+ search = {
388
+ draft: browsedHistoryDraft ?? { value: query, cursor: cursorRef.current },
389
+ query,
390
+ matches: [...history]
391
+ .reverse()
392
+ .filter((entry) => entry.toLowerCase().includes(normalizedQuery)),
393
+ position: -1,
394
+ };
395
+ }
396
+ const position = input === 'r'
397
+ ? Math.min(search.position + 1, search.matches.length - 1)
398
+ : Math.max(search.position - 1, -1);
399
+ const next = { ...search, position };
400
+ setHistorySearch(next);
401
+ if (position < 0)
402
+ setBuffer(next.draft.value, next.draft.cursor);
403
+ else
404
+ setBuffer(next.matches[position] ?? next.draft.value);
405
+ return;
406
+ }
62
407
  if (key.return) {
63
- if (showSuggestions) {
408
+ if (liveSuggestionKind === 'command') {
64
409
  // Run the highlighted command.
65
- onSubmit(`/${suggestions[selIdx]?.name ?? ''}`);
410
+ onSubmit(`/${liveCommandSuggestions[liveSelection]?.name ?? ''}`);
66
411
  reset();
67
412
  return;
68
413
  }
69
- const message = [value, ...pastes].map((s) => s.trim()).filter(Boolean).join('\n\n');
70
- if (message.length === 0 && attachments.length === 0)
414
+ if (acceptSuggestion())
71
415
  return;
72
- onSubmit(message, attachments.length > 0 ? attachments : undefined);
73
- reset();
416
+ submit('submit');
74
417
  return;
75
418
  }
76
419
  if (key.tab) {
77
- if (showSuggestions) {
78
- // Complete to the highlighted command, ready for arguments.
79
- setValue(`/${suggestions[selIdx]?.name ?? ''} `);
80
- setSelected(0);
81
- }
420
+ if (acceptSuggestion())
421
+ return;
422
+ submit('queue');
82
423
  return;
83
424
  }
84
425
  if (key.escape) {
@@ -87,7 +428,13 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
87
428
  // following the hint the status bar is showing them.
88
429
  if (escapeInterrupts)
89
430
  return;
90
- const empty = value.length === 0 && pastes.length === 0 && attachments.length === 0;
431
+ const activeSearch = historySearchRef.current;
432
+ if (activeSearch) {
433
+ setBuffer(activeSearch.draft.value, activeSearch.draft.cursor);
434
+ setHistorySearch(null);
435
+ return;
436
+ }
437
+ const empty = valueRef.current.length === 0 && pastes.length === 0 && attachments.length === 0;
91
438
  if (empty && onEditPrevious) {
92
439
  if (editPreviousArmed) {
93
440
  setEditPreviousArmed(false);
@@ -100,53 +447,171 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
100
447
  reset();
101
448
  return;
102
449
  }
103
- if (key.backspace || key.delete) {
450
+ if (key.backspace && (key.meta || key.ctrl)) {
451
+ const source = valueRef.current;
452
+ const position = cursorRef.current;
453
+ const start = previousWordBoundary(source, position);
454
+ if (start < position)
455
+ killBufferRef.current = source.slice(start, position);
456
+ const result = deletePreviousWordAt(valueRef.current, cursorRef.current);
457
+ editBuffer(result.value, result.cursor);
458
+ return;
459
+ }
460
+ if (key.backspace) {
104
461
  // Backspace on an empty line removes the last durable attachment first,
105
462
  // then pasted text.
106
- if (value.length === 0 && attachments.length > 0) {
463
+ if (valueRef.current.length === 0 && attachments.length > 0) {
107
464
  setAttachments((p) => p.slice(0, -1));
108
465
  return;
109
466
  }
110
- if (value.length === 0 && pastes.length > 0) {
467
+ if (valueRef.current.length === 0 && pastes.length > 0) {
111
468
  setPastes((p) => p.slice(0, -1));
112
469
  return;
113
470
  }
114
- setValue((v) => v.slice(0, -1));
471
+ const position = cursorRef.current;
472
+ const previous = previousGraphemeBoundary(valueRef.current, position);
473
+ editBuffer(valueRef.current.slice(0, previous) + valueRef.current.slice(position), previous);
474
+ return;
475
+ }
476
+ if (key.delete || (key.ctrl && input === 'd')) {
477
+ const position = cursorRef.current;
478
+ const next = nextGraphemeBoundary(valueRef.current, position);
479
+ editBuffer(valueRef.current.slice(0, position) + valueRef.current.slice(next), position);
480
+ return;
481
+ }
482
+ if ((key.leftArrow && (key.meta || key.ctrl)) || (key.meta && input === 'b')) {
483
+ verticalColumnRef.current = null;
484
+ const previous = previousWordBoundary(valueRef.current, cursorRef.current);
485
+ cursorRef.current = previous;
486
+ setCursorState(previous);
487
+ return;
488
+ }
489
+ if ((key.rightArrow && (key.meta || key.ctrl)) || (key.meta && input === 'f')) {
490
+ verticalColumnRef.current = null;
491
+ const next = nextWordBoundary(valueRef.current, cursorRef.current);
492
+ cursorRef.current = next;
493
+ setCursorState(next);
494
+ return;
495
+ }
496
+ if (key.leftArrow || (key.ctrl && input === 'b')) {
497
+ verticalColumnRef.current = null;
498
+ const previous = previousGraphemeBoundary(valueRef.current, cursorRef.current);
499
+ cursorRef.current = previous;
500
+ setCursorState(previous);
501
+ return;
502
+ }
503
+ if (key.rightArrow || (key.ctrl && input === 'f')) {
504
+ verticalColumnRef.current = null;
505
+ const next = nextGraphemeBoundary(valueRef.current, cursorRef.current);
506
+ cursorRef.current = next;
507
+ setCursorState(next);
508
+ return;
509
+ }
510
+ if (hasLiveSuggestions && (key.home || key.end || key.pageUp || key.pageDown)) {
511
+ if (key.home)
512
+ setSelectedIndex(0);
513
+ else if (key.end)
514
+ setSelectedIndex(liveSuggestionCount - 1);
515
+ else if (key.pageUp)
516
+ setSelectedIndex((index) => Math.max(0, index - suggestionWindow));
517
+ else
518
+ setSelectedIndex((index) => Math.min(liveSuggestionCount - 1, index + suggestionWindow));
115
519
  return;
116
520
  }
117
- if (key.upArrow) {
118
- if (showSuggestions) {
119
- setSelected((i) => Math.max(0, i - 1));
521
+ if (key.home || (key.ctrl && input === 'a')) {
522
+ verticalColumnRef.current = null;
523
+ const start = lineStart(valueRef.current, cursorRef.current);
524
+ cursorRef.current = start;
525
+ setCursorState(start);
526
+ return;
527
+ }
528
+ if (key.end || (key.ctrl && input === 'e')) {
529
+ verticalColumnRef.current = null;
530
+ const end = lineEnd(valueRef.current, cursorRef.current);
531
+ cursorRef.current = end;
532
+ setCursorState(end);
533
+ return;
534
+ }
535
+ if (key.upArrow || (key.ctrl && input === 'p')) {
536
+ if (hasLiveSuggestions) {
537
+ setSelectedIndex((i) => Math.max(0, i - 1));
538
+ return;
539
+ }
540
+ const vertical = verticalCursor(valueRef.current, cursorRef.current, -1, verticalColumnRef.current ?? undefined);
541
+ if (vertical) {
542
+ verticalColumnRef.current = vertical.column;
543
+ cursorRef.current = vertical.cursor;
544
+ setCursorState(vertical.cursor);
120
545
  return;
121
546
  }
547
+ verticalColumnRef.current = null;
122
548
  if (history.length === 0)
123
549
  return;
124
- const next = Math.min(historyIndex + 1, history.length - 1);
550
+ if (historyIndexRef.current < 0) {
551
+ historyDraftRef.current = { value: valueRef.current, cursor: cursorRef.current };
552
+ }
553
+ const next = Math.min(historyIndexRef.current + 1, history.length - 1);
125
554
  setHistoryIndex(next);
126
- setValue(history[history.length - 1 - next] ?? '');
555
+ setBuffer(history[history.length - 1 - next] ?? '');
127
556
  return;
128
557
  }
129
- if (key.downArrow) {
130
- if (showSuggestions) {
131
- setSelected((i) => Math.min(suggestions.length - 1, i + 1));
558
+ if (key.downArrow || (key.ctrl && input === 'n')) {
559
+ if (hasLiveSuggestions) {
560
+ setSelectedIndex((i) => Math.min(liveSuggestionCount - 1, i + 1));
132
561
  return;
133
562
  }
134
- if (historyIndex <= 0) {
135
- reset();
563
+ const vertical = verticalCursor(valueRef.current, cursorRef.current, 1, verticalColumnRef.current ?? undefined);
564
+ if (vertical) {
565
+ verticalColumnRef.current = vertical.column;
566
+ cursorRef.current = vertical.cursor;
567
+ setCursorState(vertical.cursor);
568
+ return;
569
+ }
570
+ verticalColumnRef.current = null;
571
+ if (historyIndexRef.current < 0)
572
+ return;
573
+ if (historyIndexRef.current === 0) {
574
+ const draft = historyDraftRef.current;
575
+ historyDraftRef.current = null;
576
+ setHistoryIndex(-1);
577
+ setBuffer(draft?.value ?? '', draft?.cursor ?? 0);
136
578
  return;
137
579
  }
138
- const next = historyIndex - 1;
580
+ const next = historyIndexRef.current - 1;
139
581
  setHistoryIndex(next);
140
- setValue(history[history.length - 1 - next] ?? '');
582
+ setBuffer(history[history.length - 1 - next] ?? '');
583
+ return;
584
+ }
585
+ if (key.ctrl && input === 'g') {
586
+ if (!onExternalEdit) {
587
+ onNotice?.('External editor support is unavailable in this terminal.');
588
+ return;
589
+ }
590
+ const seed = [valueRef.current, ...pastes]
591
+ .map((part) => part.trim())
592
+ .filter(Boolean)
593
+ .join('\n\n');
594
+ void onExternalEdit(seed)
595
+ .then((edited) => {
596
+ const cleaned = edited.trimEnd();
597
+ setPastes([]);
598
+ setHistoryIndex(-1);
599
+ setHistorySearch(null);
600
+ setSelectedIndex(0);
601
+ editBuffer(cleaned, cleaned.length);
602
+ })
603
+ .catch((error) => {
604
+ onNotice?.(`Cannot open external editor: ${error instanceof Error ? error.message : String(error)}`);
605
+ });
141
606
  return;
142
607
  }
143
- // Ctrl+V: pull an image off the clipboard and hold it as an attachment.
608
+ // Ctrl+V / Alt+V: pull an image off the clipboard and hold it as an attachment.
144
609
  //
145
610
  // Every outcome says something. The status bar advertises this key, so
146
611
  // a press that produces no chip and no words is indistinguishable from
147
612
  // a key that was never wired up — and the operator's next move differs
148
613
  // per reason: copy an image, install a tool, or stop pressing it.
149
- if (key.ctrl && input === 'v') {
614
+ if ((key.ctrl || key.meta) && input === 'v') {
150
615
  const read = readClipboardImage();
151
616
  if (read.kind === 'image') {
152
617
  setAttachments((p) => [...p, read.image]);
@@ -159,6 +624,50 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
159
624
  }
160
625
  return;
161
626
  }
627
+ if (key.ctrl && input === 'w') {
628
+ const source = valueRef.current;
629
+ const position = cursorRef.current;
630
+ const start = previousWordBoundary(source, position);
631
+ if (start < position)
632
+ killBufferRef.current = source.slice(start, position);
633
+ const result = deletePreviousWordAt(valueRef.current, cursorRef.current);
634
+ editBuffer(result.value, result.cursor);
635
+ return;
636
+ }
637
+ if (key.ctrl && input === 'u') {
638
+ const position = cursorRef.current;
639
+ const start = lineStart(valueRef.current, position);
640
+ if (start < position)
641
+ killBufferRef.current = valueRef.current.slice(start, position);
642
+ editBuffer(valueRef.current.slice(0, start) + valueRef.current.slice(position), start);
643
+ return;
644
+ }
645
+ if (key.ctrl && input === 'k') {
646
+ const position = cursorRef.current;
647
+ const end = lineEnd(valueRef.current, position);
648
+ if (position < end)
649
+ killBufferRef.current = valueRef.current.slice(position, end);
650
+ editBuffer(valueRef.current.slice(0, position) + valueRef.current.slice(end), position);
651
+ return;
652
+ }
653
+ if (key.meta && input === 'd') {
654
+ const source = valueRef.current;
655
+ const position = cursorRef.current;
656
+ const end = nextWordBoundary(source, position);
657
+ if (position < end)
658
+ killBufferRef.current = source.slice(position, end);
659
+ const result = deleteNextWordAt(valueRef.current, cursorRef.current);
660
+ editBuffer(result.value, result.cursor);
661
+ return;
662
+ }
663
+ if (key.ctrl && input === 'y') {
664
+ const killed = killBufferRef.current;
665
+ if (killed.length === 0)
666
+ return;
667
+ const position = cursorRef.current;
668
+ editBuffer(valueRef.current.slice(0, position) + killed + valueRef.current.slice(position), position + killed.length);
669
+ return;
670
+ }
162
671
  if (key.ctrl || key.meta)
163
672
  return;
164
673
  if (input.length === 0)
@@ -169,9 +678,10 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
169
678
  setPastes((p) => [...p, input]);
170
679
  return;
171
680
  }
172
- setSelected(0);
173
- setValue((v) => v + input);
174
- }, { isActive: true });
681
+ setSelectedIndex(0);
682
+ const position = cursorRef.current;
683
+ editBuffer(valueRef.current.slice(0, position) + input + valueRef.current.slice(position), position + input.length);
684
+ }, { isActive: !disabled && !hidden });
175
685
  // After every hook, so the component keeps its state while it is off screen.
176
686
  // This is the whole fix: React preserves a mounted component's state, and
177
687
  // preserving it is what an unmounting ternary could not do.
@@ -179,7 +689,18 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
179
689
  return null;
180
690
  const promptGlyph = disabled ? '…' : '>';
181
691
  const showPlaceholder = !disabled && value.length === 0 && !editPreviousArmed;
182
- return (_jsxs(Box, { flexDirection: "column", children: [pastes.length > 0 || attachments.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingBottom: 1, children: [attachments.map((attachment, i) => (_jsxs(Text, { color: theme.accent.tool, children: ["\u2398 ", attachmentLabel(attachment, i)] }, `attachment-${i}`))), pastes.map((p, i) => (_jsxs(Text, { color: theme.text.secondary, children: ["\u2398 Pasted text #", i + 1, " (+", p.split('\n').length, " lines)"] }, `paste-${i}`)))] })) : null, _jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: disabled ? theme.text.muted : theme.accent.user, bold: true, children: promptGlyph }) }), _jsx(Box, { flexGrow: 1, children: showPlaceholder ? (_jsx(Text, { color: theme.text.muted, children: "Type a message\u2026 (/help for commands)" })) : editPreviousArmed ? (_jsx(Text, { color: theme.text.muted, children: "Press Esc again to edit a previous prompt" })) : (_jsxs(Text, { color: disabled ? theme.text.muted : theme.text.primary, wrap: "wrap", children: [value, disabled ? null : _jsx(Text, { color: theme.border.focus, children: "\u258F" })] })) })] }), showSuggestions ? (_jsx(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, children: suggestions.map((cmd, i) => (_jsxs(Box, { children: [_jsx(Box, { width: 12, flexShrink: 0, children: _jsxs(Text, { color: i === selIdx ? theme.accent.user : theme.text.secondary, bold: i === selIdx, children: [i === selIdx ? '› ' : ' ', "/", cmd.name] }) }), _jsx(Text, { color: theme.text.muted, children: cmd.description })] }, cmd.name))) })) : null] }));
692
+ const historySearchQuery = historySearch
693
+ ? composerDisplayValue(historySearch.query, historySearch.query.length)
694
+ : null;
695
+ return (_jsxs(Box, { flexDirection: "column", children: [pastes.length > 0 || attachments.length > 0 ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingBottom: 1, children: [attachments.map((attachment, i) => (_jsxs(Text, { color: theme.accent.tool, children: ["\u2398 ", attachmentLabel(attachment, i)] }, `attachment-${i}`))), pastes.map((p, i) => (_jsxs(Text, { color: theme.text.secondary, children: ["\u2398 Pasted text #", i + 1, " (+", p.split('\n').length, " lines)"] }, `paste-${i}`)))] })) : null, _jsxs(Box, { paddingX: 1, children: [_jsx(Box, { width: 2, flexShrink: 0, children: _jsx(Text, { color: disabled ? theme.text.muted : theme.accent.user, bold: true, children: promptGlyph }) }), _jsx(Box, { flexGrow: 1, children: showPlaceholder ? (_jsx(Text, { color: theme.text.muted, children: "Type a message\u2026 (/help for commands)" })) : editPreviousArmed ? (_jsx(Text, { color: theme.text.muted, children: "Press Esc again to edit a previous prompt" })) : (_jsxs(Text, { color: disabled ? theme.text.muted : theme.text.primary, wrap: "wrap", children: [displayValue.leadingEllipsis ? '… ' : '', displayValue.before, disabled ? null : _jsx(Text, { color: theme.border.focus, children: "\u258F" }), displayValue.after, displayValue.trailingEllipsis ? ' …' : ''] })) })] }), historySearch && historySearchQuery ? (_jsx(Box, { paddingX: 1, paddingTop: 1, children: _jsxs(Text, { color: theme.text.muted, children: ["history \u201C", historySearchQuery.leadingEllipsis ? '… ' : '', historySearchQuery.before, "\u201D \u00B7", ' ', historySearch.position < 0
696
+ ? `draft · ${historySearch.matches.length} match${historySearch.matches.length === 1 ? '' : 'es'}`
697
+ : `${historySearch.position + 1}/${historySearch.matches.length}`, ' ', "\u00B7 Ctrl+R older \u00B7 Ctrl+S newer \u00B7 Esc restore"] }) })) : null, suggestionKind === 'command' ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, width: "100%", children: [visibleCommandSuggestions.map((cmd, i) => {
698
+ const absoluteIndex = suggestionStart + i;
699
+ return (_jsxs(Box, { width: "100%", flexDirection: "row", children: [_jsx(Box, { width: commandColumnWidth, flexShrink: 0, children: _jsxs(Text, { color: absoluteIndex === selIdx ? theme.accent.user : theme.text.secondary, bold: absoluteIndex === selIdx, children: [absoluteIndex === selIdx ? '› ' : ' ', "/", cmd.name] }) }), _jsx(Box, { flexGrow: 1, minWidth: 0, children: _jsx(Text, { color: theme.text.muted, wrap: "wrap", children: cmd.description }) })] }, cmd.name));
700
+ }), _jsxs(Text, { color: theme.text.muted, children: [selIdx + 1, "/", commandSuggestions.length, " \u00B7 \u2191\u2193 navigate \u00B7 PgUp/PgDn jump \u00B7 Enter run \u00B7 Tab complete"] })] })) : suggestionKind === 'file' ? (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingTop: 1, width: "100%", children: [visibleFileSuggestions.map((path, i) => {
701
+ const absoluteIndex = suggestionStart + i;
702
+ return (_jsxs(Text, { color: absoluteIndex === selIdx ? theme.accent.user : theme.text.secondary, bold: absoluteIndex === selIdx, children: [absoluteIndex === selIdx ? '› ' : ' ', "@", path] }, path));
703
+ }), _jsxs(Text, { color: theme.text.muted, children: [selIdx + 1, "/", fileSuggestions.length, " \u00B7 \u2191\u2193 navigate \u00B7 PgUp/PgDn jump \u00B7 Enter/Tab insert"] })] })) : null] }));
183
704
  }
184
705
  function attachmentLabel(attachment, index) {
185
706
  if (attachment.type === 'stored') {