@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.
- package/README.md +123 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +33 -27
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.js +1 -1
- 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/prior-messages.d.ts.map +1 -1
- package/dist/commands/prior-messages.js +13 -4
- package/dist/commands/prior-messages.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +22 -9
- 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/commands/upgrade.d.ts +25 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +182 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/context/runtime-message.d.ts +4 -0
- package/dist/context/runtime-message.d.ts.map +1 -0
- package/dist/context/runtime-message.js +20 -0
- package/dist/context/runtime-message.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/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +10 -1
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/sessions/turn-evidence.d.ts.map +1 -1
- package/dist/integrations/sessions/turn-evidence.js +9 -1
- package/dist/integrations/sessions/turn-evidence.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 +7 -1
- package/dist/integrations/updates.d.ts.map +1 -1
- package/dist/integrations/updates.js +86 -23
- package/dist/integrations/updates.js.map +1 -1
- package/dist/tui/App.d.ts +12 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +1211 -208
- 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 +33 -6
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +569 -48
- 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/LiveActivity.d.ts +3 -0
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +4 -1
- package/dist/tui/LiveActivity.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 +87 -20
- 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/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 +3 -1
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +4 -4
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +39 -20
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +115 -74
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/exit-summary.d.ts +6 -0
- package/dist/tui/exit-summary.d.ts.map +1 -0
- package/dist/tui/exit-summary.js +9 -0
- package/dist/tui/exit-summary.js.map +1 -0
- 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 +19 -6
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/log-pane.d.ts +13 -8
- package/dist/tui/log-pane.d.ts.map +1 -1
- package/dist/tui/log-pane.js +23 -29
- package/dist/tui/log-pane.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/open-browser.d.ts.map +1 -1
- package/dist/tui/open-browser.js +27 -9
- package/dist/tui/open-browser.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/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 +7 -6
- package/dist/commands/stubs.d.ts.map +0 -1
- package/dist/commands/stubs.js.map +0 -1
package/dist/tui/Composer.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
263
|
+
verticalColumnRef.current = null;
|
|
264
|
+
historyDraftRef.current = null;
|
|
265
|
+
setHistorySearch(null);
|
|
266
|
+
setBuffer('', 0);
|
|
27
267
|
setHistoryIndex(-1);
|
|
28
|
-
|
|
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
|
-
|
|
277
|
+
setBuffer(draftToRestore.text);
|
|
38
278
|
setHistoryIndex(-1);
|
|
39
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
408
|
+
if (liveSuggestionKind === 'command') {
|
|
64
409
|
// Run the highlighted command.
|
|
65
|
-
onSubmit(`/${
|
|
410
|
+
onSubmit(`/${liveCommandSuggestions[liveSelection]?.name ?? ''}`);
|
|
66
411
|
reset();
|
|
67
412
|
return;
|
|
68
413
|
}
|
|
69
|
-
|
|
70
|
-
if (message.length === 0 && attachments.length === 0)
|
|
414
|
+
if (acceptSuggestion())
|
|
71
415
|
return;
|
|
72
|
-
|
|
73
|
-
reset();
|
|
416
|
+
submit('submit');
|
|
74
417
|
return;
|
|
75
418
|
}
|
|
76
419
|
if (key.tab) {
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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.
|
|
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 (
|
|
463
|
+
if (valueRef.current.length === 0 && attachments.length > 0) {
|
|
107
464
|
setAttachments((p) => p.slice(0, -1));
|
|
108
465
|
return;
|
|
109
466
|
}
|
|
110
|
-
if (
|
|
467
|
+
if (valueRef.current.length === 0 && pastes.length > 0) {
|
|
111
468
|
setPastes((p) => p.slice(0, -1));
|
|
112
469
|
return;
|
|
113
470
|
}
|
|
114
|
-
|
|
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.
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
555
|
+
setBuffer(history[history.length - 1 - next] ?? '');
|
|
127
556
|
return;
|
|
128
557
|
}
|
|
129
|
-
if (key.downArrow) {
|
|
130
|
-
if (
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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 =
|
|
580
|
+
const next = historyIndexRef.current - 1;
|
|
139
581
|
setHistoryIndex(next);
|
|
140
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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') {
|