termux-dev 1.4.19 → 1.6.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 +15 -3
- package/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/assets/preview.svg +12 -0
- package/bin/devx.js +0 -0
- package/dist/cli/banner.js +45 -0
- package/dist/cli/diff.js +64 -0
- package/dist/cli/doctor.js +2 -1
- package/dist/cli/headless.js +14 -8
- package/dist/cli/history_search.js +111 -0
- package/dist/cli/index.js +137 -133
- package/dist/cli/modeborder.js +31 -0
- package/dist/cli/prompt.js +138 -92
- package/dist/cli/prompt_palette.js +43 -0
- package/dist/cli/server.js +2 -1
- package/dist/cli/statusline.js +133 -0
- package/dist/cli/telemetry.js +105 -0
- package/dist/core/types.js +1 -1
- package/dist/mcp/client.js +2 -1
- package/dist/permissions/guard.js +9 -4
- package/dist/providers/openai.js +5 -4
- package/dist/tools/fs.js +41 -7
- package/dist/tools/packages.js +35 -11
- package/dist/tools/search.js +24 -5
- package/package.json +8 -3
- package/assets/preview_work1.png +0 -0
- package/dist/cli/index (# Edit conflict 2026-08-29 pe1jzcC #).js +0 -1966
package/dist/cli/prompt.js
CHANGED
|
@@ -3,6 +3,9 @@ import { scanProjectFiles } from './files.js';
|
|
|
3
3
|
import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
|
|
4
4
|
import { getCurrentTheme } from './theme.js';
|
|
5
5
|
import { CustomCommandManager } from '../core/commands.js';
|
|
6
|
+
import { formatPromptHeader } from './modeborder.js';
|
|
7
|
+
import { filterCommandPalette, estimatePromptTokens, formatPromptTokenBadge } from './prompt_palette.js';
|
|
8
|
+
import { PromptHistoryNavigator } from './history_search.js';
|
|
6
9
|
export const SLASH_COMMANDS = [
|
|
7
10
|
{ cmd: '/new', desc: 'Start a new clean chat session' },
|
|
8
11
|
{ cmd: '/resume', desc: 'Resume a previous chat session' },
|
|
@@ -27,7 +30,7 @@ export const SLASH_COMMANDS = [
|
|
|
27
30
|
{ cmd: '/commit', desc: 'AI-generated git commit message & commit' },
|
|
28
31
|
{ cmd: '/status', desc: 'Show git repository file status' },
|
|
29
32
|
{ cmd: '/compact', desc: 'Compact & summarize chat context' },
|
|
30
|
-
{ cmd: '/clear', desc: 'Clear screen
|
|
33
|
+
{ cmd: '/clear', desc: 'Clear screen' },
|
|
31
34
|
{ cmd: '/init', desc: 'Generate AGENTS.md project instructions' },
|
|
32
35
|
{ cmd: '/config', desc: 'View current configuration' },
|
|
33
36
|
{ cmd: '/help', desc: 'Show all available commands' },
|
|
@@ -39,11 +42,22 @@ function stripAnsi(str) {
|
|
|
39
42
|
}
|
|
40
43
|
export function askPrompt(opts = {}) {
|
|
41
44
|
return new Promise((resolve) => {
|
|
42
|
-
const msg = opts.message
|
|
43
|
-
? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
|
|
44
|
-
: `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
|
|
45
45
|
const theme = getCurrentTheme();
|
|
46
|
-
|
|
46
|
+
const cleanLayout = opts.ui?.layout !== 'classic';
|
|
47
|
+
const promptPrefix = cleanLayout ? pc.cyan('›') + ' ' : pc.dim('│') + ' ';
|
|
48
|
+
const promptPrefixWidth = cleanLayout ? 2 : 3;
|
|
49
|
+
if (!cleanLayout) {
|
|
50
|
+
if (opts.ui?.modeBorder) {
|
|
51
|
+
const header = formatPromptHeader(!!opts.planMode, !!opts.isYolo, opts.message);
|
|
52
|
+
console.log(header);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const msg = opts.message
|
|
56
|
+
? `${opts.message} ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`
|
|
57
|
+
: `Ask anything... ${pc.dim(`(Tab = ${opts.planMode ? 'AGENT' : 'PLAN'})`)}`;
|
|
58
|
+
console.log(theme.colorFn('◆') + ' ' + pc.bold(msg));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
47
61
|
let availableFiles = [];
|
|
48
62
|
let customCommandsList = [];
|
|
49
63
|
scanProjectFiles().then((f) => (availableFiles = f)).catch(() => { });
|
|
@@ -54,8 +68,14 @@ export function askPrompt(opts = {}) {
|
|
|
54
68
|
let cursorPos = input.length;
|
|
55
69
|
let selectedIndex = 0;
|
|
56
70
|
let lastRenderedDropdownLines = 0;
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
const historyNav = new PromptHistoryNavigator(GLOBAL_PROMPT_HISTORY, !!opts.ui?.promptHistoryFuzzy);
|
|
72
|
+
PromptHistoryNavigator.loadPersistent().then((persisted) => {
|
|
73
|
+
for (const p of persisted) {
|
|
74
|
+
if (!GLOBAL_PROMPT_HISTORY.includes(p)) {
|
|
75
|
+
GLOBAL_PROMPT_HISTORY.push(p);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}).catch(() => { });
|
|
59
79
|
const imageAttachments = [];
|
|
60
80
|
const usedImageNames = new Set();
|
|
61
81
|
const pastedBlocks = [];
|
|
@@ -82,7 +102,7 @@ export function askPrompt(opts = {}) {
|
|
|
82
102
|
return spans;
|
|
83
103
|
}
|
|
84
104
|
function formatInputWithBadges(str) {
|
|
85
|
-
return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => theme.badgeFn(` ${match.slice(1, -1)} `) + '\x1b[0m');
|
|
105
|
+
return str.replace(/(\[(?:Pasted text #\d+ \+\d+ lines|\d+\.png \d+kb)\])/g, (match) => (cleanLayout ? pc.dim(match) : theme.badgeFn(` ${match.slice(1, -1)} `)) + '\x1b[0m');
|
|
86
106
|
}
|
|
87
107
|
function expandPastes(text) {
|
|
88
108
|
let expanded = text;
|
|
@@ -133,18 +153,34 @@ export function askPrompt(opts = {}) {
|
|
|
133
153
|
function getDropdownItems() {
|
|
134
154
|
if (input.startsWith('/')) {
|
|
135
155
|
const q = input.trim().toLowerCase();
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
if (cleanLayout || opts.ui?.commandPalette) {
|
|
157
|
+
const baseList = [
|
|
158
|
+
...SLASH_COMMANDS.map((c) => ({ cmd: c.cmd, desc: c.desc })),
|
|
159
|
+
...customCommandsList.map((c) => ({ cmd: c.cmd, desc: `(custom) ${c.desc}` }))
|
|
160
|
+
];
|
|
161
|
+
const filtered = filterCommandPalette(input, baseList).slice(0, 15);
|
|
162
|
+
return filtered.map((item) => ({
|
|
163
|
+
label: item.cmd,
|
|
164
|
+
desc: item.desc,
|
|
165
|
+
replacement: `${item.cmd} `,
|
|
166
|
+
replaceStart: 0,
|
|
167
|
+
replaceLen: input.length
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const baseList = [
|
|
172
|
+
...SLASH_COMMANDS.map((c) => ({ label: c.cmd, desc: c.desc, replacement: c.cmd })),
|
|
173
|
+
...customCommandsList.map((c) => ({ label: c.cmd, desc: `(custom) ${c.desc}`, replacement: c.cmd }))
|
|
174
|
+
];
|
|
175
|
+
const filtered = baseList.filter((c) => c.label.toLowerCase().startsWith(q) || q === '/');
|
|
176
|
+
return filtered.map((item) => ({
|
|
177
|
+
label: item.label,
|
|
178
|
+
desc: item.desc,
|
|
179
|
+
replacement: item.replacement,
|
|
180
|
+
replaceStart: 0,
|
|
181
|
+
replaceLen: input.length
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
148
184
|
}
|
|
149
185
|
const beforeCursor = input.slice(0, cursorPos);
|
|
150
186
|
const atMatch = beforeCursor.match(/@([a-zA-Z0-9_\-./\u0400-\u04FF]*)$/);
|
|
@@ -180,8 +216,6 @@ export function askPrompt(opts = {}) {
|
|
|
180
216
|
// Limit dropdown height gracefully so it always fits comfortably
|
|
181
217
|
const maxDropdownRows = Math.max(2, Math.min(rows - 3, 5));
|
|
182
218
|
const pageSize = Math.min(4, Math.max(2, Math.min(maxDropdownRows - 2, Math.floor(rows / 4))));
|
|
183
|
-
// Strict boundary: left margin (2) + box borders (2) + inner width <= cols - 4
|
|
184
|
-
// This mathematically prevents terminal auto-wrapping on narrow mobile screens
|
|
185
219
|
const boxWidth = Math.max(12, Math.min(cols - 6, 46));
|
|
186
220
|
const innerWidth = Math.max(8, boxWidth - 2);
|
|
187
221
|
const total = items.length;
|
|
@@ -192,70 +226,86 @@ export function askPrompt(opts = {}) {
|
|
|
192
226
|
}
|
|
193
227
|
}
|
|
194
228
|
const endIndex = Math.min(startIndex + pageSize, total);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
|
|
208
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
209
|
-
const item = items[i];
|
|
210
|
-
const isSelected = i === selectedIndex;
|
|
211
|
-
const pointer = isSelected ? '› ' : ' ';
|
|
212
|
-
const avail = Math.max(4, innerWidth - pointer.length);
|
|
213
|
-
let rowText = '';
|
|
214
|
-
if (avail < 16) {
|
|
215
|
-
const labelStr = item.label.length > avail
|
|
216
|
-
? item.label.slice(0, avail - 1) + '…'
|
|
217
|
-
: item.label.padEnd(avail, ' ');
|
|
218
|
-
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
|
|
229
|
+
if (cleanLayout) {
|
|
230
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
231
|
+
const item = items[i];
|
|
232
|
+
const selected = i === selectedIndex;
|
|
233
|
+
const labelWidth = Math.min(20, cols - 5);
|
|
234
|
+
const label = item.label.length > labelWidth ? `${item.label.slice(0, labelWidth - 1)}…` : item.label;
|
|
235
|
+
const descWidth = cols - label.length - 7;
|
|
236
|
+
const desc = descWidth >= 5
|
|
237
|
+
? item.desc.slice(0, descWidth)
|
|
238
|
+
: '';
|
|
239
|
+
dropdownLines.push(` ${selected ? pc.cyan('›') : ' '} ${selected ? pc.bold(label) : label}${desc ? ` ${pc.dim(desc)}` : ''}`);
|
|
219
240
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
: item.desc.padEnd(descMax, ' ');
|
|
229
|
-
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const hasMoreUp = startIndex > 0;
|
|
244
|
+
const hasMoreDown = endIndex < total;
|
|
245
|
+
let topBorderStr = '─'.repeat(innerWidth);
|
|
246
|
+
if (hasMoreUp) {
|
|
247
|
+
const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
|
|
248
|
+
topBorderStr = '─'.repeat(mid) + ' ▲ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
|
|
230
249
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (isSelected) {
|
|
236
|
-
const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
237
|
-
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
|
|
250
|
+
let botBorderStr = '─'.repeat(innerWidth);
|
|
251
|
+
if (hasMoreDown) {
|
|
252
|
+
const mid = Math.max(0, Math.floor(innerWidth / 2) - 2);
|
|
253
|
+
botBorderStr = '─'.repeat(mid) + ' ▼ ' + '─'.repeat(Math.max(0, innerWidth - mid - 3));
|
|
238
254
|
}
|
|
239
|
-
|
|
240
|
-
|
|
255
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('╭' + topBorderStr + '╮') + '\x1b[0m');
|
|
256
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
257
|
+
const item = items[i];
|
|
258
|
+
const isSelected = i === selectedIndex;
|
|
259
|
+
const pointer = isSelected ? '› ' : ' ';
|
|
260
|
+
const avail = Math.max(4, innerWidth - pointer.length);
|
|
261
|
+
let rowText = '';
|
|
262
|
+
if (avail < 16) {
|
|
263
|
+
const labelStr = item.label.length > avail
|
|
264
|
+
? item.label.slice(0, avail - 1) + '…'
|
|
265
|
+
: item.label.padEnd(avail, ' ');
|
|
266
|
+
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const labelMax = Math.min(12, Math.max(6, Math.floor(avail * 0.38)));
|
|
270
|
+
const labelStr = item.label.length > labelMax
|
|
271
|
+
? item.label.slice(0, labelMax - 1) + '…'
|
|
272
|
+
: item.label.padEnd(labelMax, ' ');
|
|
273
|
+
const descMax = Math.max(3, avail - labelMax - 1);
|
|
274
|
+
const descStr = item.desc.length > descMax
|
|
275
|
+
? item.desc.slice(0, descMax - 1) + '…'
|
|
276
|
+
: item.desc.padEnd(descMax, ' ');
|
|
277
|
+
rowText = pointer + (isSelected ? theme.boldFn(labelStr) : labelStr) + ' ' + pc.gray(descStr);
|
|
278
|
+
}
|
|
279
|
+
const plainLen = rowText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').length;
|
|
280
|
+
const padCount = Math.max(0, innerWidth - plainLen);
|
|
281
|
+
const fullRow = rowText + ' '.repeat(padCount);
|
|
282
|
+
if (isSelected) {
|
|
283
|
+
const cleanFull = fullRow.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
284
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + theme.badgeFn(cleanFull) + pc.dim('│') + '\x1b[0m');
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('│') + fullRow + pc.dim('│') + '\x1b[0m');
|
|
288
|
+
}
|
|
241
289
|
}
|
|
290
|
+
dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
|
|
242
291
|
}
|
|
243
|
-
dropdownLines.push(pc.dim('│ ') + pc.dim('╰' + botBorderStr + '╯') + '\x1b[0m');
|
|
244
292
|
}
|
|
245
293
|
// 1. Format and write the Prompt line safely
|
|
246
|
-
let inputDisplay =
|
|
247
|
-
let renderCursorCol =
|
|
294
|
+
let inputDisplay = promptPrefix;
|
|
295
|
+
let renderCursorCol = promptPrefixWidth;
|
|
248
296
|
if (input.length === 0) {
|
|
249
|
-
const maxPlace = Math.max(10, cols -
|
|
297
|
+
const maxPlace = Math.max(10, cols - promptPrefixWidth - 2);
|
|
250
298
|
const displayPlace = placeholder.length > maxPlace ? placeholder.slice(0, maxPlace - 1) + '…' : placeholder;
|
|
251
299
|
inputDisplay += pc.dim(displayPlace) + '\x1b[0m';
|
|
252
|
-
renderCursorCol =
|
|
300
|
+
renderCursorCol = promptPrefixWidth;
|
|
253
301
|
}
|
|
254
302
|
else {
|
|
255
|
-
const
|
|
303
|
+
const tokenBadge = !cleanLayout && opts.ui?.inputTokenCounter ? formatPromptTokenBadge(estimatePromptTokens(input), 40) : '';
|
|
304
|
+
const badgeSuffix = tokenBadge ? ` ${tokenBadge}` : '';
|
|
305
|
+
const maxInputLen = Math.max(10, cols - promptPrefixWidth - 3 - (tokenBadge ? 12 : 0));
|
|
256
306
|
if (input.length <= maxInputLen) {
|
|
257
|
-
inputDisplay += formatInputWithBadges(input) + '\x1b[0m';
|
|
258
|
-
renderCursorCol =
|
|
307
|
+
inputDisplay += formatInputWithBadges(input) + badgeSuffix + '\x1b[0m';
|
|
308
|
+
renderCursorCol = promptPrefixWidth + cursorPos;
|
|
259
309
|
}
|
|
260
310
|
else {
|
|
261
311
|
// Horizontal scrolling to prevent auto-wrapping
|
|
@@ -263,8 +313,8 @@ export function askPrompt(opts = {}) {
|
|
|
263
313
|
const visibleChunk = input.slice(start, start + maxInputLen);
|
|
264
314
|
const prefix = start > 0 ? '…' : '';
|
|
265
315
|
const suffix = start + maxInputLen < input.length ? '…' : '';
|
|
266
|
-
inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + '\x1b[0m';
|
|
267
|
-
renderCursorCol =
|
|
316
|
+
inputDisplay += pc.dim(prefix) + formatInputWithBadges(visibleChunk) + pc.dim(suffix) + badgeSuffix + '\x1b[0m';
|
|
317
|
+
renderCursorCol = promptPrefixWidth + (prefix ? 1 : 0) + (cursorPos - start);
|
|
268
318
|
}
|
|
269
319
|
}
|
|
270
320
|
// Write prompt line
|
|
@@ -323,7 +373,7 @@ export function askPrompt(opts = {}) {
|
|
|
323
373
|
GLOBAL_PROMPT_HISTORY.push(fullText.trim());
|
|
324
374
|
}
|
|
325
375
|
}
|
|
326
|
-
process.stdout.write(`\x1b[0m\r\x1b[2K${
|
|
376
|
+
process.stdout.write(`\x1b[0m\r\x1b[2K${promptPrefix}${formatInputWithBadges(finalInput)}\x1b[0m\n\n`);
|
|
327
377
|
cleanup();
|
|
328
378
|
resolve(fullText);
|
|
329
379
|
}
|
|
@@ -473,14 +523,13 @@ export function askPrompt(opts = {}) {
|
|
|
473
523
|
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
|
|
474
524
|
render();
|
|
475
525
|
}
|
|
476
|
-
else
|
|
477
|
-
|
|
478
|
-
|
|
526
|
+
else {
|
|
527
|
+
const prev = historyNav.navigateUp(input);
|
|
528
|
+
if (prev !== null) {
|
|
529
|
+
input = prev;
|
|
530
|
+
cursorPos = input.length;
|
|
531
|
+
render();
|
|
479
532
|
}
|
|
480
|
-
historyIndex--;
|
|
481
|
-
input = GLOBAL_PROMPT_HISTORY[historyIndex];
|
|
482
|
-
cursorPos = input.length;
|
|
483
|
-
render();
|
|
484
533
|
}
|
|
485
534
|
return;
|
|
486
535
|
}
|
|
@@ -490,16 +539,13 @@ export function askPrompt(opts = {}) {
|
|
|
490
539
|
selectedIndex = (selectedIndex + 1) % items.length;
|
|
491
540
|
render();
|
|
492
541
|
}
|
|
493
|
-
else
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
input =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
input = GLOBAL_PROMPT_HISTORY[historyIndex];
|
|
542
|
+
else {
|
|
543
|
+
const next = historyNav.navigateDown();
|
|
544
|
+
if (next !== null) {
|
|
545
|
+
input = next;
|
|
546
|
+
cursorPos = input.length;
|
|
547
|
+
render();
|
|
500
548
|
}
|
|
501
|
-
cursorPos = input.length;
|
|
502
|
-
render();
|
|
503
549
|
}
|
|
504
550
|
return;
|
|
505
551
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export function filterCommandPalette(query, commands) {
|
|
3
|
+
if (!query || query === '/') {
|
|
4
|
+
return [...commands];
|
|
5
|
+
}
|
|
6
|
+
const q = query.toLowerCase().replace(/^\//, '');
|
|
7
|
+
return commands.filter((c) => {
|
|
8
|
+
const name = c.cmd.toLowerCase().replace(/^\//, '');
|
|
9
|
+
return name.includes(q) || c.desc.toLowerCase().includes(q);
|
|
10
|
+
}).sort((a, b) => {
|
|
11
|
+
const rank = (cmd) => {
|
|
12
|
+
const name = cmd.toLowerCase().replace(/^\//, '');
|
|
13
|
+
return name.startsWith(q) ? 0 : name.includes(q) ? 1 : 2;
|
|
14
|
+
};
|
|
15
|
+
return rank(a.cmd) - rank(b.cmd);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function estimatePromptTokens(text) {
|
|
19
|
+
if (!text)
|
|
20
|
+
return 0;
|
|
21
|
+
let tokens = 0;
|
|
22
|
+
for (let i = 0; i < text.length; i++) {
|
|
23
|
+
const code = text.charCodeAt(i);
|
|
24
|
+
if (code >= 0x0400 && code <= 0x04ff) {
|
|
25
|
+
tokens += 0.8;
|
|
26
|
+
}
|
|
27
|
+
else if (code > 0x07ff) {
|
|
28
|
+
tokens += 1.2;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
tokens += 0.25;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Math.max(1, Math.round(tokens));
|
|
35
|
+
}
|
|
36
|
+
export function formatPromptTokenBadge(tokens, threshold = 30) {
|
|
37
|
+
if (tokens < threshold)
|
|
38
|
+
return '';
|
|
39
|
+
if (tokens >= 1000) {
|
|
40
|
+
return pc.dim(`(~${(tokens / 1000).toFixed(1)}k tok)`);
|
|
41
|
+
}
|
|
42
|
+
return pc.dim(`(~${tokens} tok)`);
|
|
43
|
+
}
|
package/dist/cli/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn } from 'child_process';
|
|
|
7
7
|
import pc from 'picocolors';
|
|
8
8
|
import qrcode from 'qrcode-terminal';
|
|
9
9
|
import { getTheme } from './theme.js';
|
|
10
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
10
11
|
const MIME_TYPES = {
|
|
11
12
|
'.html': 'text/html; charset=utf-8',
|
|
12
13
|
'.htm': 'text/html; charset=utf-8',
|
|
@@ -139,7 +140,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
|
|
|
139
140
|
${parentLink}
|
|
140
141
|
${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
|
|
141
142
|
</ul>
|
|
142
|
-
<div class="footer">devx
|
|
143
|
+
<div class="footer">devx v${DEVX_VERSION} • Terminal-Native AI Assistant</div>
|
|
143
144
|
</div>
|
|
144
145
|
</body>
|
|
145
146
|
</html>`;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { getCurrentTheme } from './theme.js';
|
|
4
|
+
export class GitStatusCache {
|
|
5
|
+
static cachedInfo = undefined;
|
|
6
|
+
static lastCheckTime = 0;
|
|
7
|
+
static TTL_MS = 10000; // 10s fallback TTL
|
|
8
|
+
static getStatus(forceRefresh = false) {
|
|
9
|
+
const now = Date.now();
|
|
10
|
+
if (!forceRefresh && GitStatusCache.cachedInfo !== undefined && (now - GitStatusCache.lastCheckTime < GitStatusCache.TTL_MS)) {
|
|
11
|
+
return GitStatusCache.cachedInfo;
|
|
12
|
+
}
|
|
13
|
+
GitStatusCache.lastCheckTime = now;
|
|
14
|
+
try {
|
|
15
|
+
const branch = execSync('git branch --show-current', {
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
encoding: 'utf8',
|
|
18
|
+
timeout: 1000
|
|
19
|
+
}).trim();
|
|
20
|
+
if (!branch) {
|
|
21
|
+
GitStatusCache.cachedInfo = null;
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const status = execSync('git status --porcelain', {
|
|
25
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
timeout: 1000
|
|
28
|
+
}).trim();
|
|
29
|
+
const isDirty = status.length > 0;
|
|
30
|
+
GitStatusCache.cachedInfo = { branch, isDirty };
|
|
31
|
+
return GitStatusCache.cachedInfo;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
GitStatusCache.cachedInfo = null;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
static invalidate() {
|
|
39
|
+
GitStatusCache.cachedInfo = undefined;
|
|
40
|
+
GitStatusCache.lastCheckTime = 0;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function renderProgressBar(percent, barWidth = 10) {
|
|
44
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
45
|
+
const filledCount = Math.round((clamped / 100) * barWidth);
|
|
46
|
+
const emptyCount = Math.max(0, barWidth - filledCount);
|
|
47
|
+
const bar = '█'.repeat(filledCount) + '░'.repeat(emptyCount);
|
|
48
|
+
if (clamped >= 90)
|
|
49
|
+
return pc.red(`[${bar}] ${clamped}%`);
|
|
50
|
+
if (clamped >= 75)
|
|
51
|
+
return pc.yellow(`[${bar}] ${clamped}%`);
|
|
52
|
+
return pc.cyan(`[${bar}] ${clamped}%`);
|
|
53
|
+
}
|
|
54
|
+
export function formatCostBadge(cost, maxCostUSD) {
|
|
55
|
+
const costFormatted = `$${cost.toFixed(4)}`;
|
|
56
|
+
if (!maxCostUSD || maxCostUSD <= 0) {
|
|
57
|
+
return pc.green(costFormatted);
|
|
58
|
+
}
|
|
59
|
+
const ratio = cost / maxCostUSD;
|
|
60
|
+
if (ratio >= 1.0) {
|
|
61
|
+
return pc.red(costFormatted);
|
|
62
|
+
}
|
|
63
|
+
if (ratio >= 0.8) {
|
|
64
|
+
return pc.yellow(costFormatted);
|
|
65
|
+
}
|
|
66
|
+
return pc.green(costFormatted);
|
|
67
|
+
}
|
|
68
|
+
function clipLabel(label, width) {
|
|
69
|
+
if (label.length <= width)
|
|
70
|
+
return label;
|
|
71
|
+
return width > 1 ? `${label.slice(0, width - 1)}…` : label.slice(0, width);
|
|
72
|
+
}
|
|
73
|
+
export function renderCleanStatus(opts) {
|
|
74
|
+
const cols = Math.max(20, opts.cols || process.stdout.columns || 80);
|
|
75
|
+
const available = cols - 2;
|
|
76
|
+
const mode = opts.isYolo ? 'YOLO' : opts.mode;
|
|
77
|
+
const modelWidth = Math.max(4, available - mode.length - 2);
|
|
78
|
+
const shortModel = opts.model.length > modelWidth && opts.model.includes('/')
|
|
79
|
+
? opts.model.split('/').pop() || opts.model
|
|
80
|
+
: opts.model;
|
|
81
|
+
const model = clipLabel(shortModel, modelWidth);
|
|
82
|
+
const percent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
|
|
83
|
+
const contextLabel = cols < 38 ? 'ctx' : 'context';
|
|
84
|
+
let details = `${contextLabel} ${percent}% · $${opts.cost.toFixed(4)}`;
|
|
85
|
+
if (details.length > available)
|
|
86
|
+
details = `${percent}% · $${opts.cost.toFixed(2)}`;
|
|
87
|
+
const git = GitStatusCache.getStatus();
|
|
88
|
+
const branch = git ? ` · ${git.branch}${git.isDirty ? '*' : ''}` : '';
|
|
89
|
+
if (details.length + branch.length <= available)
|
|
90
|
+
details += branch;
|
|
91
|
+
details = clipLabel(details, available);
|
|
92
|
+
return `${pc.cyan(mode.toLowerCase())} ${pc.bold(model)}\n${pc.dim(details)}`;
|
|
93
|
+
}
|
|
94
|
+
export function renderPowerlineStatus(opts) {
|
|
95
|
+
const cols = opts.cols || process.stdout.columns || 80;
|
|
96
|
+
const theme = getCurrentTheme();
|
|
97
|
+
// Mode badge
|
|
98
|
+
let modeBadge = '';
|
|
99
|
+
if (opts.isYolo) {
|
|
100
|
+
modeBadge = pc.bgRed(pc.bold(pc.white(' YOLO ')));
|
|
101
|
+
}
|
|
102
|
+
else if (opts.mode === 'PLAN') {
|
|
103
|
+
modeBadge = pc.bgCyan(pc.bold(pc.black(' PLAN ')));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
modeBadge = pc.bgGreen(pc.bold(pc.black(' AGENT ')));
|
|
107
|
+
}
|
|
108
|
+
// Model shortening for narrow screens
|
|
109
|
+
let displayModel = opts.model;
|
|
110
|
+
if (cols < 75 && displayModel.length > 18) {
|
|
111
|
+
const parts = displayModel.split('/');
|
|
112
|
+
displayModel = parts.length > 1 ? parts.slice(1).join('/') : displayModel;
|
|
113
|
+
if (displayModel.length > 18) {
|
|
114
|
+
displayModel = displayModel.slice(0, 15) + '…';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Git info
|
|
118
|
+
const git = GitStatusCache.getStatus();
|
|
119
|
+
const gitStr = git ? ` · ${pc.magenta(git.branch)}${git.isDirty ? pc.yellow('✱') : ''}` : '';
|
|
120
|
+
// Context progress
|
|
121
|
+
const usagePercent = Math.min(100, Math.round((opts.currentTokens / Math.max(1, opts.maxTokens)) * 100));
|
|
122
|
+
const barWidth = cols < 60 ? 6 : 10;
|
|
123
|
+
const progressStr = renderProgressBar(usagePercent, barWidth);
|
|
124
|
+
const costStr = formatCostBadge(opts.cost, opts.maxCostUSD);
|
|
125
|
+
if (cols < 65) {
|
|
126
|
+
// 2-line layout for mobile Termux
|
|
127
|
+
const line1 = `${modeBadge} ${pc.bold(displayModel)}${gitStr}`;
|
|
128
|
+
const line2 = `${pc.dim('│')} Context ${progressStr} · ${costStr}`;
|
|
129
|
+
return `${line1}\n${line2}`;
|
|
130
|
+
}
|
|
131
|
+
// 1-line powerline layout for desktop
|
|
132
|
+
return `${modeBadge} ${pc.bold(displayModel)}${gitStr} · Context ${progressStr} · ${costStr}`;
|
|
133
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
export class LiveTelemetryTracker {
|
|
3
|
+
startTime = 0;
|
|
4
|
+
phase = 'connecting';
|
|
5
|
+
totalTokens = 0;
|
|
6
|
+
samples = [];
|
|
7
|
+
windowMs = 2000;
|
|
8
|
+
constructor(windowMs = 2000) {
|
|
9
|
+
this.windowMs = windowMs;
|
|
10
|
+
}
|
|
11
|
+
start(phase = 'connecting') {
|
|
12
|
+
this.startTime = Date.now();
|
|
13
|
+
this.phase = phase;
|
|
14
|
+
this.totalTokens = 0;
|
|
15
|
+
this.samples = [];
|
|
16
|
+
}
|
|
17
|
+
setPhase(phase) {
|
|
18
|
+
this.phase = phase;
|
|
19
|
+
}
|
|
20
|
+
recordTokens(count) {
|
|
21
|
+
if (count <= 0)
|
|
22
|
+
return;
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
this.totalTokens += count;
|
|
25
|
+
this.samples.push({ timestamp: now, tokens: count });
|
|
26
|
+
this.pruneOldSamples(now);
|
|
27
|
+
}
|
|
28
|
+
estimateTokensFromText(text) {
|
|
29
|
+
if (!text)
|
|
30
|
+
return 0;
|
|
31
|
+
// Fast token estimation: ~0.8 tokens per Cyrillic char, ~0.25 per ASCII
|
|
32
|
+
let tokens = 0;
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
const code = text.charCodeAt(i);
|
|
35
|
+
if (code >= 0x0400 && code <= 0x04ff) {
|
|
36
|
+
tokens += 0.8;
|
|
37
|
+
}
|
|
38
|
+
else if (code > 0x07ff) {
|
|
39
|
+
tokens += 1.2;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
tokens += 0.25;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return Math.max(1, Math.round(tokens));
|
|
46
|
+
}
|
|
47
|
+
pruneOldSamples(now) {
|
|
48
|
+
const threshold = now - this.windowMs;
|
|
49
|
+
while (this.samples.length > 0 && this.samples[0].timestamp < threshold) {
|
|
50
|
+
this.samples.shift();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
getTokensPerSecond() {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
this.pruneOldSamples(now);
|
|
56
|
+
if (this.samples.length === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
const windowTokens = this.samples.reduce((acc, s) => acc + s.tokens, 0);
|
|
59
|
+
const earliest = this.samples[0].timestamp;
|
|
60
|
+
const durationSec = Math.max(0.2, (now - earliest) / 1000);
|
|
61
|
+
return Math.round(windowTokens / durationSec);
|
|
62
|
+
}
|
|
63
|
+
getElapsedSeconds() {
|
|
64
|
+
if (this.startTime === 0)
|
|
65
|
+
return 0;
|
|
66
|
+
return Math.max(0, Math.round((Date.now() - this.startTime) / 1000));
|
|
67
|
+
}
|
|
68
|
+
getTotalTokens() {
|
|
69
|
+
return this.totalTokens;
|
|
70
|
+
}
|
|
71
|
+
static formatTokenCount(n) {
|
|
72
|
+
if (n >= 1_000_000)
|
|
73
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
74
|
+
if (n >= 1_000)
|
|
75
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
76
|
+
return `${Math.round(n)}`;
|
|
77
|
+
}
|
|
78
|
+
formatLabel() {
|
|
79
|
+
const secs = `${this.getElapsedSeconds()}s`;
|
|
80
|
+
const tokens = LiveTelemetryTracker.formatTokenCount(this.totalTokens);
|
|
81
|
+
const tps = this.getTokensPerSecond();
|
|
82
|
+
if (this.phase === 'connecting') {
|
|
83
|
+
return `connecting… ${pc.dim(secs)}`;
|
|
84
|
+
}
|
|
85
|
+
if (this.phase === 'thinking') {
|
|
86
|
+
if (this.totalTokens > 0) {
|
|
87
|
+
return `thinking… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
|
|
88
|
+
}
|
|
89
|
+
return `thinking… ${pc.dim(secs)}`;
|
|
90
|
+
}
|
|
91
|
+
if (this.phase === 'generating') {
|
|
92
|
+
if (this.totalTokens > 0) {
|
|
93
|
+
return `generating… ${pc.dim(`${secs} · ${tokens} tok · ${tps} tok/s`)}`;
|
|
94
|
+
}
|
|
95
|
+
return `generating… ${pc.dim(secs)}`;
|
|
96
|
+
}
|
|
97
|
+
return `executing… ${pc.dim(secs)}`;
|
|
98
|
+
}
|
|
99
|
+
reset() {
|
|
100
|
+
this.startTime = 0;
|
|
101
|
+
this.phase = 'connecting';
|
|
102
|
+
this.totalTokens = 0;
|
|
103
|
+
this.samples = [];
|
|
104
|
+
}
|
|
105
|
+
}
|
package/dist/core/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const DEVX_VERSION = '1.6.0';
|
package/dist/mcp/client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
2
3
|
export class MCPClient {
|
|
3
4
|
name;
|
|
4
5
|
config;
|
|
@@ -63,7 +64,7 @@ export class MCPClient {
|
|
|
63
64
|
capabilities: {},
|
|
64
65
|
clientInfo: {
|
|
65
66
|
name: 'devx',
|
|
66
|
-
version:
|
|
67
|
+
version: DEVX_VERSION
|
|
67
68
|
}
|
|
68
69
|
});
|
|
69
70
|
if (!initResult) {
|