termux-dev 1.0.2
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/LICENSE +21 -0
- package/README.md +290 -0
- package/assets/banner.svg +33 -0
- package/assets/preview.png +0 -0
- package/bin/devx.js +2 -0
- package/dist/cli/clipboard.js +136 -0
- package/dist/cli/files.js +93 -0
- package/dist/cli/index.js +1506 -0
- package/dist/cli/markdown.js +147 -0
- package/dist/cli/prompt.js +553 -0
- package/dist/cli/providers.js +892 -0
- package/dist/cli/server.js +137 -0
- package/dist/cli/updater.js +245 -0
- package/dist/core/history.js +121 -0
- package/dist/core/loop.js +164 -0
- package/dist/core/memory.js +68 -0
- package/dist/core/models.js +72 -0
- package/dist/core/pricing.js +65 -0
- package/dist/core/session.js +129 -0
- package/dist/core/snapshot.js +88 -0
- package/dist/core/types.js +1 -0
- package/dist/permissions/guard.js +104 -0
- package/dist/prompts/builder.js +69 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/openai.js +318 -0
- package/dist/tools/bash.js +51 -0
- package/dist/tools/diagnostics.js +63 -0
- package/dist/tools/fs.js +185 -0
- package/dist/tools/index.js +17 -0
- package/dist/tools/packages.js +80 -0
- package/dist/tools/plan.js +52 -0
- package/dist/tools/questions.js +101 -0
- package/dist/tools/search.js +90 -0
- package/dist/tools/web.js +155 -0
- package/package.json +64 -0
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { scanProjectFiles } from './files.js';
|
|
3
|
+
import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
|
|
4
|
+
export const SLASH_COMMANDS = [
|
|
5
|
+
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
6
|
+
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
7
|
+
{ cmd: '/session', desc: 'Show active session ID, stats, and info' },
|
|
8
|
+
{ cmd: '/session del', desc: 'Select and delete saved sessions' },
|
|
9
|
+
{ cmd: '/settings', desc: 'Configure permissions & auto-approval' },
|
|
10
|
+
{ cmd: '/update', desc: 'Check and install updates from GitHub' },
|
|
11
|
+
{ cmd: '/model', desc: 'Switch model for current provider' },
|
|
12
|
+
{ cmd: '/provider', desc: 'Switch AI provider (OpenRouter, Google, etc.)' },
|
|
13
|
+
{ cmd: '/plan', desc: 'Switch to PLAN mode (architect & planner)' },
|
|
14
|
+
{ cmd: '/agent', desc: 'Switch to AGENT mode (coder & executor)' },
|
|
15
|
+
{ cmd: '/image', desc: 'Paste image from clipboard as [1.png 203kb]' },
|
|
16
|
+
{ cmd: '/serve', desc: 'Start local web server for web/game preview' },
|
|
17
|
+
{ cmd: '/memory', desc: 'View, add, or clear project memory bank' },
|
|
18
|
+
{ cmd: '/undo', desc: 'Revert last file changes made by AI' },
|
|
19
|
+
{ cmd: '/diff', desc: 'Show git diff of modified project files' },
|
|
20
|
+
{ cmd: '/commit', desc: 'AI-generated git commit message & commit' },
|
|
21
|
+
{ cmd: '/status', desc: 'Show git repository file status' },
|
|
22
|
+
{ cmd: '/compact', desc: 'Compact & summarize chat context' },
|
|
23
|
+
{ cmd: '/clear', desc: 'Clear screen & redraw banner' },
|
|
24
|
+
{ cmd: '/init', desc: 'Generate AGENTS.md project instructions' },
|
|
25
|
+
{ cmd: '/config', desc: 'View current configuration' },
|
|
26
|
+
{ cmd: '/help', desc: 'Show all available commands' },
|
|
27
|
+
{ cmd: '/exit', desc: 'Exit devx' }
|
|
28
|
+
];
|
|
29
|
+
const GLOBAL_PROMPT_HISTORY = [];
|
|
30
|
+
export function askPrompt(opts = {}) {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
process.stdin.resume();
|
|
33
|
+
const msg = opts.message ? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}` : `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
|
|
34
|
+
const placeholder = opts.placeholder || 'Describe a task, @file, /help, paste image, or press Tab to switch mode';
|
|
35
|
+
let input = opts.initialValue || '';
|
|
36
|
+
let cursorPos = input.length;
|
|
37
|
+
let selectedIndex = 0;
|
|
38
|
+
let lastDropdownLines = 0;
|
|
39
|
+
let disposed = false;
|
|
40
|
+
let historyIndex = GLOBAL_PROMPT_HISTORY.length;
|
|
41
|
+
let tempDraft = '';
|
|
42
|
+
let availableFiles = [];
|
|
43
|
+
// Preload project files for fast @ autocomplete
|
|
44
|
+
scanProjectFiles().then(files => {
|
|
45
|
+
availableFiles = files;
|
|
46
|
+
}).catch(() => { });
|
|
47
|
+
const pastes = [];
|
|
48
|
+
const imageAttachments = [];
|
|
49
|
+
const usedImageNames = new Set();
|
|
50
|
+
// Header printed once
|
|
51
|
+
console.log(pc.cyan('◆') + ' ' + pc.bold(msg));
|
|
52
|
+
if (process.stdin.isTTY) {
|
|
53
|
+
process.stdin.setRawMode(true);
|
|
54
|
+
}
|
|
55
|
+
if (process.stdout.isTTY) {
|
|
56
|
+
process.stdout.write('\x1b[?2004h');
|
|
57
|
+
}
|
|
58
|
+
function getDropdownItems() {
|
|
59
|
+
if (input.startsWith('/')) {
|
|
60
|
+
const q = input.trim().toLowerCase();
|
|
61
|
+
const filtered = SLASH_COMMANDS.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
|
|
62
|
+
return filtered.map(c => ({
|
|
63
|
+
label: c.cmd,
|
|
64
|
+
desc: c.desc,
|
|
65
|
+
replacement: c.cmd,
|
|
66
|
+
replaceStart: 0,
|
|
67
|
+
replaceLen: input.length
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
// Check for @ mention at cursor position
|
|
71
|
+
const beforeCursor = input.slice(0, cursorPos);
|
|
72
|
+
const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./]*)$/);
|
|
73
|
+
if (atMatch && availableFiles.length > 0) {
|
|
74
|
+
const query = atMatch[1].toLowerCase();
|
|
75
|
+
const matched = availableFiles
|
|
76
|
+
.filter(f => f.toLowerCase().includes(query) || query === '')
|
|
77
|
+
.slice(0, 8);
|
|
78
|
+
return matched.map(f => ({
|
|
79
|
+
label: `@${f}`,
|
|
80
|
+
desc: f.endsWith('/') ? 'directory' : 'file',
|
|
81
|
+
replacement: `@${f}${f.endsWith('/') ? '' : ' '}`,
|
|
82
|
+
replaceStart: cursorPos - atMatch[0].length,
|
|
83
|
+
replaceLen: atMatch[0].length
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
function formatInputWithBadges(rawText) {
|
|
89
|
+
let formatted = rawText;
|
|
90
|
+
for (const p of pastes) {
|
|
91
|
+
const styled = pc.bold(pc.cyan(`[Pasted text #${p.id} +${p.linesCount} lines]`));
|
|
92
|
+
formatted = formatted.split(p.tag).join(styled);
|
|
93
|
+
}
|
|
94
|
+
for (const img of imageAttachments) {
|
|
95
|
+
const styled = pc.bold(pc.magenta(img.tag));
|
|
96
|
+
formatted = formatted.split(img.tag).join(styled);
|
|
97
|
+
}
|
|
98
|
+
return formatted;
|
|
99
|
+
}
|
|
100
|
+
function expandPastes(rawText) {
|
|
101
|
+
let result = rawText;
|
|
102
|
+
for (const p of pastes) {
|
|
103
|
+
result = result.split(p.tag).join(p.content);
|
|
104
|
+
}
|
|
105
|
+
for (const img of imageAttachments) {
|
|
106
|
+
result = result.split(img.tag).join(`@${img.filePath.replace(/\\/g, '/')} `);
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
function render() {
|
|
111
|
+
if (disposed)
|
|
112
|
+
return;
|
|
113
|
+
const items = getDropdownItems();
|
|
114
|
+
const dropdownLines = [];
|
|
115
|
+
if (items.length > 0) {
|
|
116
|
+
if (selectedIndex >= items.length)
|
|
117
|
+
selectedIndex = 0;
|
|
118
|
+
if (selectedIndex < 0)
|
|
119
|
+
selectedIndex = items.length - 1;
|
|
120
|
+
const boxWidth = Math.min((process.stdout.columns || 80) - 6, 60);
|
|
121
|
+
const pageSize = Math.min(5, Math.max(3, Math.floor(((process.stdout.rows || 24) - 4) / 2)));
|
|
122
|
+
const total = items.length;
|
|
123
|
+
let startIndex = 0;
|
|
124
|
+
if (total > pageSize) {
|
|
125
|
+
if (selectedIndex >= pageSize) {
|
|
126
|
+
startIndex = Math.min(selectedIndex - pageSize + 1, total - pageSize);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const endIndex = Math.min(startIndex + pageSize, total);
|
|
130
|
+
const hasMoreUp = startIndex > 0;
|
|
131
|
+
const hasMoreDown = endIndex < total;
|
|
132
|
+
let topBorderStr = '─'.repeat(boxWidth);
|
|
133
|
+
if (hasMoreUp) {
|
|
134
|
+
const mid = Math.max(0, Math.floor(boxWidth / 2) - 2);
|
|
135
|
+
topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, boxWidth - mid - 3));
|
|
136
|
+
}
|
|
137
|
+
let botBorderStr = '─'.repeat(boxWidth);
|
|
138
|
+
if (hasMoreDown) {
|
|
139
|
+
const mid = Math.max(0, Math.floor(boxWidth / 2) - 2);
|
|
140
|
+
botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, boxWidth - mid - 3));
|
|
141
|
+
}
|
|
142
|
+
dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╭' + topBorderStr + '╮'));
|
|
143
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
144
|
+
const item = items[i];
|
|
145
|
+
const isSelected = i === selectedIndex;
|
|
146
|
+
const labelStr = item.label.length > 20 ? item.label.slice(0, 19) + '…' : item.label.padEnd(20);
|
|
147
|
+
const maxDescLen = Math.max(6, boxWidth - 25);
|
|
148
|
+
const descStr = item.desc.length > maxDescLen ? item.desc.slice(0, maxDescLen - 3) + '...' : item.desc.padEnd(maxDescLen);
|
|
149
|
+
let row = ` ${isSelected ? pc.cyan('›') : ' '} ${isSelected ? pc.bold(pc.cyan(labelStr)) : pc.white(labelStr)} ${pc.gray(descStr)} `;
|
|
150
|
+
if (isSelected) {
|
|
151
|
+
row = pc.bgCyan(pc.black(` › ${labelStr} ${descStr} `));
|
|
152
|
+
}
|
|
153
|
+
dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
|
|
154
|
+
}
|
|
155
|
+
dropdownLines.push(pc.dim('│') + ' ' + pc.dim('╰' + botBorderStr + '╯'));
|
|
156
|
+
}
|
|
157
|
+
// 1. Draw/update input line (line 0)
|
|
158
|
+
let inputDisplay = pc.dim('│') + ' ';
|
|
159
|
+
if (input.length === 0) {
|
|
160
|
+
inputDisplay += pc.dim(placeholder);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
inputDisplay += formatInputWithBadges(input);
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(`\r\x1b[2K${inputDisplay}`);
|
|
166
|
+
// 2. Draw dropdown lines below, and clear any leftover old lines
|
|
167
|
+
const totalLinesToProcess = Math.max(dropdownLines.length, lastDropdownLines);
|
|
168
|
+
if (totalLinesToProcess > 0) {
|
|
169
|
+
for (let i = 0; i < totalLinesToProcess; i++) {
|
|
170
|
+
if (i < dropdownLines.length) {
|
|
171
|
+
process.stdout.write(`\n\x1b[2K${dropdownLines[i]}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
process.stdout.write(`\n\x1b[2K`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 3. Move cursor back UP to prompt line (line 0) at cursorPos column
|
|
178
|
+
process.stdout.write(`\x1b[${totalLinesToProcess}A\r\x1b[${3 + cursorPos}C`);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
process.stdout.write(`\r\x1b[${3 + cursorPos}C`);
|
|
182
|
+
}
|
|
183
|
+
lastDropdownLines = dropdownLines.length;
|
|
184
|
+
}
|
|
185
|
+
render();
|
|
186
|
+
function cleanup() {
|
|
187
|
+
disposed = true;
|
|
188
|
+
if (process.stdout.isTTY) {
|
|
189
|
+
process.stdout.write('\x1b[?2004l');
|
|
190
|
+
}
|
|
191
|
+
process.stdin.removeListener('data', onData);
|
|
192
|
+
if (process.stdin.isTTY) {
|
|
193
|
+
process.stdin.setRawMode(false);
|
|
194
|
+
}
|
|
195
|
+
process.stdin.pause();
|
|
196
|
+
}
|
|
197
|
+
function clearBoxAndExit(finalInput) {
|
|
198
|
+
if (lastDropdownLines > 0) {
|
|
199
|
+
for (let i = 0; i < lastDropdownLines; i++) {
|
|
200
|
+
process.stdout.write(`\n\x1b[2K`);
|
|
201
|
+
}
|
|
202
|
+
process.stdout.write(`\x1b[${lastDropdownLines}A\r`);
|
|
203
|
+
lastDropdownLines = 0;
|
|
204
|
+
}
|
|
205
|
+
const fullText = expandPastes(finalInput);
|
|
206
|
+
if (fullText.trim()) {
|
|
207
|
+
if (GLOBAL_PROMPT_HISTORY.length === 0 || GLOBAL_PROMPT_HISTORY[GLOBAL_PROMPT_HISTORY.length - 1] !== fullText.trim()) {
|
|
208
|
+
GLOBAL_PROMPT_HISTORY.push(fullText.trim());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const lines = fullText.split('\n');
|
|
212
|
+
process.stdout.write(`\r\x1b[2K${pc.dim('│')} ${formatInputWithBadges(lines[0])}\n`);
|
|
213
|
+
for (let i = 1; i < lines.length; i++) {
|
|
214
|
+
process.stdout.write(pc.dim('│') + ' ' + lines[i] + '\n');
|
|
215
|
+
}
|
|
216
|
+
process.stdout.write('\n');
|
|
217
|
+
cleanup();
|
|
218
|
+
resolve(fullText);
|
|
219
|
+
}
|
|
220
|
+
async function handlePaste(pastedContent) {
|
|
221
|
+
const cleaned = pastedContent.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '');
|
|
222
|
+
// Check if pastedContent contains file path(s) to images
|
|
223
|
+
const pathCandidates = cleaned.split(/[&;\n\r]+/).map(s => s.trim()).filter(Boolean);
|
|
224
|
+
let foundAnyImage = false;
|
|
225
|
+
for (const cand of pathCandidates) {
|
|
226
|
+
const imgRes = await processPastedFilePath(cand, usedImageNames);
|
|
227
|
+
if (imgRes) {
|
|
228
|
+
foundAnyImage = true;
|
|
229
|
+
const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
|
|
230
|
+
imageAttachments.push({
|
|
231
|
+
tag,
|
|
232
|
+
fileName: imgRes.fileName,
|
|
233
|
+
filePath: imgRes.filePath,
|
|
234
|
+
sizeStr: imgRes.sizeStr
|
|
235
|
+
});
|
|
236
|
+
usedImageNames.add(imgRes.fileName);
|
|
237
|
+
input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
|
|
238
|
+
cursorPos += tag.length + 1;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (foundAnyImage) {
|
|
242
|
+
selectedIndex = 0;
|
|
243
|
+
render();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const normalized = cleaned.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
247
|
+
const lines = normalized.split('\n');
|
|
248
|
+
if (lines.length >= 2 || normalized.length > 80) {
|
|
249
|
+
const linesCount = lines.length;
|
|
250
|
+
const id = pastes.length + 1;
|
|
251
|
+
const tag = `[Pasted text #${id} +${linesCount} lines]`;
|
|
252
|
+
pastes.push({ id, tag, content: normalized, linesCount });
|
|
253
|
+
input = input.slice(0, cursorPos) + tag + input.slice(cursorPos);
|
|
254
|
+
cursorPos += tag.length;
|
|
255
|
+
selectedIndex = 0;
|
|
256
|
+
render();
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
input = input.slice(0, cursorPos) + normalized + input.slice(cursorPos);
|
|
260
|
+
cursorPos += normalized.length;
|
|
261
|
+
selectedIndex = 0;
|
|
262
|
+
render();
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function getTagSpans() {
|
|
266
|
+
const spans = [];
|
|
267
|
+
for (const p of pastes) {
|
|
268
|
+
let pos = 0;
|
|
269
|
+
while ((pos = input.indexOf(p.tag, pos)) !== -1) {
|
|
270
|
+
spans.push({
|
|
271
|
+
start: pos,
|
|
272
|
+
end: pos + p.tag.length,
|
|
273
|
+
tag: p.tag
|
|
274
|
+
});
|
|
275
|
+
pos += p.tag.length;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const img of imageAttachments) {
|
|
279
|
+
let pos = 0;
|
|
280
|
+
while ((pos = input.indexOf(img.tag, pos)) !== -1) {
|
|
281
|
+
spans.push({
|
|
282
|
+
start: pos,
|
|
283
|
+
end: pos + img.tag.length,
|
|
284
|
+
tag: img.tag
|
|
285
|
+
});
|
|
286
|
+
pos += img.tag.length;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return spans.sort((a, b) => a.start - b.start);
|
|
290
|
+
}
|
|
291
|
+
let pasteBuffer = '';
|
|
292
|
+
let inBracketedPaste = false;
|
|
293
|
+
function onData(chunk) {
|
|
294
|
+
if (disposed)
|
|
295
|
+
return;
|
|
296
|
+
const str = chunk.toString('utf8');
|
|
297
|
+
// Check for bracketed paste start/end
|
|
298
|
+
if (str.includes('\x1b[200~')) {
|
|
299
|
+
inBracketedPaste = true;
|
|
300
|
+
pasteBuffer = '';
|
|
301
|
+
}
|
|
302
|
+
if (inBracketedPaste) {
|
|
303
|
+
pasteBuffer += str;
|
|
304
|
+
if (pasteBuffer.includes('\x1b[201~')) {
|
|
305
|
+
inBracketedPaste = false;
|
|
306
|
+
handlePaste(pasteBuffer);
|
|
307
|
+
pasteBuffer = '';
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Check for raw multi-character paste with newlines or large size
|
|
312
|
+
if (str.length > 1 && (str.includes('\n') || str.includes('\r') || str.length > 80)) {
|
|
313
|
+
handlePaste(str);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Handle individual keys
|
|
317
|
+
// Ctrl+V (ASCII 22 / 0x16) or Ctrl+P (ASCII 16 / 0x10)
|
|
318
|
+
if (str === '\x16' || str === '\x10' || (str.length === 1 && (str.charCodeAt(0) === 22 || str.charCodeAt(0) === 16))) {
|
|
319
|
+
saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
|
|
320
|
+
if (imgRes) {
|
|
321
|
+
const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
|
|
322
|
+
imageAttachments.push({
|
|
323
|
+
tag,
|
|
324
|
+
fileName: imgRes.fileName,
|
|
325
|
+
filePath: imgRes.filePath,
|
|
326
|
+
sizeStr: imgRes.sizeStr
|
|
327
|
+
});
|
|
328
|
+
usedImageNames.add(imgRes.fileName);
|
|
329
|
+
input = input.slice(0, cursorPos) + tag + ' ' + input.slice(cursorPos);
|
|
330
|
+
cursorPos += tag.length + 1;
|
|
331
|
+
render();
|
|
332
|
+
}
|
|
333
|
+
}).catch(() => { });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
// Ctrl+C
|
|
337
|
+
if (str === '\x03' || (str.length === 1 && str.charCodeAt(0) === 3)) {
|
|
338
|
+
if (lastDropdownLines > 0) {
|
|
339
|
+
for (let i = 0; i < lastDropdownLines; i++) {
|
|
340
|
+
process.stdout.write(`\n\x1b[2K`);
|
|
341
|
+
}
|
|
342
|
+
process.stdout.write(`\x1b[${lastDropdownLines}A\r`);
|
|
343
|
+
lastDropdownLines = 0;
|
|
344
|
+
}
|
|
345
|
+
process.stdout.write(`\r\x1b[2K\n`);
|
|
346
|
+
cleanup();
|
|
347
|
+
resolve('__CANCEL__');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const items = getDropdownItems();
|
|
351
|
+
// Enter / Return
|
|
352
|
+
if (str === '\r' || str === '\n' || (str.length === 1 && (str.charCodeAt(0) === 13 || str.charCodeAt(0) === 10))) {
|
|
353
|
+
// Direct /image command
|
|
354
|
+
if (input.trim() === '/image' || (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length && items[selectedIndex].label === '/image')) {
|
|
355
|
+
saveClipboardImage('image.png', usedImageNames).then((imgRes) => {
|
|
356
|
+
if (imgRes) {
|
|
357
|
+
const tag = `[${imgRes.fileName} ${imgRes.sizeStr}]`;
|
|
358
|
+
imageAttachments.push({
|
|
359
|
+
tag,
|
|
360
|
+
fileName: imgRes.fileName,
|
|
361
|
+
filePath: imgRes.filePath,
|
|
362
|
+
sizeStr: imgRes.sizeStr
|
|
363
|
+
});
|
|
364
|
+
usedImageNames.add(imgRes.fileName);
|
|
365
|
+
input = `${tag} `;
|
|
366
|
+
cursorPos = input.length;
|
|
367
|
+
render();
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
process.stdout.write(`\r\x1b[2K${pc.yellow('⚠️ No image in clipboard. Take a screenshot first (Win+Shift+S)\n')}`);
|
|
371
|
+
input = '';
|
|
372
|
+
cursorPos = 0;
|
|
373
|
+
render();
|
|
374
|
+
}
|
|
375
|
+
}).catch(() => { });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
|
|
379
|
+
const selected = items[selectedIndex];
|
|
380
|
+
if (input.startsWith('/')) {
|
|
381
|
+
input = selected.replacement;
|
|
382
|
+
clearBoxAndExit(input);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// @ mention autocomplete on Enter
|
|
387
|
+
input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
|
|
388
|
+
cursorPos = selected.replaceStart + selected.replacement.length;
|
|
389
|
+
render();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
clearBoxAndExit(input);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// Tab
|
|
397
|
+
if (str === '\t' || (str.length === 1 && str.charCodeAt(0) === 9)) {
|
|
398
|
+
if (items.length > 0 && selectedIndex >= 0 && selectedIndex < items.length) {
|
|
399
|
+
const selected = items[selectedIndex];
|
|
400
|
+
input = input.slice(0, selected.replaceStart) + selected.replacement + input.slice(selected.replaceStart + selected.replaceLen);
|
|
401
|
+
cursorPos = selected.replaceStart + selected.replacement.length;
|
|
402
|
+
render();
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
if (lastDropdownLines > 0) {
|
|
406
|
+
for (let i = 0; i < lastDropdownLines; i++) {
|
|
407
|
+
process.stdout.write(`\n\x1b[2K`);
|
|
408
|
+
}
|
|
409
|
+
process.stdout.write(`\x1b[${lastDropdownLines}A\r`);
|
|
410
|
+
lastDropdownLines = 0;
|
|
411
|
+
}
|
|
412
|
+
process.stdout.write(`\r\x1b[2K`);
|
|
413
|
+
cleanup();
|
|
414
|
+
resolve(`__TOGGLE_MODE__:${input}`);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
// Up Arrow
|
|
419
|
+
if (str === '\x1b[A' || str === '\x1bOA') {
|
|
420
|
+
if (items.length > 0) {
|
|
421
|
+
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
422
|
+
render();
|
|
423
|
+
}
|
|
424
|
+
else if (GLOBAL_PROMPT_HISTORY.length > 0 && historyIndex > 0) {
|
|
425
|
+
if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
|
|
426
|
+
tempDraft = input;
|
|
427
|
+
}
|
|
428
|
+
historyIndex--;
|
|
429
|
+
input = GLOBAL_PROMPT_HISTORY[historyIndex];
|
|
430
|
+
cursorPos = input.length;
|
|
431
|
+
render();
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Down Arrow
|
|
436
|
+
if (str === '\x1b[B' || str === '\x1bOB') {
|
|
437
|
+
if (items.length > 0) {
|
|
438
|
+
selectedIndex = (selectedIndex + 1) % items.length;
|
|
439
|
+
render();
|
|
440
|
+
}
|
|
441
|
+
else if (historyIndex < GLOBAL_PROMPT_HISTORY.length) {
|
|
442
|
+
historyIndex++;
|
|
443
|
+
if (historyIndex === GLOBAL_PROMPT_HISTORY.length) {
|
|
444
|
+
input = tempDraft;
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
input = GLOBAL_PROMPT_HISTORY[historyIndex];
|
|
448
|
+
}
|
|
449
|
+
cursorPos = input.length;
|
|
450
|
+
render();
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// Left Arrow (Jump across whole paste badge atomically)
|
|
455
|
+
if (str === '\x1b[D' || str === '\x1bOD') {
|
|
456
|
+
if (cursorPos > 0) {
|
|
457
|
+
const spans = getTagSpans();
|
|
458
|
+
const endingSpan = spans.find(s => s.end === cursorPos);
|
|
459
|
+
if (endingSpan) {
|
|
460
|
+
cursorPos = endingSpan.start;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
cursorPos--;
|
|
464
|
+
const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
|
|
465
|
+
if (inside)
|
|
466
|
+
cursorPos = inside.start;
|
|
467
|
+
}
|
|
468
|
+
render();
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Right Arrow (Jump across whole paste badge atomically)
|
|
473
|
+
if (str === '\x1b[C' || str === '\x1bOC') {
|
|
474
|
+
if (cursorPos < input.length) {
|
|
475
|
+
const spans = getTagSpans();
|
|
476
|
+
const startingSpan = spans.find(s => s.start === cursorPos);
|
|
477
|
+
if (startingSpan) {
|
|
478
|
+
cursorPos = startingSpan.end;
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
cursorPos++;
|
|
482
|
+
const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
|
|
483
|
+
if (inside)
|
|
484
|
+
cursorPos = inside.end;
|
|
485
|
+
}
|
|
486
|
+
render();
|
|
487
|
+
}
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Home
|
|
491
|
+
if (str === '\x1b[H' || str === '\x1b[1~') {
|
|
492
|
+
cursorPos = 0;
|
|
493
|
+
render();
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// End
|
|
497
|
+
if (str === '\x1b[F' || str === '\x1b[4~') {
|
|
498
|
+
cursorPos = input.length;
|
|
499
|
+
render();
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
// Delete key (\x1b[3~)
|
|
503
|
+
if (str === '\x1b[3~') {
|
|
504
|
+
if (cursorPos < input.length) {
|
|
505
|
+
const spans = getTagSpans();
|
|
506
|
+
const startingSpan = spans.find(s => s.start === cursorPos);
|
|
507
|
+
if (startingSpan) {
|
|
508
|
+
input = input.slice(0, startingSpan.start) + input.slice(startingSpan.end);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
input = input.slice(0, cursorPos) + input.slice(cursorPos + 1);
|
|
512
|
+
}
|
|
513
|
+
selectedIndex = 0;
|
|
514
|
+
render();
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
// Backspace
|
|
519
|
+
if (str === '\x08' || str === '\x7f' || (str.length === 1 && (str.charCodeAt(0) === 8 || str.charCodeAt(0) === 127))) {
|
|
520
|
+
if (cursorPos > 0) {
|
|
521
|
+
const spans = getTagSpans();
|
|
522
|
+
const endingSpan = spans.find(s => s.end === cursorPos);
|
|
523
|
+
if (endingSpan) {
|
|
524
|
+
input = input.slice(0, endingSpan.start) + input.slice(endingSpan.end);
|
|
525
|
+
cursorPos = endingSpan.start;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
input = input.slice(0, cursorPos - 1) + input.slice(cursorPos);
|
|
529
|
+
cursorPos--;
|
|
530
|
+
}
|
|
531
|
+
selectedIndex = 0;
|
|
532
|
+
render();
|
|
533
|
+
}
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// Ignore unknown escape sequences
|
|
537
|
+
if (str.startsWith('\x1b')) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// Normal character input - snap out of span if needed
|
|
541
|
+
const spans = getTagSpans();
|
|
542
|
+
const inside = spans.find(s => cursorPos > s.start && cursorPos < s.end);
|
|
543
|
+
if (inside) {
|
|
544
|
+
cursorPos = inside.end;
|
|
545
|
+
}
|
|
546
|
+
input = input.slice(0, cursorPos) + str + input.slice(cursorPos);
|
|
547
|
+
cursorPos += str.length;
|
|
548
|
+
selectedIndex = 0;
|
|
549
|
+
render();
|
|
550
|
+
}
|
|
551
|
+
process.stdin.on('data', onData);
|
|
552
|
+
});
|
|
553
|
+
}
|