termux-dev 1.1.2 → 1.2.1

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)
@@ -348,6 +351,10 @@ function resetTerminalTheme() {
348
351
  }
349
352
  let activeAbortHandler = null;
350
353
  process.on('exit', () => {
354
+ if (process.stdin.isTTY) {
355
+ process.stdin.setRawMode(false);
356
+ }
357
+ process.stdout.write('\x1B[?25h');
351
358
  resetTerminalTheme();
352
359
  });
353
360
  process.on('SIGINT', () => {
@@ -355,6 +362,11 @@ process.on('SIGINT', () => {
355
362
  activeAbortHandler();
356
363
  }
357
364
  else {
365
+ if (process.stdin.isTTY) {
366
+ process.stdin.setRawMode(false);
367
+ process.stdin.pause();
368
+ }
369
+ process.stdout.write('\x1B[?25h'); // restore cursor
358
370
  resetTerminalTheme();
359
371
  process.exit(0);
360
372
  }
@@ -371,14 +383,15 @@ function clearTerminalScreen() {
371
383
  function drawLogo() {
372
384
  const cols = process.stdout.columns || 80;
373
385
  clearTerminalScreen();
386
+ const theme = getCurrentTheme();
374
387
  if (cols < 56) {
375
388
  // Ultra-clean compact ASCII for small mobile screens (width: ~26 chars)
376
389
  const logo = [
377
390
  '',
378
- pc.cyan(' ā–ˆā–€ā–€ā–„ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ ā–ˆ ā–ˆ'),
379
- pc.cyan(' ā–ˆ ā–ˆ ā–ˆā–€ā–€ā–€ ▀▄▀ ▀▄▀ '),
380
- pc.cyan(' ā–ˆā–„ā–„ā–€ ā–ˆā–„ā–„ā–„ ā–€ ā–€ ā–€ '),
381
- ' ' + pc.cyan(pc.bold('v1.1.2')),
391
+ theme.colorFn(' ā–ˆā–€ā–€ā–„ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ ā–ˆ ā–ˆ'),
392
+ theme.colorFn(' ā–ˆ ā–ˆ ā–ˆā–€ā–€ā–€ ▀▄▀ ▀▄▀ '),
393
+ theme.colorFn(' ā–ˆā–„ā–„ā–€ ā–ˆā–„ā–„ā–„ ā–€ ā–€ ā–€ '),
394
+ ' ' + theme.boldFn('v1.2.0'),
382
395
  ''
383
396
  ];
384
397
  for (const line of logo) {
@@ -390,10 +403,10 @@ function drawLogo() {
390
403
  const indent = cols < 68 ? ' ' : ' ';
391
404
  const logo = [
392
405
  '',
393
- indent + pc.cyan('ā–€ā–€ā–€ā–ˆā–€ā–€ā–€ ā–ˆā–€ā–€ā–€ ā–ˆā–€ā–€ā–ˆ ā–ˆā–„ ā–„ā–ˆ ā–ˆ ā–ˆ ▀▄ ā–„ā–€ ā–ˆā–€ā–€ā–„ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ'),
394
- indent + pc.cyan(' ā–ˆ ā–ˆā–€ā–€ā–€ ā–ˆā–„ā–„ā–€ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ▀▀ ā–ˆ ā–ˆ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ'),
395
- indent + pc.cyan(' ā–ˆ ā–ˆā–„ā–„ā–„ ā–ˆ ▀▄ ā–ˆ ā–ˆ ▀▄▄▀ ā–„ā–€ ▀▄ ā–ˆā–„ā–„ā–€ ā–ˆā–„ā–„ā–„ ▀▄▀ '),
396
- indent + pc.cyan(pc.bold('v1.1.2')),
406
+ indent + theme.colorFn('ā–€ā–€ā–€ā–ˆā–€ā–€ā–€ ā–ˆā–€ā–€ā–€ ā–ˆā–€ā–€ā–ˆ ā–ˆā–„ ā–„ā–ˆ ā–ˆ ā–ˆ ▀▄ ā–„ā–€ ā–ˆā–€ā–€ā–„ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ'),
407
+ indent + theme.colorFn(' ā–ˆ ā–ˆā–€ā–€ā–€ ā–ˆā–„ā–„ā–€ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ā–ˆ ▀▀ ā–ˆ ā–ˆ ā–ˆā–€ā–€ā–€ ā–ˆ ā–ˆ'),
408
+ indent + theme.colorFn(' ā–ˆ ā–ˆā–„ā–„ā–„ ā–ˆ ▀▄ ā–ˆ ā–ˆ ▀▄▄▀ ā–„ā–€ ▀▄ ā–ˆā–„ā–„ā–€ ā–ˆā–„ā–„ā–„ ▀▄▀ '),
409
+ indent + theme.boldFn('v1.2.0'),
397
410
  ''
398
411
  ];
399
412
  for (const line of logo) {
@@ -520,6 +533,39 @@ export async function main() {
520
533
  const options = program.opts();
521
534
  let planMode = !!options.plan;
522
535
  let config = await loadConfig();
536
+ if (!config.onboarded) {
537
+ const cols = Math.min(process.stdout.columns || 40, 42);
538
+ const fill = Math.max(2, cols - 16);
539
+ const topBorder = 'ā”Œā”€ preview.ts ' + '─'.repeat(fill) + '┐';
540
+ const bottomBorder = 'ā””' + '─'.repeat(cols - 2) + 'ā”˜';
541
+ const themeChoices = listThemes().map(t => {
542
+ const addLine = t.diffAddBg(` + 1 | const theme = "${t.id}";`.padEnd(cols - 2));
543
+ const remLine = t.diffRemoveBg(` - 2 | const theme = "none";`.padEnd(cols - 2));
544
+ const preview = `${t.colorFn(topBorder)}\n${addLine}\n${remLine}\n${t.colorFn(bottomBorder)}\n\n${pc.dim('šŸ’” You can change this anytime with /theme')}`;
545
+ return {
546
+ name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)}`,
547
+ value: t.id,
548
+ description: preview
549
+ };
550
+ });
551
+ try {
552
+ clearTerminalScreen();
553
+ const selected = await search({
554
+ message: `${pc.bold('šŸŽØ Pick a theme to personalize your workspace:')}`,
555
+ source: async () => themeChoices,
556
+ pageSize: 6
557
+ });
558
+ if (selected) {
559
+ config.theme = selected;
560
+ }
561
+ }
562
+ catch { }
563
+ config.onboarded = true;
564
+ await saveConfig(config);
565
+ }
566
+ if (config.theme) {
567
+ setActiveTheme(config.theme);
568
+ }
523
569
  enableDarkTheme(config.pureBlackTheme !== false);
524
570
  let history = new History();
525
571
  const sysPrompt = await buildSystemPrompt(planMode);
@@ -546,6 +592,7 @@ export async function main() {
546
592
  const tokenStats = `Context: ${formatTokens(currentTokens)} / ${formatTokens(maxTokens)} (${usagePercent}%) • ${costStr}`;
547
593
  const cols = process.stdout.columns || 80;
548
594
  const modeName = planMode ? 'PLAN' : 'AGENT';
595
+ const theme = getCurrentTheme();
549
596
  // Shorten model name if too long on narrow mobile screens
550
597
  let displayModel = config.model;
551
598
  if (cols < 75 && displayModel.length > 20) {
@@ -555,7 +602,7 @@ export async function main() {
555
602
  displayModel = displayModel.slice(0, 17) + '...';
556
603
  }
557
604
  }
558
- const badge = pc.bgCyan(pc.black(` devx | ${modeName} | ${displayModel} `));
605
+ const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
559
606
  if (cols < 75) {
560
607
  // 2-line layout for mobile screens: perfectly aligned with clack box borders
561
608
  p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
@@ -568,7 +615,7 @@ export async function main() {
568
615
  if (autoTriggerPrompt) {
569
616
  answer = autoTriggerPrompt;
570
617
  autoTriggerPrompt = '';
571
- console.log(pc.cyan('ā—†') + ' ' + pc.bold(pc.white(answer)));
618
+ console.log(theme.colorFn('ā—†') + ' ' + pc.bold(pc.white(answer)));
572
619
  }
573
620
  else {
574
621
  const inputStr = await askPrompt({
@@ -609,16 +656,44 @@ export async function main() {
609
656
  if (answer.startsWith('/')) {
610
657
  const parts = answer.split(' ');
611
658
  let cmd = parts[0];
659
+ async function handleThemeSelect(config) {
660
+ const currentTh = getCurrentTheme();
661
+ const themeChoices = listThemes().map(t => ({
662
+ name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
663
+ value: t.id,
664
+ description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
665
+ }));
666
+ try {
667
+ const selected = await select({
668
+ message: `${pc.bold('šŸŽØ Select UI Theme / Выберите Ń†Š²ŠµŃ‚Š¾Š²ŃƒŃŽ Ń‚ŠµŠ¼Ńƒ:')}`,
669
+ choices: themeChoices
670
+ });
671
+ if (selected) {
672
+ config.theme = selected;
673
+ await saveConfig(config);
674
+ const th = setActiveTheme(selected);
675
+ drawLogo();
676
+ p.log.success(th.boldFn(`šŸŽØ Theme switched to ${th.emoji} ${th.name}!`));
677
+ }
678
+ }
679
+ catch { }
680
+ }
612
681
  async function handleSettings(config) {
613
682
  while (true) {
614
683
  try {
615
684
  const maxIter = config.maxIterations || 100;
616
685
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
686
+ const currentTh = getCurrentTheme();
617
687
  const choice = await select({
618
- message: `${pc.bold('āš™ļø Settings')} ${pc.dim('(devx v1.1.2 • by ApvCode)')}`,
688
+ message: `${pc.bold('āš™ļø Settings')} ${pc.dim(`(devx v1.2.0 • theme: ${currentTh.name})`)}`,
619
689
  choices: [
620
690
  {
621
- name: `${config.pureBlackTheme !== false ? pc.green('šŸŽØ Pure Black Background: ON') : pc.yellow('šŸŽØ Pure Black Background: OFF')}`,
691
+ name: `šŸŽØ Color Theme: ${currentTh.emoji} ${currentTh.name}`,
692
+ value: 'change_theme',
693
+ description: `Switch UI accent colors (${currentTh.desc})`
694
+ },
695
+ {
696
+ name: `${config.pureBlackTheme !== false ? pc.green('šŸ–¤ Pure Black Background: ON') : pc.yellow('šŸ–¤ Pure Black Background: OFF')}`,
622
697
  value: 'toggle_black_theme',
623
698
  description: config.pureBlackTheme !== false
624
699
  ? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
@@ -646,12 +721,12 @@ export async function main() {
646
721
  : 'Disable update checking on startup (run /update manually instead)'
647
722
  },
648
723
  {
649
- name: `šŸ”„ Max Agent Iterations: ${pc.cyan(maxIterLabel)}`,
724
+ name: `šŸ”„ Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
650
725
  value: 'change_max_iterations',
651
726
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
652
727
  },
653
728
  {
654
- name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.1.2 by ApvCode)')}`,
729
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.2.0 by ApvCode)')}`,
655
730
  value: 'about',
656
731
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
657
732
  },
@@ -662,8 +737,13 @@ export async function main() {
662
737
  }
663
738
  ]
664
739
  });
740
+ if (choice === 'change_theme') {
741
+ await handleThemeSelect(config);
742
+ continue;
743
+ }
665
744
  if (choice === 'about') {
666
- p.note(`⚔ devx v1.1.2 — Terminal-Native AI Coding Agent\n` +
745
+ p.note(`⚔ devx v1.2.0 — Terminal-Native AI Coding Agent\n` +
746
+ `šŸŽØ Theme: ${currentTh.emoji} ${currentTh.name}\n` +
667
747
  `šŸ‘¤ Author: ApvCode (https://github.com/apvcode)\n` +
668
748
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
669
749
  `šŸ“œ License: MIT License (2026)\n` +
@@ -692,13 +772,13 @@ export async function main() {
692
772
  if (choice === 'toggle_memory') {
693
773
  config.enableMemory = config.enableMemory === false ? true : false;
694
774
  await saveConfig(config);
695
- p.log.success(`Project memory: ${config.enableMemory ? pc.bold(pc.green('ON (Enabled)')) : pc.bold(pc.yellow('OFF (Disabled)'))}`);
775
+ p.log.success(`Project memory bank: ${config.enableMemory !== false ? pc.bold(pc.green('ON (Persistent .devx/memory.md)')) : pc.bold(pc.yellow('OFF'))}`);
696
776
  continue;
697
777
  }
698
778
  if (choice === 'toggle_check_updates') {
699
779
  config.checkUpdates = config.checkUpdates === false ? true : false;
700
780
  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)'))}`);
781
+ 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
782
  continue;
703
783
  }
704
784
  if (choice === 'change_max_iterations') {
@@ -726,18 +806,34 @@ export async function main() {
726
806
  process.stdin.resume();
727
807
  return config;
728
808
  }
729
- const VALID_COMMANDS = ['/new', '/reset', '/resume', '/session', '/sessions', '/history', '/settings', '/update', '/model', '/provider', '/providers', '/plan', '/agent', '/config', '/clear', '/exit', '/help'];
809
+ const VALID_COMMANDS = [
810
+ '/new', '/reset', '/resume', '/session', '/sessions', '/history',
811
+ '/theme', '/themes',
812
+ '/settings', '/update', '/model', '/provider', '/providers',
813
+ '/plan', '/agent', '/image', '/serve', '/memory', '/undo',
814
+ '/diff', '/commit', '/status', '/compact', '/init', '/doctor',
815
+ '/config', '/clear', '/exit', '/quit', '/help'
816
+ ];
730
817
  if (!VALID_COMMANDS.includes(cmd)) {
731
818
  const SLASH_COMMANDS = [
732
819
  { name: '/new - Start a new clean chat session', value: '/new' },
733
820
  { name: '/resume - Resume a previous chat session', value: '/resume' },
734
821
  { name: '/session del - Select and delete saved sessions', value: '/session del' },
822
+ { name: '/theme - Switch UI color theme', value: '/theme' },
823
+ { name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
735
824
  { name: '/settings - Configure permissions & auto-approval', value: '/settings' },
736
825
  { name: '/update - Check and install updates from GitHub', value: '/update' },
737
826
  { name: '/model - Switch model for current provider', value: '/model' },
738
827
  { name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
739
828
  { name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
740
829
  { name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
830
+ { name: '/serve - Start local web server for web preview', value: '/serve' },
831
+ { name: '/memory - View or edit project memory bank', value: '/memory' },
832
+ { name: '/undo - Revert last file changes made by AI', value: '/undo' },
833
+ { name: '/diff - Show git diff of modified files', value: '/diff' },
834
+ { name: '/commit - AI-generated git commit message', value: '/commit' },
835
+ { name: '/status - Show git repository status', value: '/status' },
836
+ { name: '/compact - Compact conversation context', value: '/compact' },
741
837
  { name: '/config - View current configuration', value: '/config' },
742
838
  { name: '/clear - Clear message history', value: '/clear' },
743
839
  { name: '/help - Show commands overview', value: '/help' },
@@ -1070,8 +1166,8 @@ export async function main() {
1070
1166
  initialValue: true
1071
1167
  });
1072
1168
  if (!p.isCancel(confirmed) && confirmed) {
1073
- execSync('git add -A', { stdio: 'ignore' });
1074
- execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'ignore' });
1169
+ execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
1170
+ execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'ignore' });
1075
1171
  p.log.success(pc.bold(pc.green(`āœ… Committed: ${commitMsg}`)));
1076
1172
  }
1077
1173
  else {
@@ -1151,6 +1247,10 @@ export async function main() {
1151
1247
  }
1152
1248
  continue;
1153
1249
  }
1250
+ if (cmd === '/doctor') {
1251
+ await runDoctor(config);
1252
+ continue;
1253
+ }
1154
1254
  if (cmd === '/serve') {
1155
1255
  const sub = parts[1]?.toLowerCase();
1156
1256
  if (sub === 'stop') {
@@ -1165,10 +1265,7 @@ export async function main() {
1165
1265
  const customPort = parseInt(parts[1], 10) || 3000;
1166
1266
  try {
1167
1267
  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'));
1268
+ await displayServerBanner(localUrl, networkUrl);
1172
1269
  }
1173
1270
  catch (err) {
1174
1271
  p.log.error(`Failed to start web server: ${err.message}`);
@@ -1176,6 +1273,27 @@ export async function main() {
1176
1273
  }
1177
1274
  continue;
1178
1275
  }
1276
+ if (cmd === '/theme' || cmd === '/themes') {
1277
+ const themeArg = answer.replace(/^\/(?:theme|themes)\s*/i, '').trim();
1278
+ if (themeArg) {
1279
+ const matched = findTheme(themeArg);
1280
+ if (matched) {
1281
+ config.theme = matched.id;
1282
+ await saveConfig(config);
1283
+ const th = setActiveTheme(matched.id);
1284
+ drawLogo();
1285
+ p.log.success(th.boldFn(`šŸŽØ Theme switched to ${th.emoji} ${th.name}!`));
1286
+ }
1287
+ else {
1288
+ const themes = listThemes();
1289
+ p.log.warn(`Unknown theme: "${themeArg}". Available themes: ${themes.map(t => `${t.emoji} ${t.id}`).join(', ')}`);
1290
+ }
1291
+ }
1292
+ else {
1293
+ await handleThemeSelect(config);
1294
+ }
1295
+ continue;
1296
+ }
1179
1297
  if (cmd === '/memory') {
1180
1298
  const sub = parts[1]?.toLowerCase();
1181
1299
  if (sub === 'clear') {
@@ -1357,6 +1475,8 @@ export async function main() {
1357
1475
  }
1358
1476
  catch { }
1359
1477
  }
1478
+ const turnStartTime = Date.now();
1479
+ let toolsExecutedCount = 0;
1360
1480
  try {
1361
1481
  for await (const event of agent.run(abortController.signal)) {
1362
1482
  if (aborted)
@@ -1395,15 +1515,16 @@ export async function main() {
1395
1515
  }
1396
1516
  }
1397
1517
  else if (event.type === 'tool_generating') {
1518
+ const theme = getCurrentTheme();
1398
1519
  const label = formatToolGeneratingLabel(event.name, event.targetHint);
1399
1520
  if (!spinnerActive) {
1400
1521
  streamer.finish();
1401
1522
  finishThinking(false);
1402
- s.start(pc.bold(pc.cyan(label)));
1523
+ s.start(theme.boldFn(label));
1403
1524
  spinnerActive = true;
1404
1525
  }
1405
1526
  else {
1406
- s.message(pc.bold(pc.cyan(label)));
1527
+ s.message(theme.boldFn(label));
1407
1528
  }
1408
1529
  }
1409
1530
  else if (event.type === 'tool_start') {
@@ -1424,6 +1545,7 @@ export async function main() {
1424
1545
  }
1425
1546
  }
1426
1547
  else if (event.type === 'tool_end') {
1548
+ toolsExecutedCount++;
1427
1549
  if (spinnerActive) {
1428
1550
  s.stop();
1429
1551
  spinnerActive = false;
@@ -1435,28 +1557,50 @@ export async function main() {
1435
1557
  process.stdout.write(parsed.displayCard);
1436
1558
  }
1437
1559
  else if (parsed.action === 'edit' && parsed.diffLines) {
1560
+ const theme = getCurrentTheme();
1438
1561
  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);
1562
+ const boxWidth = Math.max(30, Math.min(cols - 4, 76));
1563
+ const fileName = typeof parsed.path === 'string' ? parsed.path.split(/[\/\\]/).pop() || 'file' : 'file';
1564
+ const fillCount = Math.max(2, boxWidth - 5 - fileName.length);
1565
+ console.log(theme.colorFn('ā”Œā”€ ') + pc.bold(fileName) + ' ' + theme.colorFn('─'.repeat(fillCount) + '┐'));
1566
+ const maxShown = Math.min(parsed.diffLines.length, 30);
1443
1567
  for (let i = 0; i < maxShown; i++) {
1444
1568
  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));
1569
+ const match = line.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
1570
+ if (match) {
1571
+ const lineNum = match[1].padStart(4, ' ');
1572
+ const symbol = match[2];
1573
+ const contentStr = match[3] || '';
1574
+ const prefix = symbol.includes('-') ? '-' : symbol.includes('+') ? '+' : ' ';
1575
+ const maxCodeLen = Math.max(10, boxWidth - 11);
1576
+ const paddedCode = contentStr.length > maxCodeLen
1577
+ ? contentStr.substring(0, maxCodeLen - 1) + '…'
1578
+ : contentStr.padEnd(maxCodeLen, ' ');
1579
+ const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
1580
+ if (prefix === '-') {
1581
+ console.log(theme.colorFn('│') + theme.diffRemoveBg(innerRow) + theme.colorFn('│'));
1582
+ }
1583
+ else if (prefix === '+') {
1584
+ console.log(theme.colorFn('│') + theme.diffAddBg(innerRow) + theme.colorFn('│'));
1585
+ }
1586
+ else {
1587
+ console.log(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
1588
+ }
1450
1589
  }
1451
1590
  else {
1452
- console.log(pc.dim('│ ') + pc.white(line));
1591
+ const maxLen = Math.max(10, boxWidth - 4);
1592
+ const padded = line.length > maxLen ? line.substring(0, maxLen - 1) + '…' : line.padEnd(maxLen, ' ');
1593
+ console.log(theme.colorFn('│ ') + pc.white(padded) + theme.colorFn(' │'));
1453
1594
  }
1454
1595
  }
1455
1596
  if (parsed.diffLines.length > maxShown) {
1456
- console.log(pc.dim(`│ ... +${parsed.diffLines.length - maxShown} more lines`));
1597
+ const dots = `... +${parsed.diffLines.length - maxShown} more lines`;
1598
+ const maxLen = Math.max(10, boxWidth - 4);
1599
+ const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + '…' : dots.padEnd(maxLen, ' ');
1600
+ console.log(theme.colorFn('│ ') + pc.dim(paddedDots) + theme.colorFn(' │'));
1457
1601
  }
1458
- console.log(pc.dim('ā””' + borderChar.repeat(boxWidth) + 'ā”˜'));
1459
- console.log(pc.green(` └─ ${parsed.summary}`));
1602
+ console.log(theme.colorFn('ā””' + '─'.repeat(boxWidth - 2) + 'ā”˜'));
1603
+ console.log(theme.boldFn(` └─ ${parsed.summary}`));
1460
1604
  }
1461
1605
  else if (parsed.summary) {
1462
1606
  console.log(pc.green(` └─ ${parsed.summary}`));
@@ -1535,6 +1679,12 @@ export async function main() {
1535
1679
  process.stdout.write('\n');
1536
1680
  }
1537
1681
  globalSnapshotManager.finishTurn();
1682
+ const turnElapsedMs = Date.now() - turnStartTime;
1683
+ const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
1684
+ if (shouldNotify && !aborted) {
1685
+ const secs = Math.round(turnElapsedMs / 1000);
1686
+ notifyDevice('devx', `Task completed in ${secs}s!`);
1687
+ }
1538
1688
  // Auto-save session
1539
1689
  await sessionManager.save(history.getMessages(), totalSessionCost, config.model, planMode);
1540
1690
  // 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.1",
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
  }