termux-dev 1.5.0 → 1.8.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 +215 -163
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +94 -0
- package/dist/cli/banner.js +7 -3
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/headless.js +29 -14
- package/dist/cli/index.js +1395 -474
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/markdown.js +158 -36
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +135 -0
- package/dist/cli/prompt.js +338 -121
- package/dist/cli/prompt_palette.js +6 -0
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +27 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +114 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1021 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +210 -0
- package/dist/core/locales/en.js +221 -0
- package/dist/core/locales/es.js +210 -0
- package/dist/core/locales/fr.js +210 -0
- package/dist/core/locales/ja.js +210 -0
- package/dist/core/locales/pt.js +210 -0
- package/dist/core/locales/ru.js +210 -0
- package/dist/core/locales/uk.js +210 -0
- package/dist/core/locales/zh.js +210 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +4 -2
- package/assets/preview_work1.png +0 -0
package/dist/cli/prompt.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
2
|
import { scanProjectFiles } from './files.js';
|
|
3
|
-
import {
|
|
3
|
+
import { IMAGE_TAG_SOURCE, imageTag, pruneMedia, processPastedFilePath, readClipboardText, saveClipboardImage } from './clipboard.js';
|
|
4
4
|
import { getCurrentTheme } from './theme.js';
|
|
5
5
|
import { CustomCommandManager } from '../core/commands.js';
|
|
6
6
|
import { formatPromptHeader } from './modeborder.js';
|
|
7
7
|
import { filterCommandPalette, estimatePromptTokens, formatPromptTokenBadge } from './prompt_palette.js';
|
|
8
8
|
import { PromptHistoryNavigator } from './history_search.js';
|
|
9
|
+
import { buildPromptFrame, palette, renderUserEcho, resolveMode } from './ui.js';
|
|
10
|
+
import { padEndWidth, truncateWidth } from './width.js';
|
|
9
11
|
export const SLASH_COMMANDS = [
|
|
10
12
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
11
13
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
@@ -14,23 +16,28 @@ export const SLASH_COMMANDS = [
|
|
|
14
16
|
{ cmd: '/usage', desc: 'Show network bandwidth, data saver & token cost' },
|
|
15
17
|
{ cmd: '/export', desc: 'Export session conversation to Markdown' },
|
|
16
18
|
{ cmd: '/mcp', desc: 'Manage Model Context Protocol (MCP) servers & tools' },
|
|
17
|
-
{ cmd: '/theme', desc: 'Switch UI theme (
|
|
19
|
+
{ cmd: '/theme', desc: 'Switch UI theme (18 palettes: Nord, Dracula, Tokyo Night...)' },
|
|
18
20
|
{ cmd: '/doctor', desc: 'Run system & environment health diagnostics' },
|
|
19
21
|
{ cmd: '/settings', desc: 'Configure permissions & auto-approval' },
|
|
20
22
|
{ cmd: '/update', desc: 'Check and install updates from GitHub' },
|
|
21
23
|
{ cmd: '/model', desc: 'Switch model for current provider' },
|
|
24
|
+
{ cmd: '/effort', desc: 'Set the reasoning effort for the current model' },
|
|
22
25
|
{ cmd: '/provider', desc: 'Switch AI provider (OpenRouter, Google, etc.)' },
|
|
23
26
|
{ cmd: '/plan', desc: 'Switch to PLAN mode (architect & planner)' },
|
|
24
27
|
{ cmd: '/agent', desc: 'Switch to AGENT mode (coder & executor)' },
|
|
25
|
-
{ cmd: '/image', desc: 'Paste image from clipboard as
|
|
28
|
+
{ cmd: '/image', desc: 'Paste the image from the clipboard (same as Ctrl+V or Alt+V)' },
|
|
26
29
|
{ cmd: '/serve', desc: 'Start local web server for web/game preview' },
|
|
30
|
+
{ cmd: '/copy', desc: 'Copy the last answer of the AI to the clipboard' },
|
|
31
|
+
{ cmd: '/rewind', desc: 'Go back to one of your messages: the chat after it is removed' },
|
|
32
|
+
{ cmd: '/rename', desc: 'Rename the chat (/rename <name>), or let the AI name it again' },
|
|
27
33
|
{ cmd: '/memory', desc: 'View, add, or clear project memory bank' },
|
|
34
|
+
{ cmd: '/permissions', desc: 'What is allowed without asking; /permissions clear resets it' },
|
|
28
35
|
{ cmd: '/undo', desc: 'Revert last file changes made by AI' },
|
|
29
36
|
{ cmd: '/diff', desc: 'Show git diff of modified project files' },
|
|
30
37
|
{ cmd: '/commit', desc: 'AI-generated git commit message & commit' },
|
|
31
38
|
{ cmd: '/status', desc: 'Show git repository file status' },
|
|
32
39
|
{ cmd: '/compact', desc: 'Compact & summarize chat context' },
|
|
33
|
-
{ cmd: '/clear', desc: 'Clear
|
|
40
|
+
{ cmd: '/clear', desc: 'Clear context and start a new session' },
|
|
34
41
|
{ cmd: '/init', desc: 'Generate AGENTS.md project instructions' },
|
|
35
42
|
{ cmd: '/config', desc: 'View current configuration' },
|
|
36
43
|
{ cmd: '/help', desc: 'Show all available commands' },
|
|
@@ -43,19 +50,25 @@ function stripAnsi(str) {
|
|
|
43
50
|
export function askPrompt(opts = {}) {
|
|
44
51
|
return new Promise((resolve) => {
|
|
45
52
|
const theme = getCurrentTheme();
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const cleanLayout = opts.ui?.layout !== 'classic';
|
|
54
|
+
const promptPrefix = cleanLayout ? pc.cyan('›') + ' ' : pc.dim('│') + ' ';
|
|
55
|
+
const promptPrefixWidth = cleanLayout ? 2 : 3;
|
|
56
|
+
if (!cleanLayout) {
|
|
57
|
+
if (opts.ui?.modeBorder) {
|
|
58
|
+
const header = formatPromptHeader(!!opts.planMode, !!opts.isYolo, opts.message);
|
|
59
|
+
console.log(header);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const msg = opts.message
|
|
63
|
+
? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
|
|
64
|
+
: `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
|
|
65
|
+
console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
|
|
66
|
+
}
|
|
55
67
|
}
|
|
56
68
|
let availableFiles = [];
|
|
57
69
|
let customCommandsList = [];
|
|
58
70
|
scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
|
|
71
|
+
void pruneMedia().catch(() => { });
|
|
59
72
|
CustomCommandManager.listCommands()
|
|
60
73
|
.then((c) => (customCommandsList = c.map((x) => ({ cmd: x.cmd, desc: x.desc }))))
|
|
61
74
|
.catch(() => { });
|
|
@@ -77,19 +90,99 @@ export function askPrompt(opts = {}) {
|
|
|
77
90
|
let nextPasteId = 1;
|
|
78
91
|
let inBracketedPaste = false;
|
|
79
92
|
let bracketedBuffer = '';
|
|
80
|
-
const placeholder = opts.placeholder || 'Describe a task, @file, /help,
|
|
93
|
+
const placeholder = opts.placeholder || 'Describe a task, @file, /help, Ctrl+V pastes an image, or press Tab to switch mode';
|
|
81
94
|
let disposed = false;
|
|
82
95
|
process.stdin.resume();
|
|
83
96
|
if (process.stdin.isTTY) {
|
|
84
97
|
process.stdin.setRawMode(true);
|
|
85
98
|
process.stdout.write('\x1b[?2004h'); // Enable bracketed paste mode
|
|
86
99
|
}
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
// ── framed prompt (clean layout) ──
|
|
101
|
+
// The frame is a block of rows; the cursor rests on the input row. Redrawing moves up
|
|
102
|
+
// to the top of the block, rewrites every row and returns to the cursor position.
|
|
103
|
+
let frameRowsDrawn = 0;
|
|
104
|
+
let frameCursorRow = 0;
|
|
105
|
+
function cleanDropdownRows(items, cols, rows) {
|
|
106
|
+
if (items.length === 0)
|
|
107
|
+
return [];
|
|
108
|
+
if (selectedIndex >= items.length)
|
|
109
|
+
selectedIndex = items.length - 1;
|
|
110
|
+
if (selectedIndex < 0)
|
|
111
|
+
selectedIndex = 0;
|
|
112
|
+
const pageSize = Math.max(2, Math.min(6, rows - 7));
|
|
113
|
+
let startIndex = 0;
|
|
114
|
+
if (items.length > pageSize && selectedIndex >= pageSize) {
|
|
115
|
+
startIndex = Math.min(selectedIndex - pageSize + 1, items.length - pageSize);
|
|
116
|
+
}
|
|
117
|
+
const endIndex = Math.min(startIndex + pageSize, items.length);
|
|
118
|
+
const labelWidth = Math.min(22, Math.max(8, Math.floor(cols * 0.4)));
|
|
119
|
+
const descRoom = cols - 1 - 2 - 2 - labelWidth - 2;
|
|
120
|
+
const out = [];
|
|
121
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
122
|
+
const item = items[i];
|
|
123
|
+
const selected = i === selectedIndex;
|
|
124
|
+
const label = truncateWidth(item.label, labelWidth);
|
|
125
|
+
const labelCell = padEndWidth(selected ? palette.bold(palette.accent(label)) : palette.text(label), labelWidth);
|
|
126
|
+
const desc = descRoom >= 6 ? truncateWidth(item.desc, descRoom) : '';
|
|
127
|
+
out.push(`${selected ? palette.accent('›') : ' '} ${labelCell}${desc ? ' ' + palette.faint(desc) : ''}`);
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
function drawFrame() {
|
|
132
|
+
const cols = process.stdout.columns || 80;
|
|
133
|
+
const rows = process.stdout.rows || 24;
|
|
134
|
+
const frame = buildPromptFrame({
|
|
135
|
+
cols,
|
|
136
|
+
mode: resolveMode(!!opts.planMode, !!opts.isYolo),
|
|
137
|
+
model: opts.model,
|
|
138
|
+
cwd: opts.cwd,
|
|
139
|
+
branch: opts.branch,
|
|
140
|
+
dirty: opts.dirty,
|
|
141
|
+
contextPercent: opts.contextPercent ?? 0,
|
|
142
|
+
cost: opts.cost ?? 0,
|
|
143
|
+
costState: opts.costState,
|
|
144
|
+
input,
|
|
145
|
+
cursorPos,
|
|
146
|
+
placeholder,
|
|
147
|
+
below: cleanDropdownRows(getDropdownItems(), cols, rows),
|
|
148
|
+
});
|
|
149
|
+
let buf = '\x1b[0m';
|
|
150
|
+
if (frameRowsDrawn > 0 && frameCursorRow > 0)
|
|
151
|
+
buf += `\x1b[${frameCursorRow}A`;
|
|
152
|
+
buf += '\r';
|
|
153
|
+
frame.lines.forEach((line, i) => {
|
|
154
|
+
buf += '\x1b[2K' + line + '\x1b[0m' + (i < frame.lines.length - 1 ? '\n' : '');
|
|
155
|
+
});
|
|
156
|
+
let rowNow = frame.lines.length - 1;
|
|
157
|
+
for (let k = frame.lines.length; k < frameRowsDrawn; k++) {
|
|
158
|
+
buf += '\n\x1b[2K'; // wipe rows left over from a taller previous frame
|
|
159
|
+
rowNow++;
|
|
160
|
+
}
|
|
161
|
+
if (rowNow > frame.cursorRow)
|
|
162
|
+
buf += `\x1b[${rowNow - frame.cursorRow}A`;
|
|
163
|
+
buf += '\r' + (frame.cursorCol > 0 ? `\x1b[${frame.cursorCol}C` : '');
|
|
164
|
+
process.stdout.write(buf);
|
|
165
|
+
frameRowsDrawn = frame.lines.length;
|
|
166
|
+
frameCursorRow = frame.cursorRow;
|
|
167
|
+
}
|
|
168
|
+
function eraseFrame() {
|
|
169
|
+
if (frameRowsDrawn === 0)
|
|
170
|
+
return;
|
|
171
|
+
process.stdout.write(`\x1b[0m${frameCursorRow > 0 ? `\x1b[${frameCursorRow}A` : ''}\r\x1b[J`);
|
|
172
|
+
frameRowsDrawn = 0;
|
|
173
|
+
frameCursorRow = 0;
|
|
174
|
+
}
|
|
175
|
+
const onResize = () => {
|
|
176
|
+
if (disposed)
|
|
177
|
+
return;
|
|
178
|
+
if (cleanLayout)
|
|
179
|
+
eraseFrame();
|
|
180
|
+
render();
|
|
181
|
+
};
|
|
89
182
|
process.stdout.on('resize', onResize);
|
|
90
183
|
function getTagSpans() {
|
|
91
184
|
const spans = [];
|
|
92
|
-
const regex =
|
|
185
|
+
const regex = new RegExp(`\\[Pasted text #\\d+ \\+\\d+ lines\\]|${IMAGE_TAG_SOURCE}`, 'g');
|
|
93
186
|
let match;
|
|
94
187
|
while ((match = regex.exec(input)) !== null) {
|
|
95
188
|
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
@@ -97,7 +190,16 @@ export function askPrompt(opts = {}) {
|
|
|
97
190
|
return spans;
|
|
98
191
|
}
|
|
99
192
|
function formatInputWithBadges(str) {
|
|
100
|
-
return str.replace(
|
|
193
|
+
return str.replace(new RegExp(`(\\[Pasted text #\\d+ \\+\\d+ lines\\]|${IMAGE_TAG_SOURCE})`, 'g'), (match) => (cleanLayout ? pc.dim(match) : theme.badgeFn(` ${match.slice(1, -1)} `)) + '\x1b[0m');
|
|
194
|
+
}
|
|
195
|
+
/** What the echo of the sent message shows: pasted text in full (images stay as their badges). */
|
|
196
|
+
function echoText(text) {
|
|
197
|
+
let shown = text;
|
|
198
|
+
for (const block of pastedBlocks)
|
|
199
|
+
shown = shown.split(block.tag).join(block.text);
|
|
200
|
+
const lines = shown.replace(/\t/g, ' ').split('\n');
|
|
201
|
+
const MAX = 300; // a huge paste must not flood the screen: the model still gets all of it
|
|
202
|
+
return lines.length > MAX ? [...lines.slice(0, MAX), `… +${lines.length - MAX} lines`].join('\n') : shown;
|
|
101
203
|
}
|
|
102
204
|
function expandPastes(text) {
|
|
103
205
|
let expanded = text;
|
|
@@ -105,39 +207,90 @@ export function askPrompt(opts = {}) {
|
|
|
105
207
|
expanded = expanded.split(block.tag).join(block.text);
|
|
106
208
|
}
|
|
107
209
|
for (const img of imageAttachments) {
|
|
108
|
-
|
|
210
|
+
const p = img.filePath.replace(/\\/g, '/');
|
|
211
|
+
expanded = expanded.split(img.tag).join(/^[A-Za-z0-9_\-./\u0400-\u04FF]+$/.test(p) ? `@${p} ` : `@"${p}" `);
|
|
109
212
|
}
|
|
110
213
|
return expanded;
|
|
111
214
|
}
|
|
112
|
-
|
|
215
|
+
let imageCounter = 0;
|
|
216
|
+
let pasting = false;
|
|
217
|
+
/** Puts an image into the input as a badge; the file path goes into the message when it is sent. */
|
|
218
|
+
function attachImage(img) {
|
|
219
|
+
const tag = imageTag(++imageCounter, img.sizeStr);
|
|
220
|
+
imageAttachments.push({ tag, fileName: img.fileName, filePath: img.filePath, sizeStr: img.sizeStr });
|
|
221
|
+
usedImageNames.add(img.fileName);
|
|
222
|
+
input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
|
|
223
|
+
cursorPos += tag.length + 1;
|
|
224
|
+
render();
|
|
225
|
+
}
|
|
226
|
+
/** A one-line message above the prompt (it stays in the scrollback, like other notices). */
|
|
227
|
+
function say(text) {
|
|
228
|
+
if (cleanLayout)
|
|
229
|
+
eraseFrame();
|
|
230
|
+
process.stdout.write(`\x1b[0m\r\x1b[2K${pc.yellow(text)}\n`);
|
|
231
|
+
render();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Ctrl+V, Alt+V and the /image command: the image on the clipboard if there is one,
|
|
235
|
+
* otherwise the text. (A terminal only pastes text by itself; an image needs this.)
|
|
236
|
+
*/
|
|
237
|
+
async function pasteFromClipboard() {
|
|
238
|
+
if (pasting)
|
|
239
|
+
return;
|
|
240
|
+
pasting = true;
|
|
241
|
+
try {
|
|
242
|
+
const img = await saveClipboardImage('image.png', usedImageNames);
|
|
243
|
+
if (disposed)
|
|
244
|
+
return;
|
|
245
|
+
if (img) {
|
|
246
|
+
attachImage(img);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const text = await readClipboardText();
|
|
250
|
+
if (disposed)
|
|
251
|
+
return;
|
|
252
|
+
if (text) {
|
|
253
|
+
handlePaste(text);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const termux = !!process.env.PREFIX?.includes('com.termux');
|
|
257
|
+
say(termux
|
|
258
|
+
? 'Nothing to paste. On Termux an image cannot be read from the clipboard: give the file path instead (@image.png).'
|
|
259
|
+
: 'Nothing to paste: the clipboard has no image or text. (Wayland needs wl-clipboard, X11 needs xclip.)');
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
pasting = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function handlePaste(rawText) {
|
|
266
|
+
// A short single-line chunk often arrives together with its Enter (fast typing, IME):
|
|
267
|
+
// a stray CR / LF must never end up inside the input line.
|
|
268
|
+
const text = rawText.replace(/(?:\r\n|\r|\n)+$/, '');
|
|
113
269
|
const trimmed = text.trim();
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
cursorPos += text.length;
|
|
118
|
-
render();
|
|
270
|
+
// Some terminals send an empty paste when the clipboard holds only an image.
|
|
271
|
+
if (!trimmed) {
|
|
272
|
+
void pasteFromClipboard();
|
|
119
273
|
return;
|
|
120
274
|
}
|
|
121
|
-
|
|
275
|
+
const lines = trimmed.split(/\r\n|\r|\n/);
|
|
276
|
+
// A path to an image (dragged into the terminal or copied from a file manager).
|
|
277
|
+
if (lines.length === 1 && /\.(png|jpe?g|webp|gif)['"]?$/i.test(trimmed)) {
|
|
122
278
|
processPastedFilePath(trimmed, usedImageNames)
|
|
123
279
|
.then((imgRes) => {
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
sizeStr: imgRes.sizeStr
|
|
131
|
-
});
|
|
132
|
-
usedImageNames.add(imgRes.fileName);
|
|
133
|
-
input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
|
|
134
|
-
cursorPos += tag.length + 1;
|
|
135
|
-
render();
|
|
136
|
-
}
|
|
280
|
+
if (disposed)
|
|
281
|
+
return;
|
|
282
|
+
if (imgRes)
|
|
283
|
+
attachImage(imgRes);
|
|
284
|
+
else
|
|
285
|
+
insertText(text);
|
|
137
286
|
})
|
|
138
287
|
.catch(() => { });
|
|
139
288
|
return;
|
|
140
289
|
}
|
|
290
|
+
if (lines.length <= 1 && text.length < 80) {
|
|
291
|
+
insertText(text);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
141
294
|
const id = nextPasteId++;
|
|
142
295
|
const tag = `[Pasted text #${id} +${lines.length} lines]`;
|
|
143
296
|
pastedBlocks.push({ id, lines: lines.length, text, tag });
|
|
@@ -145,10 +298,15 @@ export function askPrompt(opts = {}) {
|
|
|
145
298
|
cursorPos += tag.length;
|
|
146
299
|
render();
|
|
147
300
|
}
|
|
301
|
+
function insertText(text) {
|
|
302
|
+
input = input.slice(0, cursorPos) + text + input.slice(cursorPos);
|
|
303
|
+
cursorPos += text.length;
|
|
304
|
+
render();
|
|
305
|
+
}
|
|
148
306
|
function getDropdownItems() {
|
|
149
307
|
if (input.startsWith('/')) {
|
|
150
308
|
const q = input.trim().toLowerCase();
|
|
151
|
-
if (opts.ui?.commandPalette) {
|
|
309
|
+
if (cleanLayout || opts.ui?.commandPalette) {
|
|
152
310
|
const baseList = [
|
|
153
311
|
...SLASH_COMMANDS.map((c) => ({ cmd: c.cmd, desc: c.desc })),
|
|
154
312
|
...customCommandsList.map((c) => ({ cmd: c.cmd, desc: `(custom) ${c.desc}` }))
|
|
@@ -199,6 +357,10 @@ export function askPrompt(opts = {}) {
|
|
|
199
357
|
function render() {
|
|
200
358
|
if (disposed)
|
|
201
359
|
return;
|
|
360
|
+
if (cleanLayout) {
|
|
361
|
+
drawFrame();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
202
364
|
const cols = Math.max(20, process.stdout.columns || 40);
|
|
203
365
|
const rows = Math.max(8, process.stdout.rows || 20);
|
|
204
366
|
const items = getDropdownItems();
|
|
@@ -211,8 +373,6 @@ export function askPrompt(opts = {}) {
|
|
|
211
373
|
// Limit dropdown height gracefully so it always fits comfortably
|
|
212
374
|
const maxDropdownRows = Math.max(2, Math.min(rows - 3, 5));
|
|
213
375
|
const pageSize = Math.min(4, Math.max(2, Math.min(maxDropdownRows - 2, Math.floor(rows / 4))));
|
|
214
|
-
// Strict boundary: left margin (2) + box borders (2) + inner width <= cols - 4
|
|
215
|
-
// This mathematically prevents terminal auto-wrapping on narrow mobile screens
|
|
216
376
|
const boxWidth = Math.max(12, Math.min(cols - 6, 46));
|
|
217
377
|
const innerWidth = Math.max(8, boxWidth - 2);
|
|
218
378
|
const total = items.length;
|
|
@@ -223,72 +383,86 @@ export function askPrompt(opts = {}) {
|
|
|
223
383
|
}
|
|
224
384
|
}
|
|
225
385
|
const endIndex = Math.min(startIndex + pageSize, total);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
|
|
239
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
240
|
-
const item = items[i];
|
|
241
|
-
const isSelected = i === selectedIndex;
|
|
242
|
-
const pointer = isSelected ? '› ' : ' ';
|
|
243
|
-
const avail = Math.max(4, innerWidth - pointer.length);
|
|
244
|
-
let rowText = '';
|
|
245
|
-
if (avail < 16) {
|
|
246
|
-
const labelStr = item.label.length > avail
|
|
247
|
-
? item.label.slice(0, avail - 1) + '…'
|
|
248
|
-
: item.label.padEnd(avail, ' ');
|
|
249
|
-
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
|
|
386
|
+
if (cleanLayout) {
|
|
387
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
388
|
+
const item = items[i];
|
|
389
|
+
const selected = i === selectedIndex;
|
|
390
|
+
const labelWidth = Math.min(20, cols - 5);
|
|
391
|
+
const label = item.label.length > labelWidth ? `${item.label.slice(0, labelWidth - 1)}…` : item.label;
|
|
392
|
+
const descWidth = cols - label.length - 7;
|
|
393
|
+
const desc = descWidth >= 5
|
|
394
|
+
? item.desc.slice(0, descWidth)
|
|
395
|
+
: '';
|
|
396
|
+
dropdownLines.push(` ${selected ? pc.cyan('›') : ' '} ${selected ? pc.bold(label) : label}${desc ? ` ${pc.dim(desc)}` : ''}`);
|
|
250
397
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
: item.desc.padEnd(descMax, ' ');
|
|
260
|
-
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const hasMoreUp = startIndex > 0;
|
|
401
|
+
const hasMoreDown = endIndex < total;
|
|
402
|
+
let topBorderStr = '─'.repeat(innerWidth);
|
|
403
|
+
if (hasMoreUp) {
|
|
404
|
+
const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
|
|
405
|
+
topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
|
|
261
406
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (isSelected) {
|
|
267
|
-
const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
268
|
-
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
|
|
407
|
+
let botBorderStr = '─'.repeat(innerWidth);
|
|
408
|
+
if (hasMoreDown) {
|
|
409
|
+
const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
|
|
410
|
+
botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
|
|
269
411
|
}
|
|
270
|
-
|
|
271
|
-
|
|
412
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
|
|
413
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
414
|
+
const item = items[i];
|
|
415
|
+
const isSelected = i === selectedIndex;
|
|
416
|
+
const pointer = isSelected ? '› ' : ' ';
|
|
417
|
+
const avail = Math.max(4, innerWidth - pointer.length);
|
|
418
|
+
let rowText = '';
|
|
419
|
+
if (avail < 16) {
|
|
420
|
+
const labelStr = item.label.length > avail
|
|
421
|
+
? item.label.slice(0, avail - 1) + '…'
|
|
422
|
+
: item.label.padEnd(avail, ' ');
|
|
423
|
+
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const labelMax = Math.min(12, Math.max(6, Math.floor(avail * 0.38)));
|
|
427
|
+
const labelStr = item.label.length > labelMax
|
|
428
|
+
? item.label.slice(0, labelMax - 1) + '…'
|
|
429
|
+
: item.label.padEnd(labelMax, ' ');
|
|
430
|
+
const descMax = Math.max(3, avail - labelMax - 1);
|
|
431
|
+
const descStr = item.desc.length > descMax
|
|
432
|
+
? item.desc.slice(0, descMax - 1) + '…'
|
|
433
|
+
: item.desc.padEnd(descMax, ' ');
|
|
434
|
+
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
|
|
435
|
+
}
|
|
436
|
+
const plainLen = rowText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
|
|
437
|
+
const padCount = Math.max(0, innerWidth - plainLen);
|
|
438
|
+
const fullRow = rowText + ' '.repeat(padCount);
|
|
439
|
+
if (isSelected) {
|
|
440
|
+
const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
441
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + fullRow + pc.dim('│') + '\x1b[0m');
|
|
445
|
+
}
|
|
272
446
|
}
|
|
447
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
|
|
273
448
|
}
|
|
274
|
-
dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
|
|
275
449
|
}
|
|
276
450
|
// 1. Format and write the Prompt line safely
|
|
277
|
-
let inputDisplay =
|
|
278
|
-
let renderCursorCol =
|
|
451
|
+
let inputDisplay = promptPrefix;
|
|
452
|
+
let renderCursorCol = promptPrefixWidth;
|
|
279
453
|
if (input.length === 0) {
|
|
280
|
-
const maxPlace = Math.max(10, cols -
|
|
454
|
+
const maxPlace = Math.max(10, cols - promptPrefixWidth - 2);
|
|
281
455
|
const displayPlace = placeholder.length > maxPlace ? placeholder.slice(0, maxPlace - 1) + '…' : placeholder;
|
|
282
456
|
inputDisplay += pc.dim(displayPlace) + '\x1b[0m';
|
|
283
|
-
renderCursorCol =
|
|
457
|
+
renderCursorCol = promptPrefixWidth;
|
|
284
458
|
}
|
|
285
459
|
else {
|
|
286
|
-
const tokenBadge = opts.ui?.inputTokenCounter ? formatPromptTokenBadge(estimatePromptTokens(input), 40) : '';
|
|
460
|
+
const tokenBadge = !cleanLayout && opts.ui?.inputTokenCounter ? formatPromptTokenBadge(estimatePromptTokens(input), 40) : '';
|
|
287
461
|
const badgeSuffix = tokenBadge ? ` ${tokenBadge}` : '';
|
|
288
|
-
const maxInputLen = Math.max(10, cols -
|
|
462
|
+
const maxInputLen = Math.max(10, cols - promptPrefixWidth - 3 - (tokenBadge ? 12 : 0));
|
|
289
463
|
if (input.length <= maxInputLen) {
|
|
290
464
|
inputDisplay += formatInputWithBadges(input) + badgeSuffix + '\x1b[0m';
|
|
291
|
-
renderCursorCol =
|
|
465
|
+
renderCursorCol = promptPrefixWidth + cursorPos;
|
|
292
466
|
}
|
|
293
467
|
else {
|
|
294
468
|
// Horizontal scrolling to prevent auto-wrapping
|
|
@@ -297,7 +471,7 @@ export function askPrompt(opts = {}) {
|
|
|
297
471
|
const prefix = start > 0 ? '…' : '';
|
|
298
472
|
const suffix = start + maxInputLen < input.length ? '…' : '';
|
|
299
473
|
inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + badgeSuffix + '\x1b[0m';
|
|
300
|
-
renderCursorCol =
|
|
474
|
+
renderCursorCol = promptPrefixWidth + (prefix ? 1 : 0) + (cursorPos - start);
|
|
301
475
|
}
|
|
302
476
|
}
|
|
303
477
|
// Write prompt line
|
|
@@ -342,6 +516,20 @@ export function askPrompt(opts = {}) {
|
|
|
342
516
|
process.stdin.pause();
|
|
343
517
|
}
|
|
344
518
|
function clearBoxAndExit(finalInput) {
|
|
519
|
+
if (cleanLayout) {
|
|
520
|
+
eraseFrame();
|
|
521
|
+
const fullTextClean = expandPastes(finalInput);
|
|
522
|
+
if (fullTextClean.trim()) {
|
|
523
|
+
if (GLOBAL_PROMPT_HISTORY.length === 0 ||
|
|
524
|
+
GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullTextClean.trim()) {
|
|
525
|
+
GLOBAL_PROMPT_HISTORY.push(fullTextClean.trim());
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
process.stdout.write(renderUserEcho(echoText(finalInput), process.stdout.columns || 80).join('\n') + '\n');
|
|
529
|
+
cleanup();
|
|
530
|
+
resolve(fullTextClean);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
345
533
|
if (lastRenderedDropdownLines > 0) {
|
|
346
534
|
for (let i = 0; i < lastRenderedDropdownLines; i++) {
|
|
347
535
|
process.stdout.write(`\x1b[1B\r\x1b[0m\x1b[2K`);
|
|
@@ -356,7 +544,7 @@ export function askPrompt(opts = {}) {
|
|
|
356
544
|
GLOBAL_PROMPT_HISTORY.push(fullText.trim());
|
|
357
545
|
}
|
|
358
546
|
}
|
|
359
|
-
process.stdout.write(`\x1b[0m\r\x1b[2K${
|
|
547
|
+
process.stdout.write(`\x1b[0m\r\x1b[2K${promptPrefix}${formatInputWithBadges(finalInput)}\x1b[0m\n\n`);
|
|
360
548
|
cleanup();
|
|
361
549
|
resolve(fullText);
|
|
362
550
|
}
|
|
@@ -393,6 +581,19 @@ export function askPrompt(opts = {}) {
|
|
|
393
581
|
return;
|
|
394
582
|
}
|
|
395
583
|
}
|
|
584
|
+
// Ctrl+P and Ctrl+O: fold or unfold the reasoning / the tool output of the whole chat.
|
|
585
|
+
const viewKey = str === '\x10' || str === '\x1b[112;5u' ? 'thinking' : str === '\x0f' || str === '\x1b[111;5u' ? 'output' : null;
|
|
586
|
+
if (viewKey && opts.onViewToggle && cleanLayout) {
|
|
587
|
+
eraseFrame();
|
|
588
|
+
opts.onViewToggle(viewKey);
|
|
589
|
+
render();
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
// Ctrl+V and Alt+V (also as the sequences of terminals with an enhanced keyboard mode).
|
|
593
|
+
if (str === '\x16' || str === '\x1bv' || str === '\x1bV' || str === '\x1b[118;5u' || str === '\x1b[118;3u') {
|
|
594
|
+
void pasteFromClipboard();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
396
597
|
// Check for raw multi-character paste with newlines or large size
|
|
397
598
|
if (str.length > 1 && (str.includes('\n') || str.includes('\r') || str.length > 80)) {
|
|
398
599
|
handlePaste(str);
|
|
@@ -404,6 +605,12 @@ export function askPrompt(opts = {}) {
|
|
|
404
605
|
}
|
|
405
606
|
// Ctrl+C / SIGINT
|
|
406
607
|
if (str === '\x03') {
|
|
608
|
+
if (cleanLayout) {
|
|
609
|
+
eraseFrame();
|
|
610
|
+
cleanup();
|
|
611
|
+
resolve('__CANCEL__');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
407
614
|
if (lastRenderedDropdownLines > 0) {
|
|
408
615
|
for (let i = 0; i < lastRenderedDropdownLines; i++) {
|
|
409
616
|
process.stdout.write(`\x1b[1B\r\x1b[0m\x1b[2K`);
|
|
@@ -427,29 +634,9 @@ export function askPrompt(opts = {}) {
|
|
|
427
634
|
selectedIndex >= 0 &&
|
|
428
635
|
selectedIndex < items.length &&
|
|
429
636
|
items[selectedIndex].label === '/image')) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
|
|
434
|
-
imageAttachments.push({
|
|
435
|
-
tag,
|
|
436
|
-
fileName: imgRes.fileName,
|
|
437
|
-
filePath: imgRes.filePath,
|
|
438
|
-
sizeStr: imgRes.sizeStr
|
|
439
|
-
});
|
|
440
|
-
usedImageNames.add(imgRes.fileName);
|
|
441
|
-
input = `${tag} `;
|
|
442
|
-
cursorPos = input.length;
|
|
443
|
-
render();
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
process.stdout.write(`\x1b[0m\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
|
|
447
|
-
input = '';
|
|
448
|
-
cursorPos = 0;
|
|
449
|
-
render();
|
|
450
|
-
}
|
|
451
|
-
})
|
|
452
|
-
.catch(() => { });
|
|
637
|
+
input = '';
|
|
638
|
+
cursorPos = 0;
|
|
639
|
+
void pasteFromClipboard();
|
|
453
640
|
return;
|
|
454
641
|
}
|
|
455
642
|
if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
|
|
@@ -494,7 +681,10 @@ export function askPrompt(opts = {}) {
|
|
|
494
681
|
}
|
|
495
682
|
}
|
|
496
683
|
else {
|
|
497
|
-
|
|
684
|
+
if (cleanLayout)
|
|
685
|
+
eraseFrame();
|
|
686
|
+
else
|
|
687
|
+
process.stdout.write(`\x1b[0m\r\x1b[2K`);
|
|
498
688
|
cleanup();
|
|
499
689
|
resolve(`__TOGGLE_MODE__:${input}`);
|
|
500
690
|
}
|
|
@@ -503,7 +693,7 @@ export function askPrompt(opts = {}) {
|
|
|
503
693
|
// Up Arrow
|
|
504
694
|
if (str === '\x1b[A' || str === '\x1bOA') {
|
|
505
695
|
if (items.length > 0) {
|
|
506
|
-
selectedIndex = (selectedIndex - 1
|
|
696
|
+
selectedIndex = Math.max(0, selectedIndex - 1); // stops at the first item
|
|
507
697
|
render();
|
|
508
698
|
}
|
|
509
699
|
else {
|
|
@@ -519,7 +709,7 @@ export function askPrompt(opts = {}) {
|
|
|
519
709
|
// Down Arrow
|
|
520
710
|
if (str === '\x1b[B' || str === '\x1bOB') {
|
|
521
711
|
if (items.length > 0) {
|
|
522
|
-
selectedIndex = (selectedIndex + 1)
|
|
712
|
+
selectedIndex = Math.min(items.length - 1, selectedIndex + 1); // stops at the last item
|
|
523
713
|
render();
|
|
524
714
|
}
|
|
525
715
|
else {
|
|
@@ -596,6 +786,33 @@ export function askPrompt(opts = {}) {
|
|
|
596
786
|
}
|
|
597
787
|
return;
|
|
598
788
|
}
|
|
789
|
+
// Ctrl+Backspace (0x08 in most terminals), Alt+Backspace (ESC DEL) and Ctrl+W: erase a word.
|
|
790
|
+
// A pasted-text badge counts as one word.
|
|
791
|
+
if (str === '\x08' || str === '\x1b\x7f' || str === '\x17') {
|
|
792
|
+
if (cursorPos > 0) {
|
|
793
|
+
const spans = getTagSpans();
|
|
794
|
+
const endingSpan = spans.find((s) => s.end === cursorPos);
|
|
795
|
+
let start = cursorPos;
|
|
796
|
+
if (endingSpan) {
|
|
797
|
+
start = endingSpan.start;
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
const isWordChar = (c) => /[\p{L}\p{N}_]/u.test(c);
|
|
801
|
+
while (start > 0 && !isWordChar(input[start - 1]))
|
|
802
|
+
start--;
|
|
803
|
+
while (start > 0 && isWordChar(input[start - 1]))
|
|
804
|
+
start--;
|
|
805
|
+
const inside = spans.find((s) => start > s.start && start < s.end);
|
|
806
|
+
if (inside)
|
|
807
|
+
start = inside.end;
|
|
808
|
+
}
|
|
809
|
+
input = input.slice(0, start) + input.slice(cursorPos);
|
|
810
|
+
cursorPos = start;
|
|
811
|
+
selectedIndex = 0;
|
|
812
|
+
render();
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
599
816
|
// Backspace / DEL handling
|
|
600
817
|
if (str.includes('\x08') || str.includes('\x7f')) {
|
|
601
818
|
for (let i = 0; i < str.length; i++) {
|