termux-dev 1.1.2 → 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 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.1.2</tspan>
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 -->
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
- pc.cyan(' ā–ˆā–€ā–€ā–„ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ ā–ˆ ā–ˆ'),
379
- pc.cyan(' ā–ˆ ā–ˆ ā–ˆā–€ā–€ā–€ ▀▄▀ ▀▄▀ '),
380
- pc.cyan(' ā–ˆā–„ā–„ā–€ ā–ˆā–„ā–„ā–„ ā–€ ā–€ ā–€ '),
381
- ' ' + pc.cyan(pc.bold('v1.1.2')),
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 + pc.cyan('ā–€ā–€ā–€ā–ˆā–€ā–€ā–€ ā–ˆā–€ā–€ā–€ ā–ˆā–€ā–€ā–ˆ ā–ˆā–„ ā–„ā–ˆ ā–ˆ ā–ˆ ▀▄ ā–„ā–€ ā–ˆā–€ā–€ā–„ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ'),
394
- indent + pc.cyan(' ā–ˆ ā–ˆā–€ā–€ā–€ ā–ˆā–„ā–„ā–€ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ▀▀ ā–ˆ ā–ˆ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ'),
395
- indent + pc.cyan(' ā–ˆ ā–ˆā–„ā–„ā–„ ā–ˆ ▀▄ ā–ˆ ā–ˆ ▀▄▄▀ ā–„ā–€ ▀▄ ā–ˆā–„ā–„ā–€ ā–ˆā–„ā–„ā–„ ▀▄▀ '),
396
- indent + pc.cyan(pc.bold('v1.1.2')),
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) {
@@ -520,6 +524,39 @@ export async function main() {
520
524
  const options = program.opts();
521
525
  let planMode = !!options.plan;
522
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
+ }
523
560
  enableDarkTheme(config.pureBlackTheme !== false);
524
561
  let history = new History();
525
562
  const sysPrompt = await buildSystemPrompt(planMode);
@@ -546,6 +583,7 @@ export async function main() {
546
583
  const tokenStats = `Context: ${formatTokens(currentTokens)} / ${formatTokens(maxTokens)} (${usagePercent}%) • ${costStr}`;
547
584
  const cols = process.stdout.columns || 80;
548
585
  const modeName = planMode ? 'PLAN' : 'AGENT';
586
+ const theme = getCurrentTheme();
549
587
  // Shorten model name if too long on narrow mobile screens
550
588
  let displayModel = config.model;
551
589
  if (cols < 75 && displayModel.length > 20) {
@@ -555,7 +593,7 @@ export async function main() {
555
593
  displayModel = displayModel.slice(0, 17) + '...';
556
594
  }
557
595
  }
558
- const badge = pc.bgCyan(pc.black(` devx | ${modeName} | ${displayModel} `));
596
+ const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
559
597
  if (cols < 75) {
560
598
  // 2-line layout for mobile screens: perfectly aligned with clack box borders
561
599
  p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
@@ -568,7 +606,7 @@ export async function main() {
568
606
  if (autoTriggerPrompt) {
569
607
  answer = autoTriggerPrompt;
570
608
  autoTriggerPrompt = '';
571
- console.log(pc.cyan('ā—†') + ' ' + pc.bold(pc.white(answer)));
609
+ console.log(theme.colorFn('ā—†') + ' ' + pc.bold(pc.white(answer)));
572
610
  }
573
611
  else {
574
612
  const inputStr = await askPrompt({
@@ -609,16 +647,44 @@ export async function main() {
609
647
  if (answer.startsWith('/')) {
610
648
  const parts = answer.split(' ');
611
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
+ }
612
672
  async function handleSettings(config) {
613
673
  while (true) {
614
674
  try {
615
675
  const maxIter = config.maxIterations || 100;
616
676
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
677
+ const currentTh = getCurrentTheme();
617
678
  const choice = await select({
618
- message: `${pc.bold('āš™ļø Settings')} ${pc.dim('(devx v1.1.2 • by ApvCode)')}`,
679
+ message: `${pc.bold('āš™ļø Settings')} ${pc.dim(`(devx v1.2.0 • theme: ${currentTh.name})`)}`,
619
680
  choices: [
620
681
  {
621
- name: `${config.pureBlackTheme !== false ? pc.green('šŸŽØ Pure Black Background: ON') : pc.yellow('šŸŽØ Pure Black Background: OFF')}`,
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')}`,
622
688
  value: 'toggle_black_theme',
623
689
  description: config.pureBlackTheme !== false
624
690
  ? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
@@ -646,12 +712,12 @@ export async function main() {
646
712
  : 'Disable update checking on startup (run /update manually instead)'
647
713
  },
648
714
  {
649
- name: `šŸ”„ Max Agent Iterations: ${pc.cyan(maxIterLabel)}`,
715
+ name: `šŸ”„ Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
650
716
  value: 'change_max_iterations',
651
717
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
652
718
  },
653
719
  {
654
- name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.1.2 by ApvCode)')}`,
720
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.2.0 by ApvCode)')}`,
655
721
  value: 'about',
656
722
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
657
723
  },
@@ -662,8 +728,13 @@ export async function main() {
662
728
  }
663
729
  ]
664
730
  });
731
+ if (choice === 'change_theme') {
732
+ await handleThemeSelect(config);
733
+ continue;
734
+ }
665
735
  if (choice === 'about') {
666
- p.note(`⚔ devx v1.1.2 — Terminal-Native AI Coding Agent\n` +
736
+ p.note(`⚔ devx v1.2.0 — Terminal-Native AI Coding Agent\n` +
737
+ `šŸŽØ Theme: ${currentTh.emoji} ${currentTh.name}\n` +
667
738
  `šŸ‘¤ Author: ApvCode (https://github.com/apvcode)\n` +
668
739
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
669
740
  `šŸ“œ License: MIT License (2026)\n` +
@@ -692,13 +763,13 @@ export async function main() {
692
763
  if (choice === 'toggle_memory') {
693
764
  config.enableMemory = config.enableMemory === false ? true : false;
694
765
  await saveConfig(config);
695
- p.log.success(`Project memory: ${config.enableMemory ? pc.bold(pc.green('ON (Enabled)')) : pc.bold(pc.yellow('OFF (Disabled)'))}`);
766
+ p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
696
767
  continue;
697
768
  }
698
769
  if (choice === 'toggle_check_updates') {
699
770
  config.checkUpdates = config.checkUpdates === false ? true : false;
700
771
  await saveConfig(config);
701
- p.log.success(`Check for updates on startup: ${config.checkUpdates ? pc.bold(pc.green('ON (Enabled)')) : pc.bold(pc.yellow('OFF (Disabled)'))}`);
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)'))}`);
702
773
  continue;
703
774
  }
704
775
  if (choice === 'change_max_iterations') {
@@ -726,18 +797,34 @@ export async function main() {
726
797
  process.stdin.resume();
727
798
  return config;
728
799
  }
729
- const VALID_COMMANDS = ['/new', '/reset', '/resume', '/session', '/sessions', '/history', '/settings', '/update', '/model', '/provider', '/providers', '/plan', '/agent', '/config', '/clear', '/exit', '/help'];
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
+ ];
730
808
  if (!VALID_COMMANDS.includes(cmd)) {
731
809
  const SLASH_COMMANDS = [
732
810
  { name: '/new - Start a new clean chat session', value: '/new' },
733
811
  { name: '/resume - Resume a previous chat session', value: '/resume' },
734
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' },
735
815
  { name: '/settings - Configure permissions & auto-approval', value: '/settings' },
736
816
  { name: '/update - Check and install updates from GitHub', value: '/update' },
737
817
  { name: '/model - Switch model for current provider', value: '/model' },
738
818
  { name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
739
819
  { name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
740
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' },
741
828
  { name: '/config - View current configuration', value: '/config' },
742
829
  { name: '/clear - Clear message history', value: '/clear' },
743
830
  { name: '/help - Show commands overview', value: '/help' },
@@ -1070,8 +1157,8 @@ export async function main() {
1070
1157
  initialValue: true
1071
1158
  });
1072
1159
  if (!p.isCancel(confirmed) && confirmed) {
1073
- execSync('git add -A', { stdio: 'ignore' });
1074
- execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'ignore' });
1160
+ execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
1161
+ execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'ignore' });
1075
1162
  p.log.success(pc.bold(pc.green(`āœ… Committed: ${commitMsg}`)));
1076
1163
  }
1077
1164
  else {
@@ -1151,6 +1238,10 @@ export async function main() {
1151
1238
  }
1152
1239
  continue;
1153
1240
  }
1241
+ if (cmd === '/doctor') {
1242
+ await runDoctor(config);
1243
+ continue;
1244
+ }
1154
1245
  if (cmd === '/serve') {
1155
1246
  const sub = parts[1]?.toLowerCase();
1156
1247
  if (sub === 'stop') {
@@ -1165,10 +1256,7 @@ export async function main() {
1165
1256
  const customPort = parseInt(parts[1], 10) || 3000;
1166
1257
  try {
1167
1258
  const { port, localUrl, networkUrl } = await startServer(customPort);
1168
- p.log.success(pc.bold(pc.green(`🌐 Web Server running on port ${port}!`)));
1169
- console.log(pc.cyan(` • Local: ${localUrl}`));
1170
- console.log(pc.cyan(` • Network: ${networkUrl}`));
1171
- console.log(pc.dim(' (Use /serve stop to stop the server)\n'));
1259
+ await displayServerBanner(localUrl, networkUrl);
1172
1260
  }
1173
1261
  catch (err) {
1174
1262
  p.log.error(`Failed to start web server: ${err.message}`);
@@ -1176,6 +1264,27 @@ export async function main() {
1176
1264
  }
1177
1265
  continue;
1178
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
+ }
1179
1288
  if (cmd === '/memory') {
1180
1289
  const sub = parts[1]?.toLowerCase();
1181
1290
  if (sub === 'clear') {
@@ -1357,6 +1466,8 @@ export async function main() {
1357
1466
  }
1358
1467
  catch { }
1359
1468
  }
1469
+ const turnStartTime = Date.now();
1470
+ let toolsExecutedCount = 0;
1360
1471
  try {
1361
1472
  for await (const event of agent.run(abortController.signal)) {
1362
1473
  if (aborted)
@@ -1395,15 +1506,16 @@ export async function main() {
1395
1506
  }
1396
1507
  }
1397
1508
  else if (event.type === 'tool_generating') {
1509
+ const theme = getCurrentTheme();
1398
1510
  const label = formatToolGeneratingLabel(event.name, event.targetHint);
1399
1511
  if (!spinnerActive) {
1400
1512
  streamer.finish();
1401
1513
  finishThinking(false);
1402
- s.start(pc.bold(pc.cyan(label)));
1514
+ s.start(theme.boldFn(label));
1403
1515
  spinnerActive = true;
1404
1516
  }
1405
1517
  else {
1406
- s.message(pc.bold(pc.cyan(label)));
1518
+ s.message(theme.boldFn(label));
1407
1519
  }
1408
1520
  }
1409
1521
  else if (event.type === 'tool_start') {
@@ -1424,6 +1536,7 @@ export async function main() {
1424
1536
  }
1425
1537
  }
1426
1538
  else if (event.type === 'tool_end') {
1539
+ toolsExecutedCount++;
1427
1540
  if (spinnerActive) {
1428
1541
  s.stop();
1429
1542
  spinnerActive = false;
@@ -1435,28 +1548,50 @@ export async function main() {
1435
1548
  process.stdout.write(parsed.displayCard);
1436
1549
  }
1437
1550
  else if (parsed.action === 'edit' && parsed.diffLines) {
1551
+ const theme = getCurrentTheme();
1438
1552
  const cols = Math.min(process.stdout.columns || 80, 80);
1439
- const borderChar = '─';
1440
- const boxWidth = Math.max(20, Math.min(cols - 4, 66));
1441
- console.log(pc.dim('ā”Œ' + borderChar.repeat(boxWidth) + '┐'));
1442
- const maxShown = Math.min(parsed.diffLines.length, 12);
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);
1443
1558
  for (let i = 0; i < maxShown; i++) {
1444
1559
  const line = parsed.diffLines[i];
1445
- if (line.includes(' - ')) {
1446
- console.log(pc.dim('│ ') + pc.red(line));
1447
- }
1448
- else if (line.includes(' + ')) {
1449
- console.log(pc.dim('│ ') + pc.green(line));
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
+ }
1450
1580
  }
1451
1581
  else {
1452
- console.log(pc.dim('│ ') + pc.white(line));
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(' │'));
1453
1585
  }
1454
1586
  }
1455
1587
  if (parsed.diffLines.length > maxShown) {
1456
- console.log(pc.dim(`│ ... +${parsed.diffLines.length - maxShown} more lines`));
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(' │'));
1457
1592
  }
1458
- console.log(pc.dim('ā””' + borderChar.repeat(boxWidth) + 'ā”˜'));
1459
- console.log(pc.green(` └─ ${parsed.summary}`));
1593
+ console.log(theme.colorFn('ā””' + '─'.repeat(boxWidth - 2) + 'ā”˜'));
1594
+ console.log(theme.boldFn(` └─ ${parsed.summary}`));
1460
1595
  }
1461
1596
  else if (parsed.summary) {
1462
1597
  console.log(pc.green(` └─ ${parsed.summary}`));
@@ -1535,6 +1670,12 @@ export async function main() {
1535
1670
  process.stdout.write('\n');
1536
1671
  }
1537
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
+ }
1538
1679
  // Auto-save session
1539
1680
  await sessionManager.save(history.getMessages(), totalSessionCost, config.model, planMode);
1540
1681
  // Check if plan was finalized in PLAN mode
@@ -1,11 +1,14 @@
1
1
  import pc from 'picocolors';
2
2
  import { scanProjectFiles } from './files.js';
3
3
  import { saveClipboardImage, processPastedFilePath } from './clipboard.js';
4
+ import { getCurrentTheme, listThemes } from './theme.js';
4
5
  export const SLASH_COMMANDS = [
5
6
  { cmd: '/new', desc: 'Start a new clean chat session' },
6
7
  { cmd: '/resume', desc: 'Resume a previous chat session' },
7
8
  { cmd: '/session', desc: 'Show active session ID, stats, and info' },
8
9
  { cmd: '/session del', desc: 'Select and delete saved sessions' },
10
+ { cmd: '/theme', desc: 'Switch UI theme (Cyan, Purple, Matrix, Amber, etc.)' },
11
+ { cmd: '/doctor', desc: 'Run system & environment health diagnostics' },
9
12
  { cmd: '/settings', desc: 'Configure permissions & auto-approval' },
10
13
  { cmd: '/update', desc: 'Check and install updates from GitHub' },
11
14
  { cmd: '/model', desc: 'Switch model for current provider' },
@@ -47,8 +50,9 @@ export function askPrompt(opts = {}) {
47
50
  const pastes = [];
48
51
  const imageAttachments = [];
49
52
  const usedImageNames = new Set();
53
+ const theme = getCurrentTheme();
50
54
  // Header printed once
51
- console.log(pc.cyan('ā—†') + ' ' + pc.bold(msg));
55
+ console.log(theme.colorFn('ā—†') + ' ' + pc.bold(msg));
52
56
  if (process.stdin.isTTY) {
53
57
  process.stdin.setRawMode(true);
54
58
  }
@@ -56,6 +60,33 @@ export function askPrompt(opts = {}) {
56
60
  process.stdout.write('\x1b[?2004h');
57
61
  }
58
62
  function getDropdownItems() {
63
+ if (input.startsWith('/theme ') || input.startsWith('/themes ') || input === '/theme') {
64
+ const afterCmd = input.replace(/^\/(?:theme|themes)\s*/i, '').trim().toLowerCase();
65
+ const themes = listThemes();
66
+ const matched = themes.filter(t => !afterCmd ||
67
+ t.id.toLowerCase().startsWith(afterCmd) ||
68
+ t.name.toLowerCase().includes(afterCmd));
69
+ const list = [];
70
+ if (!afterCmd || '/theme'.startsWith(input.trim().toLowerCase())) {
71
+ list.push({
72
+ label: '/theme',
73
+ desc: 'Interactive UI theme picker menu',
74
+ replacement: '/theme',
75
+ replaceStart: 0,
76
+ replaceLen: input.length
77
+ });
78
+ }
79
+ for (const t of matched) {
80
+ list.push({
81
+ label: `/theme ${t.id}`,
82
+ desc: `${t.emoji} ${t.name} (${t.desc})`,
83
+ replacement: `/theme ${t.id}`,
84
+ replaceStart: 0,
85
+ replaceLen: input.length
86
+ });
87
+ }
88
+ return list;
89
+ }
59
90
  if (input.startsWith('/')) {
60
91
  const q = input.trim().toLowerCase();
61
92
  const filtered = SLASH_COMMANDS.filter(c => c.cmd.toLowerCase().startsWith(q) || q === '/');
@@ -146,9 +177,9 @@ export function askPrompt(opts = {}) {
146
177
  const labelStr = item.label.length > 20 ? item.label.slice(0, 19) + '…' : item.label.padEnd(20);
147
178
  const maxDescLen = Math.max(6, boxWidth - 25);
148
179
  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)} `;
180
+ let row = ` ${isSelected ? theme.colorFn('›') : ' '} ${isSelected ? theme.boldFn(labelStr) : pc.white(labelStr)} ${pc.gray(descStr)} `;
150
181
  if (isSelected) {
151
- row = pc.bgCyan(pc.black(` › ${labelStr} ${descStr} `));
182
+ row = theme.badgeFn(`› ${labelStr} ${descStr}`);
152
183
  }
153
184
  dropdownLines.push(pc.dim('│') + ' ' + pc.dim('│') + row + pc.dim('│'));
154
185
  }
@@ -4,6 +4,9 @@ import fsSync from 'fs';
4
4
  import path from 'path';
5
5
  import os from 'os';
6
6
  import { spawn } from 'child_process';
7
+ import pc from 'picocolors';
8
+ import qrcode from 'qrcode-terminal';
9
+ import { getTheme } from './theme.js';
7
10
  const MIME_TYPES = {
8
11
  '.html': 'text/html; charset=utf-8',
9
12
  '.htm': 'text/html; charset=utf-8',
@@ -136,7 +139,7 @@ function renderDirectoryHtml(dirPath, relPath, files, port) {
136
139
  ${parentLink}
137
140
  ${items || '<li style="padding: 20px; text-align: center; color: #6e7681;">No visible files in this directory</li>'}
138
141
  </ul>
139
- <div class="footer">devx v1.1.2 &bull; Terminal-Native AI Assistant</div>
142
+ <div class="footer">devx v1.2.0 &bull; Terminal-Native AI Assistant</div>
140
143
  </div>
141
144
  </body>
142
145
  </html>`;
@@ -160,6 +163,13 @@ export async function startServer(preferredPort = 3000) {
160
163
  res.end('Forbidden');
161
164
  return;
162
165
  }
166
+ // Security check: block dotfiles, hidden directories (.env, .git, etc.) and node_modules
167
+ const segments = reqPath.split(/[\/\\]/).filter(Boolean);
168
+ if (segments.some(seg => (seg.startsWith('.') && seg !== '.') || seg === 'node_modules')) {
169
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
170
+ res.end('Forbidden');
171
+ return;
172
+ }
163
173
  // 1. Root / Directory Request Handling
164
174
  if (fsSync.existsSync(targetPath)) {
165
175
  const stat = await fs.stat(targetPath);
@@ -232,8 +242,14 @@ export async function startServer(preferredPort = 3000) {
232
242
  res.end(`Internal Server Error: ${err.message}`);
233
243
  }
234
244
  });
245
+ let retryCount = 0;
235
246
  server.on('error', (err) => {
236
247
  if (err.code === 'EADDRINUSE') {
248
+ retryCount++;
249
+ if (retryCount > 20) {
250
+ reject(new Error(`Could not find an open port after 20 attempts (started at ${preferredPort})`));
251
+ return;
252
+ }
237
253
  activePort++;
238
254
  server.listen(activePort, '0.0.0.0');
239
255
  }
@@ -250,6 +266,7 @@ export async function startServer(preferredPort = 3000) {
250
266
  if (process.env.PREFIX?.includes('com.termux')) {
251
267
  try {
252
268
  const opener = spawn('termux-open-url', [localUrl], { stdio: 'ignore', detached: true });
269
+ opener.on('error', () => { });
253
270
  opener.unref();
254
271
  }
255
272
  catch { }
@@ -258,3 +275,41 @@ export async function startServer(preferredPort = 3000) {
258
275
  });
259
276
  });
260
277
  }
278
+ export function getQrCodeString(text) {
279
+ return new Promise((resolve) => {
280
+ try {
281
+ qrcode.generate(text, { small: true }, (qr) => {
282
+ resolve(qr);
283
+ });
284
+ }
285
+ catch {
286
+ resolve('');
287
+ }
288
+ });
289
+ }
290
+ export async function displayServerBanner(localUrl, networkUrl) {
291
+ const th = getTheme();
292
+ const qr = await getQrCodeString(networkUrl);
293
+ const cols = Math.min(process.stdout.columns || 80, 80);
294
+ const cardWidth = Math.max(36, Math.min(cols - 4, 66));
295
+ const innerWidth = cardWidth - 2;
296
+ const title = ' 🌐 Live Web Preview ';
297
+ const topFill = Math.max(2, cardWidth - 3 - title.length);
298
+ console.log('\n' + th.colorFn('ā”Œā”€') + pc.bold(title) + th.colorFn('─'.repeat(topFill) + '┐'));
299
+ const printRow = (content) => {
300
+ const visibleLength = content.replace(/\u001b\[[0-9;]*m/g, '').length;
301
+ const padding = Math.max(0, innerWidth - visibleLength);
302
+ console.log(th.colorFn('│') + content + ' '.repeat(padding) + th.colorFn('│'));
303
+ };
304
+ printRow(` ${pc.bold('Local:')} ${pc.cyan(localUrl)}`);
305
+ printRow(` ${pc.bold('Network:')} ${pc.green(networkUrl)}`);
306
+ printRow(' ');
307
+ printRow(` ${pc.bold('šŸ“± Mobile QR:')}`);
308
+ if (qr) {
309
+ const qrLines = qr.trim().split('\n');
310
+ for (const ql of qrLines) {
311
+ printRow(` ${ql}`);
312
+ }
313
+ }
314
+ console.log(th.colorFn('ā””' + '─'.repeat(innerWidth) + 'ā”˜\n'));
315
+ }
@@ -0,0 +1,140 @@
1
+ import pc from 'picocolors';
2
+ export const THEMES = {
3
+ cyan: {
4
+ id: 'cyan',
5
+ name: 'Cyan Cyber',
6
+ desc: 'Electric neon cyan & obsidian (Default)',
7
+ emoji: '⚔',
8
+ colorFn: (s) => pc.cyan(s),
9
+ boldFn: (s) => pc.bold(pc.cyan(s)),
10
+ accentFn: (s) => pc.blue(s),
11
+ badgeFn: (s) => pc.bgCyan(pc.black(` ${s} `)),
12
+ diffAddBg: (s) => pc.bgCyan(pc.black(s)),
13
+ diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
14
+ hex: '#00f2fe'
15
+ },
16
+ purple: {
17
+ id: 'purple',
18
+ name: 'Synthwave Purple',
19
+ desc: 'Vibrant neon magenta & violet retro',
20
+ emoji: '🟣',
21
+ colorFn: (s) => pc.magenta(s),
22
+ boldFn: (s) => pc.bold(pc.magenta(s)),
23
+ accentFn: (s) => pc.blue(s),
24
+ badgeFn: (s) => pc.bgMagenta(pc.black(` ${s} `)),
25
+ diffAddBg: (s) => pc.bgMagenta(pc.black(s)),
26
+ diffRemoveBg: (s) => pc.bgBlue(pc.white(s)),
27
+ hex: '#d946ef'
28
+ },
29
+ matrix: {
30
+ id: 'matrix',
31
+ name: 'Matrix Hacker',
32
+ desc: 'Classic bright phosphor green terminal',
33
+ emoji: '🟢',
34
+ colorFn: (s) => pc.green(s),
35
+ boldFn: (s) => pc.bold(pc.green(s)),
36
+ accentFn: (s) => pc.cyan(s),
37
+ badgeFn: (s) => pc.bgGreen(pc.black(` ${s} `)),
38
+ diffAddBg: (s) => pc.bgGreen(pc.black(s)),
39
+ diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
40
+ hex: '#22c55e'
41
+ },
42
+ amber: {
43
+ id: 'amber',
44
+ name: 'Solar Amber',
45
+ desc: 'Warm vintage CRT amber gold',
46
+ emoji: '🟔',
47
+ colorFn: (s) => pc.yellow(s),
48
+ boldFn: (s) => pc.bold(pc.yellow(s)),
49
+ accentFn: (s) => pc.red(s),
50
+ badgeFn: (s) => pc.bgYellow(pc.black(` ${s} `)),
51
+ diffAddBg: (s) => pc.bgYellow(pc.black(s)),
52
+ diffRemoveBg: (s) => pc.bgRed(pc.white(s)),
53
+ hex: '#f59e0b'
54
+ },
55
+ crimson: {
56
+ id: 'crimson',
57
+ name: 'Ruby Crimson',
58
+ desc: 'Aggressive cyberpunk scarlet red',
59
+ emoji: 'šŸ”“',
60
+ colorFn: (s) => pc.red(s),
61
+ boldFn: (s) => pc.bold(pc.red(s)),
62
+ accentFn: (s) => pc.magenta(s),
63
+ badgeFn: (s) => pc.bgRed(pc.white(` ${s} `)),
64
+ diffAddBg: (s) => pc.bgRed(pc.white(s)),
65
+ diffRemoveBg: (s) => pc.bgMagenta(pc.white(s)),
66
+ hex: '#ef4444'
67
+ },
68
+ monochrome: {
69
+ id: 'monochrome',
70
+ name: 'Pure Monochrome',
71
+ desc: 'Crisp minimal pure white & gray',
72
+ emoji: '⚪',
73
+ colorFn: (s) => pc.white(s),
74
+ boldFn: (s) => pc.bold(pc.white(s)),
75
+ accentFn: (s) => pc.dim(s),
76
+ badgeFn: (s) => pc.bgWhite(pc.black(` ${s} `)),
77
+ diffAddBg: (s) => pc.bgWhite(pc.black(s)),
78
+ diffRemoveBg: (s) => pc.bgBlack(pc.white(s)),
79
+ hex: '#ffffff'
80
+ }
81
+ };
82
+ const THEME_ALIASES = {
83
+ '1': 'cyan',
84
+ '2': 'purple',
85
+ '3': 'matrix',
86
+ '4': 'amber',
87
+ '5': 'crimson',
88
+ '6': 'monochrome',
89
+ 'blue': 'cyan',
90
+ 'neon': 'cyan',
91
+ 'cyber': 'cyan',
92
+ 'magenta': 'purple',
93
+ 'violet': 'purple',
94
+ 'pink': 'purple',
95
+ 'synthwave': 'purple',
96
+ 'green': 'matrix',
97
+ 'hacker': 'matrix',
98
+ 'terminal': 'matrix',
99
+ 'yellow': 'amber',
100
+ 'gold': 'amber',
101
+ 'solar': 'amber',
102
+ 'orange': 'amber',
103
+ 'red': 'crimson',
104
+ 'ruby': 'crimson',
105
+ 'cyberpunk': 'crimson',
106
+ 'white': 'monochrome',
107
+ 'mono': 'monochrome',
108
+ 'gray': 'monochrome',
109
+ 'grey': 'monochrome'
110
+ };
111
+ let activeThemeId = 'cyan';
112
+ export function getTheme(themeId) {
113
+ const id = (themeId || activeThemeId).toLowerCase().trim();
114
+ return THEMES[id] || (THEME_ALIASES[id] && THEMES[THEME_ALIASES[id]]) || THEMES.cyan;
115
+ }
116
+ export function findTheme(query) {
117
+ const q = (query || '').toLowerCase().trim();
118
+ if (!q)
119
+ return null;
120
+ if (THEMES[q])
121
+ return THEMES[q];
122
+ if (THEME_ALIASES[q] && THEMES[THEME_ALIASES[q]])
123
+ return THEMES[THEME_ALIASES[q]];
124
+ const all = listThemes();
125
+ const found = all.find(t => t.id.toLowerCase() === q ||
126
+ t.id.toLowerCase().startsWith(q) ||
127
+ t.name.toLowerCase().includes(q));
128
+ return found || null;
129
+ }
130
+ export function setActiveTheme(themeId) {
131
+ const theme = getTheme(themeId);
132
+ activeThemeId = theme.id;
133
+ return theme;
134
+ }
135
+ export function getCurrentTheme() {
136
+ return THEMES[activeThemeId] || THEMES.cyan;
137
+ }
138
+ export function listThemes() {
139
+ return Object.values(THEMES);
140
+ }
@@ -0,0 +1,40 @@
1
+ import { spawn } from 'child_process';
2
+ export function isTermux() {
3
+ return !!process.env.PREFIX?.includes('com.termux');
4
+ }
5
+ /**
6
+ * Sends a notification and haptic vibration to the device.
7
+ * On Termux: uses termux-notification and termux-vibrate via termux-api.
8
+ * On Desktop: emits terminal bell \u0007.
9
+ */
10
+ export function notifyDevice(title, message, options = { vibrate: true, sound: true }) {
11
+ if (isTermux()) {
12
+ try {
13
+ // 1. Android notification via Termux API
14
+ const notif = spawn('termux-notification', [
15
+ '--title', title,
16
+ '--content', message,
17
+ '--id', 'devx_task_notif',
18
+ '--priority', 'high'
19
+ ], { stdio: 'ignore', detached: true });
20
+ notif.on('error', () => { });
21
+ notif.unref();
22
+ // 2. Haptic vibration (150ms)
23
+ if (options.vibrate !== false) {
24
+ const vib = spawn('termux-vibrate', ['-d', '150'], { stdio: 'ignore', detached: true });
25
+ vib.on('error', () => { });
26
+ vib.unref();
27
+ }
28
+ }
29
+ catch { }
30
+ }
31
+ else {
32
+ // Desktop terminal bell
33
+ if (options.sound !== false) {
34
+ try {
35
+ process.stdout.write('\u0007');
36
+ }
37
+ catch { }
38
+ }
39
+ }
40
+ }
@@ -36,12 +36,8 @@ export class SnapshotManager {
36
36
  });
37
37
  }
38
38
  }
39
- catch {
40
- this.currentTurn.files.set(resolved, {
41
- filePath: resolved,
42
- existed: false,
43
- content: null
44
- });
39
+ catch (err) {
40
+ throw new Error(`Cannot safely snapshot file ${resolved} before edit: ${err.message}`);
45
41
  }
46
42
  }
47
43
  finishTurn() {
@@ -41,8 +41,9 @@ export async function buildSystemPrompt(planMode) {
41
41
  prompt += `- Whenever you modify or create files, use the 'diagnose_code' tool to check for any syntax or type errors.\n`;
42
42
  prompt += `- If any error is found, automatically fix it with 'edit_file' until all diagnostics pass cleanly.\n`;
43
43
  prompt += `- If you need dependencies, use 'install_package' to install them cleanly.\n`;
44
+ prompt += `- Whenever you create or modify web applications, sites, HTML/CSS/JS, canvas games, or React/Vite frontend apps, automatically call the 'serve_preview' tool with action='start' to start the local preview server and display the mobile QR-code for the user!\n`;
44
45
  prompt += `- Use 'save_memory' to remember important architectural decisions, user preferences, or project rules.\n`;
45
- prompt += `Always explain your actions briefly before using tools.\n`;
46
+ prompt += `- Always explain your actions briefly before using tools.\n`;
46
47
  }
47
48
  // Load Project Memory Bank
48
49
  try {
package/dist/tools/fs.js CHANGED
@@ -63,13 +63,20 @@ export const writeFileTool = {
63
63
  }
64
64
  catch { }
65
65
  await fs.writeFile(args.path, args.content, 'utf8');
66
- const newLines = args.content.split('\n').length;
66
+ const contentLines = args.content.split('\n');
67
+ const newLines = contentLines.length;
68
+ const diffLines = [];
69
+ const showLines = Math.min(contentLines.length, 6);
70
+ for (let i = 0; i < showLines; i++) {
71
+ diffLines.push(`${i + 1} + ${contentLines[i]}`);
72
+ }
67
73
  if (!existed) {
68
74
  return JSON.stringify({
69
75
  status: 'success',
70
- action: 'create',
76
+ action: 'edit',
71
77
  path: args.path,
72
78
  addedCount: newLines,
79
+ diffLines,
73
80
  summary: `Successfully created ${args.path} (+${newLines} lines)`
74
81
  });
75
82
  }
@@ -78,9 +85,10 @@ export const writeFileTool = {
78
85
  const diffStr = diff >= 0 ? `+${diff}` : `${diff}`;
79
86
  return JSON.stringify({
80
87
  status: 'success',
81
- action: 'write',
88
+ action: 'edit',
82
89
  path: args.path,
83
90
  addedCount: newLines,
91
+ diffLines,
84
92
  summary: `Successfully updated ${args.path} (${diffStr} lines, ${newLines} total)`
85
93
  });
86
94
  }
@@ -131,16 +139,20 @@ export const editFileTool = {
131
139
  }
132
140
  }
133
141
  const targetIndex = content.indexOf(target);
142
+ const allLines = content.split('\n');
134
143
  const startLine = content.slice(0, targetIndex).split('\n').length;
135
144
  const removedArr = target.split('\n');
145
+ const endLine = startLine + removedArr.length - 1;
146
+ const contextBefore = allLines.slice(Math.max(0, startLine - 3), startLine - 1);
147
+ const contextAfter = allLines.slice(endLine, Math.min(allLines.length, endLine + 2));
136
148
  const addedArr = args.replacement.split('\n');
137
149
  const diffLines = [];
138
- removedArr.forEach((l, i) => {
139
- diffLines.push(`${startLine + i} - ${l}`);
140
- });
141
- addedArr.forEach((l, i) => {
142
- diffLines.push(`${startLine + i} + ${l}`);
143
- });
150
+ let lineCounter = startLine - contextBefore.length;
151
+ contextBefore.forEach((l) => diffLines.push(`${lineCounter++} ${l}`));
152
+ removedArr.forEach((l) => diffLines.push(`${lineCounter++} - ${l}`));
153
+ let addCounter = startLine;
154
+ addedArr.forEach((l) => diffLines.push(`${addCounter++} + ${l}`));
155
+ contextAfter.forEach((l) => diffLines.push(`${addCounter++} ${l}`));
144
156
  const newContent = content.replace(target, args.replacement);
145
157
  await fs.writeFile(args.path, newContent, 'utf8');
146
158
  return JSON.stringify({
@@ -10,10 +10,10 @@ import { planReadyTool, lastPlanReady, resetPlanReady } from './plan.js';
10
10
  import { todoListTool, currentTodoList, resetTodoList } from './todo.js';
11
11
  import { servePreviewTool } from './server.js';
12
12
  export function getTools(planMode) {
13
- const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool, servePreviewTool];
13
+ const baseTools = [readFileTool, listDirTool, searchTool, askQuestionsTool, webSearchTool, fetchUrlTool, saveMemoryTool, planReadyTool, todoListTool];
14
14
  if (planMode) {
15
15
  return baseTools;
16
16
  }
17
- return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool];
17
+ return [...baseTools, writeFileTool, editFileTool, mkdirTool, bashTool, diagnoseCodeTool, installPackageTool, servePreviewTool];
18
18
  }
19
19
  export { webSearchTool, fetchUrlTool, diagnoseCodeTool, installPackageTool, saveMemoryTool, planReadyTool, lastPlanReady, resetPlanReady, todoListTool, currentTodoList, resetTodoList, servePreviewTool };
@@ -1,9 +1,9 @@
1
- import { startServer, stopServer, isServerRunning, getServerPort } from '../cli/server.js';
1
+ import { startServer, stopServer, isServerRunning, getServerPort, displayServerBanner } from '../cli/server.js';
2
2
  export const servePreviewTool = {
3
3
  name: 'serve_preview',
4
4
  definition: {
5
5
  name: 'serve_preview',
6
- description: 'Start or stop the built-in local live web preview server to view HTML/web apps in the browser.',
6
+ description: 'Start or stop the built-in local live web preview server to view HTML/web apps in the browser with instant QR-code.',
7
7
  parameters: {
8
8
  type: 'object',
9
9
  properties: {
@@ -37,7 +37,8 @@ export const servePreviewTool = {
37
37
  const preferredPort = args.port || 3000;
38
38
  try {
39
39
  const { port, localUrl, networkUrl } = await startServer(preferredPort);
40
- return `Web Server is now live!\n• Local URL: ${localUrl}\n• Network URL: ${networkUrl}\nOpened in browser automatically.`;
40
+ await displayServerBanner(localUrl, networkUrl);
41
+ return `Web Server is now live!\n• Local URL: ${localUrl}\n• Network URL: ${networkUrl}\nOpened in browser automatically with mobile QR-code displayed in terminal.`;
41
42
  }
42
43
  catch (err) {
43
44
  throw new Error(`Failed to start web server: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termux-dev",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Ultra-fast, terminal-native AI coding assistant and agent built for Android Termux, Windows, macOS, and Linux.",
5
5
  "main": "dist/cli/index.js",
6
6
  "type": "module",
@@ -54,11 +54,13 @@
54
54
  "commander": "^12.0.0",
55
55
  "marked": "^15.0.12",
56
56
  "marked-terminal": "^7.3.0",
57
- "picocolors": "^1.1.1"
57
+ "picocolors": "^1.1.1",
58
+ "qrcode-terminal": "^0.12.0"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@types/marked-terminal": "^6.1.1",
61
62
  "@types/node": "^20.0.0",
63
+ "@types/qrcode-terminal": "^0.12.2",
62
64
  "typescript": "^5.4.0"
63
65
  }
64
66
  }