@namzu/cli 14.3.0 → 15.1.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.
- package/README.md +117 -17
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -26
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +3 -2
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +45 -7
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +36 -12
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/{stubs.d.ts → serve.d.ts} +1 -10
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/{stubs.js → serve.js} +1 -24
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/skills.d.ts +19 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +107 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/integrations/mcp/servers.d.ts +7 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +33 -8
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts +8 -0
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +15 -0
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/subagents/runtime.d.ts +2 -0
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +86 -13
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/updates.d.ts.map +1 -1
- package/dist/integrations/updates.js +29 -4
- package/dist/integrations/updates.js.map +1 -1
- package/dist/tui/App.d.ts +10 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +1039 -252
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/ChoicePicker.d.ts +2 -1
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +10 -6
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +19 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +526 -70
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/CopyPicker.d.ts.map +1 -1
- package/dist/tui/CopyPicker.js +3 -4
- package/dist/tui/CopyPicker.js.map +1 -1
- package/dist/tui/EditPromptPicker.d.ts.map +1 -1
- package/dist/tui/EditPromptPicker.js +3 -4
- package/dist/tui/EditPromptPicker.js.map +1 -1
- package/dist/tui/Markdown.d.ts +3 -1
- package/dist/tui/Markdown.d.ts.map +1 -1
- package/dist/tui/Markdown.js +11 -9
- package/dist/tui/Markdown.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +11 -6
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +13 -20
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +47 -11
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/ResumePicker.d.ts.map +1 -1
- package/dist/tui/ResumePicker.js +6 -1
- package/dist/tui/ResumePicker.js.map +1 -1
- package/dist/tui/StatusBar.js +3 -3
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/TextPrompt.d.ts +21 -0
- package/dist/tui/TextPrompt.d.ts.map +1 -0
- package/dist/tui/TextPrompt.js +139 -0
- package/dist/tui/TextPrompt.js.map +1 -0
- package/dist/tui/Transcript.d.ts +9 -10
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +11 -12
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +57 -23
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +136 -86
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +2 -1
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/external-editor.d.ts +25 -0
- package/dist/tui/external-editor.d.ts.map +1 -0
- package/dist/tui/external-editor.js +169 -0
- package/dist/tui/external-editor.js.map +1 -0
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +4 -0
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/live-window.d.ts +17 -11
- package/dist/tui/live-window.d.ts.map +1 -1
- package/dist/tui/live-window.js +64 -20
- package/dist/tui/live-window.js.map +1 -1
- package/dist/tui/login-prompt.d.ts +2 -0
- package/dist/tui/login-prompt.d.ts.map +1 -1
- package/dist/tui/login-prompt.js +8 -1
- package/dist/tui/login-prompt.js.map +1 -1
- package/dist/tui/mentions.d.ts +19 -0
- package/dist/tui/mentions.d.ts.map +1 -1
- package/dist/tui/mentions.js +96 -5
- package/dist/tui/mentions.js.map +1 -1
- package/dist/tui/permission-review.d.ts +59 -0
- package/dist/tui/permission-review.d.ts.map +1 -0
- package/dist/tui/permission-review.js +219 -0
- package/dist/tui/permission-review.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +14 -0
- package/dist/tui/run-interruption.d.ts.map +1 -0
- package/dist/tui/run-interruption.js +67 -0
- package/dist/tui/run-interruption.js.map +1 -0
- package/dist/tui/selection-window.d.ts +18 -0
- package/dist/tui/selection-window.d.ts.map +1 -0
- package/dist/tui/selection-window.js +37 -0
- package/dist/tui/selection-window.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +48 -8
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +129 -47
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-hyperlinks.d.ts +20 -0
- package/dist/tui/terminal-hyperlinks.d.ts.map +1 -0
- package/dist/tui/terminal-hyperlinks.js +70 -0
- package/dist/tui/terminal-hyperlinks.js.map +1 -0
- package/dist/tui/types.d.ts +2 -0
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/tui/use-selection-index.d.ts +8 -0
- package/dist/tui/use-selection-index.d.ts.map +1 -0
- package/dist/tui/use-selection-index.js +13 -0
- package/dist/tui/use-selection-index.js.map +1 -0
- package/dist/tui/workspace-review.d.ts +15 -0
- package/dist/tui/workspace-review.d.ts.map +1 -0
- package/dist/tui/workspace-review.js +67 -0
- package/dist/tui/workspace-review.js.map +1 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +8 -7
- package/dist/commands/stubs.d.ts.map +0 -1
- package/dist/commands/stubs.js.map +0 -1
- package/dist/tui/bottom-spacer.d.ts +0 -95
- package/dist/tui/bottom-spacer.d.ts.map +0 -1
- package/dist/tui/bottom-spacer.js +0 -83
- package/dist/tui/bottom-spacer.js.map +0 -1
package/dist/tui/Composer.js
CHANGED
|
@@ -1,42 +1,147 @@
|
|
|
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, useWindowSize } 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';
|
|
6
7
|
import { terminalDisplayText } from './terminal-display.js';
|
|
7
8
|
import { theme } from './theme.js';
|
|
8
|
-
const
|
|
9
|
+
const MIN_SUGGESTIONS = 6;
|
|
10
|
+
const MAX_SUGGESTIONS = 12;
|
|
11
|
+
const SUGGESTION_FURNITURE_ROWS = 12;
|
|
9
12
|
// A single keypress longer than this (with no newline) is treated as a paste.
|
|
10
13
|
const PASTE_THRESHOLD = 80;
|
|
11
14
|
// A recalled or branch-restored prompt can be much larger than anything the
|
|
12
15
|
// operator typed one key at a time. Keep the exact source in state, but never
|
|
13
|
-
// hand an unbounded string to Ink's wrapping engine.
|
|
14
|
-
//
|
|
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.
|
|
15
18
|
const COMPOSER_DISPLAY_CODE_UNITS = 2048;
|
|
16
19
|
const COMPOSER_DISPLAY_LINES = 8;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
112
|
}
|
|
28
|
-
let
|
|
29
|
-
for (let index =
|
|
113
|
+
let afterBreaks = 0;
|
|
114
|
+
for (let index = cursor; index < end; index += 1) {
|
|
30
115
|
if (source.charCodeAt(index) !== 0x0a)
|
|
31
116
|
continue;
|
|
32
|
-
|
|
33
|
-
if (
|
|
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) {
|
|
34
130
|
start = index + 1;
|
|
35
131
|
break;
|
|
36
132
|
}
|
|
37
133
|
}
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
};
|
|
40
145
|
}
|
|
41
146
|
/**
|
|
42
147
|
* Terminal Ctrl+W / Unix word-rubout semantics.
|
|
@@ -46,17 +151,63 @@ function composerDisplayValue(source) {
|
|
|
46
151
|
* operators already bring to this key.
|
|
47
152
|
*/
|
|
48
153
|
export function deletePreviousWord(source) {
|
|
49
|
-
|
|
50
|
-
while (index > 0 && /\s/u.test(source[index - 1] ?? ''))
|
|
51
|
-
index -= 1;
|
|
52
|
-
while (index > 0 && !/\s/u.test(source[index - 1] ?? ''))
|
|
53
|
-
index -= 1;
|
|
54
|
-
return source.slice(0, index);
|
|
154
|
+
return deletePreviousWordAt(source, source.length).value;
|
|
55
155
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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 terminal = useWindowSize();
|
|
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('');
|
|
59
209
|
const [selected, setSelected] = useState(0);
|
|
210
|
+
const selectedRef = useRef(0);
|
|
60
211
|
// Large pastes are held as attachments (shown as chips) instead of being
|
|
61
212
|
// dumped into the input, then folded into the message on submit.
|
|
62
213
|
const [pastes, setPastes] = useState([]);
|
|
@@ -65,31 +216,82 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
65
216
|
const [attachments, setAttachments] = useState([]);
|
|
66
217
|
const [editPreviousArmed, setEditPreviousArmed] = useState(false);
|
|
67
218
|
const restoredTokenRef = useRef(null);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
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(terminal.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]);
|
|
73
262
|
const reset = useCallback(() => {
|
|
74
|
-
|
|
263
|
+
verticalColumnRef.current = null;
|
|
264
|
+
historyDraftRef.current = null;
|
|
265
|
+
setHistorySearch(null);
|
|
266
|
+
setBuffer('', 0);
|
|
75
267
|
setHistoryIndex(-1);
|
|
76
|
-
|
|
268
|
+
setSelectedIndex(0);
|
|
77
269
|
setPastes([]);
|
|
78
270
|
setAttachments([]);
|
|
79
271
|
setEditPreviousArmed(false);
|
|
80
|
-
}, []);
|
|
272
|
+
}, [setBuffer, setHistoryIndex, setHistorySearch, setSelectedIndex]);
|
|
81
273
|
useEffect(() => {
|
|
82
274
|
if (!draftToRestore || restoredTokenRef.current === draftToRestore.token)
|
|
83
275
|
return;
|
|
84
276
|
restoredTokenRef.current = draftToRestore.token;
|
|
85
|
-
|
|
277
|
+
setBuffer(draftToRestore.text);
|
|
86
278
|
setHistoryIndex(-1);
|
|
87
|
-
|
|
279
|
+
setSelectedIndex(0);
|
|
280
|
+
verticalColumnRef.current = null;
|
|
281
|
+
historyDraftRef.current = null;
|
|
282
|
+
setHistorySearch(null);
|
|
88
283
|
setPastes([]);
|
|
89
284
|
setAttachments(draftToRestore.attachments ? [...draftToRestore.attachments] : []);
|
|
90
285
|
setEditPreviousArmed(false);
|
|
91
286
|
onDraftRestored?.(draftToRestore.token);
|
|
92
|
-
}, [
|
|
287
|
+
}, [
|
|
288
|
+
draftToRestore,
|
|
289
|
+
onDraftRestored,
|
|
290
|
+
setBuffer,
|
|
291
|
+
setHistoryIndex,
|
|
292
|
+
setHistorySearch,
|
|
293
|
+
setSelectedIndex,
|
|
294
|
+
]);
|
|
93
295
|
useInput((input, key) => {
|
|
94
296
|
// Hidden means the screen belongs to something else, so a keypress
|
|
95
297
|
// aimed at that must not also land here.
|
|
@@ -105,10 +307,56 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
105
307
|
// strength of a second flag happening to agree with it.
|
|
106
308
|
if (disabled || hidden)
|
|
107
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
|
+
}
|
|
108
356
|
if (!key.escape && editPreviousArmed)
|
|
109
357
|
setEditPreviousArmed(false);
|
|
110
358
|
const submit = (mode) => {
|
|
111
|
-
const message = [
|
|
359
|
+
const message = [valueRef.current, ...pastes]
|
|
112
360
|
.map((s) => s.trim())
|
|
113
361
|
.filter(Boolean)
|
|
114
362
|
.join('\n\n');
|
|
@@ -122,23 +370,55 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
122
370
|
reset();
|
|
123
371
|
return true;
|
|
124
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
|
+
}
|
|
125
407
|
if (key.return) {
|
|
126
|
-
if (
|
|
408
|
+
if (liveSuggestionKind === 'command') {
|
|
127
409
|
// Run the highlighted command.
|
|
128
|
-
onSubmit(`/${
|
|
410
|
+
onSubmit(`/${liveCommandSuggestions[liveSelection]?.name ?? ''}`);
|
|
129
411
|
reset();
|
|
130
412
|
return;
|
|
131
413
|
}
|
|
414
|
+
if (acceptSuggestion())
|
|
415
|
+
return;
|
|
132
416
|
submit('submit');
|
|
133
417
|
return;
|
|
134
418
|
}
|
|
135
419
|
if (key.tab) {
|
|
136
|
-
if (
|
|
137
|
-
// Complete to the highlighted command, ready for arguments.
|
|
138
|
-
setValue(`/${suggestions[selIdx]?.name ?? ''} `);
|
|
139
|
-
setSelected(0);
|
|
420
|
+
if (acceptSuggestion())
|
|
140
421
|
return;
|
|
141
|
-
}
|
|
142
422
|
submit('queue');
|
|
143
423
|
return;
|
|
144
424
|
}
|
|
@@ -148,7 +428,13 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
148
428
|
// following the hint the status bar is showing them.
|
|
149
429
|
if (escapeInterrupts)
|
|
150
430
|
return;
|
|
151
|
-
const
|
|
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;
|
|
152
438
|
if (empty && onEditPrevious) {
|
|
153
439
|
if (editPreviousArmed) {
|
|
154
440
|
setEditPreviousArmed(false);
|
|
@@ -161,44 +447,162 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
161
447
|
reset();
|
|
162
448
|
return;
|
|
163
449
|
}
|
|
164
|
-
if (key.backspace || key.
|
|
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) {
|
|
165
461
|
// Backspace on an empty line removes the last durable attachment first,
|
|
166
462
|
// then pasted text.
|
|
167
|
-
if (
|
|
463
|
+
if (valueRef.current.length === 0 && attachments.length > 0) {
|
|
168
464
|
setAttachments((p) => p.slice(0, -1));
|
|
169
465
|
return;
|
|
170
466
|
}
|
|
171
|
-
if (
|
|
467
|
+
if (valueRef.current.length === 0 && pastes.length > 0) {
|
|
172
468
|
setPastes((p) => p.slice(0, -1));
|
|
173
469
|
return;
|
|
174
470
|
}
|
|
175
|
-
|
|
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));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
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);
|
|
176
533
|
return;
|
|
177
534
|
}
|
|
178
|
-
if (key.upArrow) {
|
|
179
|
-
if (
|
|
180
|
-
|
|
535
|
+
if (key.upArrow || (key.ctrl && input === 'p')) {
|
|
536
|
+
if (hasLiveSuggestions) {
|
|
537
|
+
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
181
538
|
return;
|
|
182
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);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
verticalColumnRef.current = null;
|
|
183
548
|
if (history.length === 0)
|
|
184
549
|
return;
|
|
185
|
-
|
|
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);
|
|
186
554
|
setHistoryIndex(next);
|
|
187
|
-
|
|
555
|
+
setBuffer(history[history.length - 1 - next] ?? '');
|
|
188
556
|
return;
|
|
189
557
|
}
|
|
190
|
-
if (key.downArrow) {
|
|
191
|
-
if (
|
|
192
|
-
|
|
558
|
+
if (key.downArrow || (key.ctrl && input === 'n')) {
|
|
559
|
+
if (hasLiveSuggestions) {
|
|
560
|
+
setSelectedIndex((i) => Math.min(liveSuggestionCount - 1, i + 1));
|
|
193
561
|
return;
|
|
194
562
|
}
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
197
568
|
return;
|
|
198
569
|
}
|
|
199
|
-
|
|
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);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const next = historyIndexRef.current - 1;
|
|
200
581
|
setHistoryIndex(next);
|
|
201
|
-
|
|
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
|
+
});
|
|
202
606
|
return;
|
|
203
607
|
}
|
|
204
608
|
// Ctrl+V / Alt+V: pull an image off the clipboard and hold it as an attachment.
|
|
@@ -221,7 +625,47 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
221
625
|
return;
|
|
222
626
|
}
|
|
223
627
|
if (key.ctrl && input === 'w') {
|
|
224
|
-
|
|
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);
|
|
225
669
|
return;
|
|
226
670
|
}
|
|
227
671
|
if (key.ctrl || key.meta)
|
|
@@ -234,9 +678,10 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
234
678
|
setPastes((p) => [...p, input]);
|
|
235
679
|
return;
|
|
236
680
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 });
|
|
240
685
|
// After every hook, so the component keeps its state while it is off screen.
|
|
241
686
|
// This is the whole fix: React preserves a mounted component's state, and
|
|
242
687
|
// preserving it is what an unmounting ternary could not do.
|
|
@@ -244,7 +689,18 @@ export function Composer({ disabled = false, onSubmit, history, userCommands = [
|
|
|
244
689
|
return null;
|
|
245
690
|
const promptGlyph = disabled ? '…' : '>';
|
|
246
691
|
const showPlaceholder = !disabled && value.length === 0 && !editPreviousArmed;
|
|
247
|
-
|
|
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] }));
|
|
248
704
|
}
|
|
249
705
|
function attachmentLabel(attachment, index) {
|
|
250
706
|
if (attachment.type === 'stored') {
|