termux-dev 1.1.1 → 1.2.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/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/dist/cli/doctor.js +141 -0
- package/dist/cli/index.js +210 -42
- package/dist/cli/prompt.js +34 -3
- package/dist/cli/server.js +218 -40
- package/dist/cli/theme.js +140 -0
- package/dist/core/loop.js +1 -1
- package/dist/core/notify.js +40 -0
- package/dist/core/snapshot.js +2 -6
- package/dist/prompts/builder.js +2 -1
- package/dist/providers/openai.js +23 -1
- package/dist/tools/fs.js +21 -9
- package/dist/tools/index.js +3 -2
- package/dist/tools/server.js +47 -0
- package/package.json +4 -2
package/assets/banner.svg
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Tagline & Badge -->
|
|
27
27
|
<text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
|
|
28
|
-
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.
|
|
28
|
+
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.2.0</tspan>
|
|
29
29
|
</text>
|
|
30
30
|
|
|
31
31
|
<!-- Top Accent Line -->
|
package/assets/preview.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import { getCurrentTheme } from './theme.js';
|
|
6
|
+
import { isTermux } from '../core/notify.js';
|
|
7
|
+
function checkCmd(cmd) {
|
|
8
|
+
try {
|
|
9
|
+
const out = execSync(cmd, { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
10
|
+
return out.split('\n')[0];
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function pingUrl(url) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const req = https.get(url, { timeout: 3000 }, (res) => {
|
|
19
|
+
resolve(res.statusCode !== undefined);
|
|
20
|
+
});
|
|
21
|
+
req.on('error', () => resolve(false));
|
|
22
|
+
req.on('timeout', () => {
|
|
23
|
+
req.destroy();
|
|
24
|
+
resolve(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function runDoctor(config) {
|
|
29
|
+
const theme = getCurrentTheme();
|
|
30
|
+
console.log('\n' + theme.boldFn('🩺 devx Doctor — System & Environment Health Diagnostics'));
|
|
31
|
+
console.log(pc.dim('──────────────────────────────────────────────────────────────────'));
|
|
32
|
+
const envChecks = [];
|
|
33
|
+
const toolChecks = [];
|
|
34
|
+
const keyChecks = [];
|
|
35
|
+
const netChecks = [];
|
|
36
|
+
// 1. Environment Checks
|
|
37
|
+
const nodeVer = process.version;
|
|
38
|
+
const nodeMajor = parseInt(nodeVer.replace(/^v/, '').split('.')[0], 10);
|
|
39
|
+
envChecks.push({
|
|
40
|
+
name: 'Node.js Runtime',
|
|
41
|
+
status: nodeMajor >= 20 ? 'ok' : 'warn',
|
|
42
|
+
details: `${nodeVer} (Recommended: >= 20.0)`
|
|
43
|
+
});
|
|
44
|
+
const termux = isTermux();
|
|
45
|
+
envChecks.push({
|
|
46
|
+
name: 'Platform / Environment',
|
|
47
|
+
status: 'ok',
|
|
48
|
+
details: termux ? 'Android (Termux ARM64)' : `${os.platform()} ${os.arch()} (${os.release()})`
|
|
49
|
+
});
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
envChecks.push({
|
|
52
|
+
name: 'Working Directory',
|
|
53
|
+
status: 'ok',
|
|
54
|
+
details: cwd
|
|
55
|
+
});
|
|
56
|
+
// 2. Essential Tools
|
|
57
|
+
const gitVer = checkCmd('git --version');
|
|
58
|
+
toolChecks.push({
|
|
59
|
+
name: 'Git VCS',
|
|
60
|
+
status: gitVer ? 'ok' : 'warn',
|
|
61
|
+
details: gitVer || 'Not installed (some git features disabled)'
|
|
62
|
+
});
|
|
63
|
+
const npmVer = checkCmd('npm --version');
|
|
64
|
+
toolChecks.push({
|
|
65
|
+
name: 'NPM Package Manager',
|
|
66
|
+
status: npmVer ? 'ok' : 'warn',
|
|
67
|
+
details: npmVer ? `v${npmVer}` : 'Not installed'
|
|
68
|
+
});
|
|
69
|
+
const pythonVer = checkCmd('python --version') || checkCmd('python3 --version');
|
|
70
|
+
toolChecks.push({
|
|
71
|
+
name: 'Python Runtime',
|
|
72
|
+
status: pythonVer ? 'ok' : 'warn',
|
|
73
|
+
details: pythonVer || 'Optional (not found)'
|
|
74
|
+
});
|
|
75
|
+
const cCompiler = checkCmd('clang --version') || checkCmd('gcc --version');
|
|
76
|
+
toolChecks.push({
|
|
77
|
+
name: 'C/C++ Compiler',
|
|
78
|
+
status: cCompiler ? 'ok' : 'warn',
|
|
79
|
+
details: cCompiler ? cCompiler.split(' ')[0] : 'Optional (not found)'
|
|
80
|
+
});
|
|
81
|
+
// 3. AI Providers & Keys
|
|
82
|
+
const activeProvider = config.provider || 'openrouter';
|
|
83
|
+
const hasActiveKey = !!config.apiKey;
|
|
84
|
+
keyChecks.push({
|
|
85
|
+
name: `Active Provider (${activeProvider})`,
|
|
86
|
+
status: hasActiveKey ? 'ok' : 'fail',
|
|
87
|
+
details: hasActiveKey ? `Configured (Model: ${config.model})` : 'Missing API Key! Set via /provider or ~/.devxrc.json'
|
|
88
|
+
});
|
|
89
|
+
const envOpenRouter = !!process.env.OPENROUTER_API_KEY;
|
|
90
|
+
const envOpenAI = !!process.env.OPENAI_API_KEY;
|
|
91
|
+
const envAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
92
|
+
const envGoogle = !!process.env.GEMINI_API_KEY;
|
|
93
|
+
keyChecks.push({
|
|
94
|
+
name: 'OpenRouter Key',
|
|
95
|
+
status: envOpenRouter || config.provider === 'openrouter' && hasActiveKey ? 'ok' : 'warn',
|
|
96
|
+
details: envOpenRouter || (config.provider === 'openrouter' && hasActiveKey) ? 'Configured' : 'Not set'
|
|
97
|
+
});
|
|
98
|
+
keyChecks.push({
|
|
99
|
+
name: 'OpenAI / Gemini / Anthropic Keys',
|
|
100
|
+
status: envOpenAI || envAnthropic || envGoogle ? 'ok' : 'warn',
|
|
101
|
+
details: [envOpenAI && 'OpenAI', envAnthropic && 'Anthropic', envGoogle && 'Gemini'].filter(Boolean).join(', ') || 'Not set in env (using default provider)'
|
|
102
|
+
});
|
|
103
|
+
// 4. Network Connectivity
|
|
104
|
+
const openRouterOnline = await pingUrl('https://openrouter.ai');
|
|
105
|
+
netChecks.push({
|
|
106
|
+
name: 'OpenRouter API Connectivity',
|
|
107
|
+
status: openRouterOnline ? 'ok' : 'warn',
|
|
108
|
+
details: openRouterOnline ? 'Online (HTTP 200/reachable)' : 'Unreachable (check VPN/DNS/WiFi)'
|
|
109
|
+
});
|
|
110
|
+
const githubOnline = await pingUrl('https://api.github.com');
|
|
111
|
+
netChecks.push({
|
|
112
|
+
name: 'GitHub API (for updater)',
|
|
113
|
+
status: githubOnline ? 'ok' : 'warn',
|
|
114
|
+
details: githubOnline ? 'Online (reachable)' : 'Unreachable'
|
|
115
|
+
});
|
|
116
|
+
// Helper to render section
|
|
117
|
+
const renderSection = (title, items) => {
|
|
118
|
+
console.log('\n' + theme.colorFn(pc.bold(`• ${title}`)));
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
let icon = pc.green('✔');
|
|
121
|
+
if (item.status === 'warn')
|
|
122
|
+
icon = pc.yellow('▲');
|
|
123
|
+
if (item.status === 'fail')
|
|
124
|
+
icon = pc.red('✖');
|
|
125
|
+
console.log(` ${icon} ${pc.bold(item.name)}: ${pc.dim(item.details)}`);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
renderSection('Environment & OS', envChecks);
|
|
129
|
+
renderSection('Developer Tools', toolChecks);
|
|
130
|
+
renderSection('AI Configuration & Keys', keyChecks);
|
|
131
|
+
renderSection('Network Connectivity', netChecks);
|
|
132
|
+
console.log('\n' + pc.dim('──────────────────────────────────────────────────────────────────'));
|
|
133
|
+
const hasFailures = [...envChecks, ...toolChecks, ...keyChecks, ...netChecks].some(i => i.status === 'fail');
|
|
134
|
+
if (hasFailures) {
|
|
135
|
+
console.log(pc.red(pc.bold('⚠ Doctor found critical issues that might prevent devx from functioning properly.')));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(theme.boldFn('🎉 Everything looks healthy! devx is fully configured and ready for vibe-coding.'));
|
|
139
|
+
}
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import os from 'os';
|
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import fs from 'fs/promises';
|
|
8
8
|
import fsSync from 'fs';
|
|
9
|
-
import { execSync } from 'child_process';
|
|
9
|
+
import { execSync, execFileSync } from 'child_process';
|
|
10
10
|
import { History } from '../core/history.js';
|
|
11
11
|
import { Agent } from '../core/loop.js';
|
|
12
12
|
import { buildSystemPrompt } from '../prompts/builder.js';
|
|
@@ -15,7 +15,9 @@ import { getTools, lastPlanReady, resetPlanReady } from '../tools/index.js';
|
|
|
15
15
|
import { CLIConsoleGuard } from '../permissions/guard.js';
|
|
16
16
|
import { globalSnapshotManager } from '../core/snapshot.js';
|
|
17
17
|
import { MemoryManager } from '../core/memory.js';
|
|
18
|
-
import { startServer, stopServer } from './server.js';
|
|
18
|
+
import { startServer, stopServer, displayServerBanner } from './server.js';
|
|
19
|
+
import { notifyDevice } from '../core/notify.js';
|
|
20
|
+
import { runDoctor } from './doctor.js';
|
|
19
21
|
import { ALL_PROVIDERS } from './providers.js';
|
|
20
22
|
import { getModelContextLimit } from '../core/models.js';
|
|
21
23
|
import { SessionManager } from '../core/session.js';
|
|
@@ -24,6 +26,7 @@ import { SmoothStreamer } from './smooth.js';
|
|
|
24
26
|
import { askPrompt } from './prompt.js';
|
|
25
27
|
import { resolveAtMentions } from './files.js';
|
|
26
28
|
import { runStartupUpdateCheck, checkForUpdates, performSelfUpdate } from './updater.js';
|
|
29
|
+
import { setActiveTheme, getCurrentTheme, listThemes, findTheme } from './theme.js';
|
|
27
30
|
const CONFIG_PATH = path.join(os.homedir(), '.devxrc.json');
|
|
28
31
|
function maskApiKey(key) {
|
|
29
32
|
if (!key)
|
|
@@ -371,14 +374,15 @@ function clearTerminalScreen() {
|
|
|
371
374
|
function drawLogo() {
|
|
372
375
|
const cols = process.stdout.columns || 80;
|
|
373
376
|
clearTerminalScreen();
|
|
377
|
+
const theme = getCurrentTheme();
|
|
374
378
|
if (cols < 56) {
|
|
375
379
|
// Ultra-clean compact ASCII for small mobile screens (width: ~26 chars)
|
|
376
380
|
const logo = [
|
|
377
381
|
'',
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
' ' +
|
|
382
|
+
theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
|
|
383
|
+
theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
|
|
384
|
+
theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
|
|
385
|
+
' ' + theme.boldFn('v1.2.0'),
|
|
382
386
|
''
|
|
383
387
|
];
|
|
384
388
|
for (const line of logo) {
|
|
@@ -390,10 +394,10 @@ function drawLogo() {
|
|
|
390
394
|
const indent = cols < 68 ? ' ' : ' ';
|
|
391
395
|
const logo = [
|
|
392
396
|
'',
|
|
393
|
-
indent +
|
|
394
|
-
indent +
|
|
395
|
-
indent +
|
|
396
|
-
indent +
|
|
397
|
+
indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
|
|
398
|
+
indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
|
|
399
|
+
indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
|
|
400
|
+
indent + theme.boldFn('v1.2.0'),
|
|
397
401
|
''
|
|
398
402
|
];
|
|
399
403
|
for (const line of logo) {
|
|
@@ -415,6 +419,33 @@ function formatSessionTime(timestamp) {
|
|
|
415
419
|
const days = Math.floor(hours / 24);
|
|
416
420
|
return `${days}d ago`;
|
|
417
421
|
}
|
|
422
|
+
function formatToolGeneratingLabel(name, targetHint) {
|
|
423
|
+
const t = name.toLowerCase();
|
|
424
|
+
const target = targetHint ? ` ${targetHint}` : '';
|
|
425
|
+
if (t === 'read_file' || t === 'read')
|
|
426
|
+
return `→ Read${target || '...'}`;
|
|
427
|
+
if (t === 'write_file' || t === 'write')
|
|
428
|
+
return `→ Write${target || '...'}`;
|
|
429
|
+
if (t === 'edit_file' || t === 'patch')
|
|
430
|
+
return `→ Edit${target || '...'}`;
|
|
431
|
+
if (t === 'delete_file' || t === 'remove_file' || t === 'rm')
|
|
432
|
+
return `→ Delete${target || '...'}`;
|
|
433
|
+
if (t === 'bash' || t === 'run_command' || t === 'exec')
|
|
434
|
+
return `→ Run:${target || '...'}`;
|
|
435
|
+
if (t === 'search' || t === 'grep_search')
|
|
436
|
+
return `→ Search${target || '...'}`;
|
|
437
|
+
if (t === 'list_dir' || t === 'ls')
|
|
438
|
+
return `→ List${target || '...'}`;
|
|
439
|
+
if (t === 'todo_list' || t === 'update_todos')
|
|
440
|
+
return `→ Update Plan & Tasks...`;
|
|
441
|
+
if (t === 'plan_ready')
|
|
442
|
+
return `→ Finalize Plan...`;
|
|
443
|
+
if (t === 'diagnose_code')
|
|
444
|
+
return `→ Diagnosing code...`;
|
|
445
|
+
if (t === 'ask_questions')
|
|
446
|
+
return `→ Clarifying questions...`;
|
|
447
|
+
return `→ ${name}${target}...`;
|
|
448
|
+
}
|
|
418
449
|
async function handleSessionDelete() {
|
|
419
450
|
while (true) {
|
|
420
451
|
const sessions = await SessionManager.listSessions();
|
|
@@ -493,6 +524,39 @@ export async function main() {
|
|
|
493
524
|
const options = program.opts();
|
|
494
525
|
let planMode = !!options.plan;
|
|
495
526
|
let config = await loadConfig();
|
|
527
|
+
if (!config.onboarded) {
|
|
528
|
+
const cols = Math.min(process.stdout.columns || 40, 42);
|
|
529
|
+
const fill = Math.max(2, cols - 16);
|
|
530
|
+
const topBorder = '┌─ preview.ts ' + '─'.repeat(fill) + '┐';
|
|
531
|
+
const bottomBorder = '└' + '─'.repeat(cols - 2) + '┘';
|
|
532
|
+
const themeChoices = listThemes().map(t => {
|
|
533
|
+
const addLine = t.diffAddBg(` + 1 | const theme = "${t.id}";`.padEnd(cols - 2));
|
|
534
|
+
const remLine = t.diffRemoveBg(` - 2 | const theme = "none";`.padEnd(cols - 2));
|
|
535
|
+
const preview = `${t.colorFn(topBorder)}\n${addLine}\n${remLine}\n${t.colorFn(bottomBorder)}\n\n${pc.dim('💡 You can change this anytime with /theme')}`;
|
|
536
|
+
return {
|
|
537
|
+
name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)}`,
|
|
538
|
+
value: t.id,
|
|
539
|
+
description: preview
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
console.clear();
|
|
544
|
+
const selected = await select({
|
|
545
|
+
message: `${pc.bold('🎨 Pick a theme to personalize your workspace:')}`,
|
|
546
|
+
choices: themeChoices,
|
|
547
|
+
pageSize: 10
|
|
548
|
+
});
|
|
549
|
+
if (selected) {
|
|
550
|
+
config.theme = selected;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
config.onboarded = true;
|
|
555
|
+
await saveConfig(config);
|
|
556
|
+
}
|
|
557
|
+
if (config.theme) {
|
|
558
|
+
setActiveTheme(config.theme);
|
|
559
|
+
}
|
|
496
560
|
enableDarkTheme(config.pureBlackTheme !== false);
|
|
497
561
|
let history = new History();
|
|
498
562
|
const sysPrompt = await buildSystemPrompt(planMode);
|
|
@@ -519,6 +583,7 @@ export async function main() {
|
|
|
519
583
|
const tokenStats = `Context: ${formatTokens(currentTokens)} / ${formatTokens(maxTokens)} (${usagePercent}%) • ${costStr}`;
|
|
520
584
|
const cols = process.stdout.columns || 80;
|
|
521
585
|
const modeName = planMode ? 'PLAN' : 'AGENT';
|
|
586
|
+
const theme = getCurrentTheme();
|
|
522
587
|
// Shorten model name if too long on narrow mobile screens
|
|
523
588
|
let displayModel = config.model;
|
|
524
589
|
if (cols < 75 && displayModel.length > 20) {
|
|
@@ -528,7 +593,7 @@ export async function main() {
|
|
|
528
593
|
displayModel = displayModel.slice(0, 17) + '...';
|
|
529
594
|
}
|
|
530
595
|
}
|
|
531
|
-
const badge =
|
|
596
|
+
const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
|
|
532
597
|
if (cols < 75) {
|
|
533
598
|
// 2-line layout for mobile screens: perfectly aligned with clack box borders
|
|
534
599
|
p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
|
|
@@ -541,7 +606,7 @@ export async function main() {
|
|
|
541
606
|
if (autoTriggerPrompt) {
|
|
542
607
|
answer = autoTriggerPrompt;
|
|
543
608
|
autoTriggerPrompt = '';
|
|
544
|
-
console.log(
|
|
609
|
+
console.log(theme.colorFn('◆') + ' ' + pc.bold(pc.white(answer)));
|
|
545
610
|
}
|
|
546
611
|
else {
|
|
547
612
|
const inputStr = await askPrompt({
|
|
@@ -582,16 +647,44 @@ export async function main() {
|
|
|
582
647
|
if (answer.startsWith('/')) {
|
|
583
648
|
const parts = answer.split(' ');
|
|
584
649
|
let cmd = parts[0];
|
|
650
|
+
async function handleThemeSelect(config) {
|
|
651
|
+
const currentTh = getCurrentTheme();
|
|
652
|
+
const themeChoices = listThemes().map(t => ({
|
|
653
|
+
name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
|
|
654
|
+
value: t.id,
|
|
655
|
+
description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
|
|
656
|
+
}));
|
|
657
|
+
try {
|
|
658
|
+
const selected = await select({
|
|
659
|
+
message: `${pc.bold('🎨 Select UI Theme / Выберите цветовую тему:')}`,
|
|
660
|
+
choices: themeChoices
|
|
661
|
+
});
|
|
662
|
+
if (selected) {
|
|
663
|
+
config.theme = selected;
|
|
664
|
+
await saveConfig(config);
|
|
665
|
+
const th = setActiveTheme(selected);
|
|
666
|
+
drawLogo();
|
|
667
|
+
p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch { }
|
|
671
|
+
}
|
|
585
672
|
async function handleSettings(config) {
|
|
586
673
|
while (true) {
|
|
587
674
|
try {
|
|
588
675
|
const maxIter = config.maxIterations || 100;
|
|
589
676
|
const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
|
|
677
|
+
const currentTh = getCurrentTheme();
|
|
590
678
|
const choice = await select({
|
|
591
|
-
message: `${pc.bold('⚙️ Settings')} ${pc.dim(
|
|
679
|
+
message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.2.0 • theme: ${currentTh.name})`)}`,
|
|
592
680
|
choices: [
|
|
593
681
|
{
|
|
594
|
-
name:
|
|
682
|
+
name: `🎨 Color Theme: ${currentTh.emoji} ${currentTh.name}`,
|
|
683
|
+
value: 'change_theme',
|
|
684
|
+
description: `Switch UI accent colors (${currentTh.desc})`
|
|
685
|
+
},
|
|
686
|
+
{
|
|
687
|
+
name: `${config.pureBlackTheme !== false ? pc.green('🖤 Pure Black Background: ON') : pc.yellow('🖤 Pure Black Background: OFF')}`,
|
|
595
688
|
value: 'toggle_black_theme',
|
|
596
689
|
description: config.pureBlackTheme !== false
|
|
597
690
|
? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
|
|
@@ -619,12 +712,12 @@ export async function main() {
|
|
|
619
712
|
: 'Disable update checking on startup (run /update manually instead)'
|
|
620
713
|
},
|
|
621
714
|
{
|
|
622
|
-
name: `🔄 Max Agent Iterations: ${
|
|
715
|
+
name: `🔄 Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
|
|
623
716
|
value: 'change_max_iterations',
|
|
624
717
|
description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
|
|
625
718
|
},
|
|
626
719
|
{
|
|
627
|
-
name: `${
|
|
720
|
+
name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.2.0 by ApvCode)')}`,
|
|
628
721
|
value: 'about',
|
|
629
722
|
description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
|
|
630
723
|
},
|
|
@@ -635,8 +728,13 @@ export async function main() {
|
|
|
635
728
|
}
|
|
636
729
|
]
|
|
637
730
|
});
|
|
731
|
+
if (choice === 'change_theme') {
|
|
732
|
+
await handleThemeSelect(config);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
638
735
|
if (choice === 'about') {
|
|
639
|
-
p.note(`⚡ devx v1.
|
|
736
|
+
p.note(`⚡ devx v1.2.0 — Terminal-Native AI Coding Agent\n` +
|
|
737
|
+
`🎨 Theme: ${currentTh.emoji} ${currentTh.name}\n` +
|
|
640
738
|
`👤 Author: ApvCode (https://github.com/apvcode)\n` +
|
|
641
739
|
`🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
|
|
642
740
|
`📜 License: MIT License (2026)\n` +
|
|
@@ -665,13 +763,13 @@ export async function main() {
|
|
|
665
763
|
if (choice === 'toggle_memory') {
|
|
666
764
|
config.enableMemory = config.enableMemory === false ? true : false;
|
|
667
765
|
await saveConfig(config);
|
|
668
|
-
p.log.success(`Project memory: ${config.enableMemory ? pc.bold(pc.green('ON (
|
|
766
|
+
p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
|
|
669
767
|
continue;
|
|
670
768
|
}
|
|
671
769
|
if (choice === 'toggle_check_updates') {
|
|
672
770
|
config.checkUpdates = config.checkUpdates === false ? true : false;
|
|
673
771
|
await saveConfig(config);
|
|
674
|
-
p.log.success(`Check for updates
|
|
772
|
+
p.log.success(`Check for updates: ${config.checkUpdates !== false ? pc.bold(pc.green('ON (Checked on startup)')) : pc.bold(pc.yellow('OFF (Manual only)'))}`);
|
|
675
773
|
continue;
|
|
676
774
|
}
|
|
677
775
|
if (choice === 'change_max_iterations') {
|
|
@@ -699,18 +797,34 @@ export async function main() {
|
|
|
699
797
|
process.stdin.resume();
|
|
700
798
|
return config;
|
|
701
799
|
}
|
|
702
|
-
const VALID_COMMANDS = [
|
|
800
|
+
const VALID_COMMANDS = [
|
|
801
|
+
'/new', '/reset', '/resume', '/session', '/sessions', '/history',
|
|
802
|
+
'/theme', '/themes',
|
|
803
|
+
'/settings', '/update', '/model', '/provider', '/providers',
|
|
804
|
+
'/plan', '/agent', '/image', '/serve', '/memory', '/undo',
|
|
805
|
+
'/diff', '/commit', '/status', '/compact', '/init', '/doctor',
|
|
806
|
+
'/config', '/clear', '/exit', '/quit', '/help'
|
|
807
|
+
];
|
|
703
808
|
if (!VALID_COMMANDS.includes(cmd)) {
|
|
704
809
|
const SLASH_COMMANDS = [
|
|
705
810
|
{ name: '/new - Start a new clean chat session', value: '/new' },
|
|
706
811
|
{ name: '/resume - Resume a previous chat session', value: '/resume' },
|
|
707
812
|
{ name: '/session del - Select and delete saved sessions', value: '/session del' },
|
|
813
|
+
{ name: '/theme - Switch UI color theme', value: '/theme' },
|
|
814
|
+
{ name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
|
|
708
815
|
{ name: '/settings - Configure permissions & auto-approval', value: '/settings' },
|
|
709
816
|
{ name: '/update - Check and install updates from GitHub', value: '/update' },
|
|
710
817
|
{ name: '/model - Switch model for current provider', value: '/model' },
|
|
711
818
|
{ name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
|
|
712
819
|
{ name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
|
|
713
820
|
{ name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
|
|
821
|
+
{ name: '/serve - Start local web server for web preview', value: '/serve' },
|
|
822
|
+
{ name: '/memory - View or edit project memory bank', value: '/memory' },
|
|
823
|
+
{ name: '/undo - Revert last file changes made by AI', value: '/undo' },
|
|
824
|
+
{ name: '/diff - Show git diff of modified files', value: '/diff' },
|
|
825
|
+
{ name: '/commit - AI-generated git commit message', value: '/commit' },
|
|
826
|
+
{ name: '/status - Show git repository status', value: '/status' },
|
|
827
|
+
{ name: '/compact - Compact conversation context', value: '/compact' },
|
|
714
828
|
{ name: '/config - View current configuration', value: '/config' },
|
|
715
829
|
{ name: '/clear - Clear message history', value: '/clear' },
|
|
716
830
|
{ name: '/help - Show commands overview', value: '/help' },
|
|
@@ -1043,8 +1157,8 @@ export async function main() {
|
|
|
1043
1157
|
initialValue: true
|
|
1044
1158
|
});
|
|
1045
1159
|
if (!p.isCancel(confirmed) && confirmed) {
|
|
1046
|
-
|
|
1047
|
-
|
|
1160
|
+
execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
|
|
1161
|
+
execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'ignore' });
|
|
1048
1162
|
p.log.success(pc.bold(pc.green(`✅ Committed: ${commitMsg}`)));
|
|
1049
1163
|
}
|
|
1050
1164
|
else {
|
|
@@ -1124,6 +1238,10 @@ export async function main() {
|
|
|
1124
1238
|
}
|
|
1125
1239
|
continue;
|
|
1126
1240
|
}
|
|
1241
|
+
if (cmd === '/doctor') {
|
|
1242
|
+
await runDoctor(config);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1127
1245
|
if (cmd === '/serve') {
|
|
1128
1246
|
const sub = parts[1]?.toLowerCase();
|
|
1129
1247
|
if (sub === 'stop') {
|
|
@@ -1138,10 +1256,7 @@ export async function main() {
|
|
|
1138
1256
|
const customPort = parseInt(parts[1], 10) || 3000;
|
|
1139
1257
|
try {
|
|
1140
1258
|
const { port, localUrl, networkUrl } = await startServer(customPort);
|
|
1141
|
-
|
|
1142
|
-
console.log(pc.cyan(` • Local: ${localUrl}`));
|
|
1143
|
-
console.log(pc.cyan(` • Network: ${networkUrl}`));
|
|
1144
|
-
console.log(pc.dim(' (Use /serve stop to stop the server)\n'));
|
|
1259
|
+
await displayServerBanner(localUrl, networkUrl);
|
|
1145
1260
|
}
|
|
1146
1261
|
catch (err) {
|
|
1147
1262
|
p.log.error(`Failed to start web server: ${err.message}`);
|
|
@@ -1149,6 +1264,27 @@ export async function main() {
|
|
|
1149
1264
|
}
|
|
1150
1265
|
continue;
|
|
1151
1266
|
}
|
|
1267
|
+
if (cmd === '/theme' || cmd === '/themes') {
|
|
1268
|
+
const themeArg = answer.replace(/^\/(?:theme|themes)\s*/i, '').trim();
|
|
1269
|
+
if (themeArg) {
|
|
1270
|
+
const matched = findTheme(themeArg);
|
|
1271
|
+
if (matched) {
|
|
1272
|
+
config.theme = matched.id;
|
|
1273
|
+
await saveConfig(config);
|
|
1274
|
+
const th = setActiveTheme(matched.id);
|
|
1275
|
+
drawLogo();
|
|
1276
|
+
p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
const themes = listThemes();
|
|
1280
|
+
p.log.warn(`Unknown theme: "${themeArg}". Available themes: ${themes.map(t => `${t.emoji} ${t.id}`).join(', ')}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
await handleThemeSelect(config);
|
|
1285
|
+
}
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1152
1288
|
if (cmd === '/memory') {
|
|
1153
1289
|
const sub = parts[1]?.toLowerCase();
|
|
1154
1290
|
if (sub === 'clear') {
|
|
@@ -1330,6 +1466,8 @@ export async function main() {
|
|
|
1330
1466
|
}
|
|
1331
1467
|
catch { }
|
|
1332
1468
|
}
|
|
1469
|
+
const turnStartTime = Date.now();
|
|
1470
|
+
let toolsExecutedCount = 0;
|
|
1333
1471
|
try {
|
|
1334
1472
|
for await (const event of agent.run(abortController.signal)) {
|
|
1335
1473
|
if (aborted)
|
|
@@ -1368,15 +1506,16 @@ export async function main() {
|
|
|
1368
1506
|
}
|
|
1369
1507
|
}
|
|
1370
1508
|
else if (event.type === 'tool_generating') {
|
|
1509
|
+
const theme = getCurrentTheme();
|
|
1510
|
+
const label = formatToolGeneratingLabel(event.name, event.targetHint);
|
|
1371
1511
|
if (!spinnerActive) {
|
|
1372
1512
|
streamer.finish();
|
|
1373
1513
|
finishThinking(false);
|
|
1374
|
-
s.start(
|
|
1514
|
+
s.start(theme.boldFn(label));
|
|
1375
1515
|
spinnerActive = true;
|
|
1376
1516
|
}
|
|
1377
1517
|
else {
|
|
1378
|
-
|
|
1379
|
-
s.message(pc.cyan(`⚡ Generating ${event.name} (${chars})...`));
|
|
1518
|
+
s.message(theme.boldFn(label));
|
|
1380
1519
|
}
|
|
1381
1520
|
}
|
|
1382
1521
|
else if (event.type === 'tool_start') {
|
|
@@ -1397,6 +1536,7 @@ export async function main() {
|
|
|
1397
1536
|
}
|
|
1398
1537
|
}
|
|
1399
1538
|
else if (event.type === 'tool_end') {
|
|
1539
|
+
toolsExecutedCount++;
|
|
1400
1540
|
if (spinnerActive) {
|
|
1401
1541
|
s.stop();
|
|
1402
1542
|
spinnerActive = false;
|
|
@@ -1408,28 +1548,50 @@ export async function main() {
|
|
|
1408
1548
|
process.stdout.write(parsed.displayCard);
|
|
1409
1549
|
}
|
|
1410
1550
|
else if (parsed.action === 'edit' && parsed.diffLines) {
|
|
1551
|
+
const theme = getCurrentTheme();
|
|
1411
1552
|
const cols = Math.min(process.stdout.columns || 80, 80);
|
|
1412
|
-
const
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1553
|
+
const boxWidth = Math.max(30, Math.min(cols - 4, 76));
|
|
1554
|
+
const fileName = typeof parsed.path === 'string' ? parsed.path.split(/[\/\\]/).pop() || 'file' : 'file';
|
|
1555
|
+
const fillCount = Math.max(2, boxWidth - 5 - fileName.length);
|
|
1556
|
+
console.log(theme.colorFn('┌─ ') + pc.bold(fileName) + ' ' + theme.colorFn('─'.repeat(fillCount) + '┐'));
|
|
1557
|
+
const maxShown = Math.min(parsed.diffLines.length, 30);
|
|
1416
1558
|
for (let i = 0; i < maxShown; i++) {
|
|
1417
1559
|
const line = parsed.diffLines[i];
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1560
|
+
const match = line.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
|
|
1561
|
+
if (match) {
|
|
1562
|
+
const lineNum = match[1].padStart(4, ' ');
|
|
1563
|
+
const symbol = match[2];
|
|
1564
|
+
const contentStr = match[3] || '';
|
|
1565
|
+
const prefix = symbol.includes('-') ? '-' : symbol.includes('+') ? '+' : ' ';
|
|
1566
|
+
const maxCodeLen = Math.max(10, boxWidth - 11);
|
|
1567
|
+
const paddedCode = contentStr.length > maxCodeLen
|
|
1568
|
+
? contentStr.substring(0, maxCodeLen - 1) + '…'
|
|
1569
|
+
: contentStr.padEnd(maxCodeLen, ' ');
|
|
1570
|
+
const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
|
|
1571
|
+
if (prefix === '-') {
|
|
1572
|
+
console.log(theme.colorFn('│') + theme.diffRemoveBg(innerRow) + theme.colorFn('│'));
|
|
1573
|
+
}
|
|
1574
|
+
else if (prefix === '+') {
|
|
1575
|
+
console.log(theme.colorFn('│') + theme.diffAddBg(innerRow) + theme.colorFn('│'));
|
|
1576
|
+
}
|
|
1577
|
+
else {
|
|
1578
|
+
console.log(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
|
|
1579
|
+
}
|
|
1423
1580
|
}
|
|
1424
1581
|
else {
|
|
1425
|
-
|
|
1582
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
1583
|
+
const padded = line.length > maxLen ? line.substring(0, maxLen - 1) + '…' : line.padEnd(maxLen, ' ');
|
|
1584
|
+
console.log(theme.colorFn('│ ') + pc.white(padded) + theme.colorFn(' │'));
|
|
1426
1585
|
}
|
|
1427
1586
|
}
|
|
1428
1587
|
if (parsed.diffLines.length > maxShown) {
|
|
1429
|
-
|
|
1588
|
+
const dots = `... +${parsed.diffLines.length - maxShown} more lines`;
|
|
1589
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
1590
|
+
const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + '…' : dots.padEnd(maxLen, ' ');
|
|
1591
|
+
console.log(theme.colorFn('│ ') + pc.dim(paddedDots) + theme.colorFn(' │'));
|
|
1430
1592
|
}
|
|
1431
|
-
console.log(
|
|
1432
|
-
console.log(
|
|
1593
|
+
console.log(theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘'));
|
|
1594
|
+
console.log(theme.boldFn(` └─ ${parsed.summary}`));
|
|
1433
1595
|
}
|
|
1434
1596
|
else if (parsed.summary) {
|
|
1435
1597
|
console.log(pc.green(` └─ ${parsed.summary}`));
|
|
@@ -1508,6 +1670,12 @@ export async function main() {
|
|
|
1508
1670
|
process.stdout.write('\n');
|
|
1509
1671
|
}
|
|
1510
1672
|
globalSnapshotManager.finishTurn();
|
|
1673
|
+
const turnElapsedMs = Date.now() - turnStartTime;
|
|
1674
|
+
const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
|
|
1675
|
+
if (shouldNotify && !aborted) {
|
|
1676
|
+
const secs = Math.round(turnElapsedMs / 1000);
|
|
1677
|
+
notifyDevice('devx', `Task completed in ${secs}s!`);
|
|
1678
|
+
}
|
|
1511
1679
|
// Auto-save session
|
|
1512
1680
|
await sessionManager.save(history.getMessages(), totalSessionCost, config.model, planMode);
|
|
1513
1681
|
// Check if plan was finalized in PLAN mode
|