termux-dev 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/banner.svg 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.1</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.1')),
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.1')),
397
+ indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
398
+ indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
399
+ indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
400
+ indent + theme.boldFn('v1.2.0'),
397
401
  ''
398
402
  ];
399
403
  for (const line of logo) {
@@ -415,6 +419,33 @@ function formatSessionTime(timestamp) {
415
419
  const days = Math.floor(hours / 24);
416
420
  return `${days}d ago`;
417
421
  }
422
+ function formatToolGeneratingLabel(name, targetHint) {
423
+ const t = name.toLowerCase();
424
+ const target = targetHint ? ` ${targetHint}` : '';
425
+ if (t === 'read_file' || t === 'read')
426
+ return `→ Read${target || '...'}`;
427
+ if (t === 'write_file' || t === 'write')
428
+ return `→ Write${target || '...'}`;
429
+ if (t === 'edit_file' || t === 'patch')
430
+ return `→ Edit${target || '...'}`;
431
+ if (t === 'delete_file' || t === 'remove_file' || t === 'rm')
432
+ return `→ Delete${target || '...'}`;
433
+ if (t === 'bash' || t === 'run_command' || t === 'exec')
434
+ return `→ Run:${target || '...'}`;
435
+ if (t === 'search' || t === 'grep_search')
436
+ return `→ Search${target || '...'}`;
437
+ if (t === 'list_dir' || t === 'ls')
438
+ return `→ List${target || '...'}`;
439
+ if (t === 'todo_list' || t === 'update_todos')
440
+ return `→ Update Plan & Tasks...`;
441
+ if (t === 'plan_ready')
442
+ return `→ Finalize Plan...`;
443
+ if (t === 'diagnose_code')
444
+ return `→ Diagnosing code...`;
445
+ if (t === 'ask_questions')
446
+ return `→ Clarifying questions...`;
447
+ return `→ ${name}${target}...`;
448
+ }
418
449
  async function handleSessionDelete() {
419
450
  while (true) {
420
451
  const sessions = await SessionManager.listSessions();
@@ -493,6 +524,39 @@ export async function main() {
493
524
  const options = program.opts();
494
525
  let planMode = !!options.plan;
495
526
  let config = await loadConfig();
527
+ if (!config.onboarded) {
528
+ const cols = Math.min(process.stdout.columns || 40, 42);
529
+ const fill = Math.max(2, cols - 16);
530
+ const topBorder = '┌─ preview.ts ' + '─'.repeat(fill) + '┐';
531
+ const bottomBorder = '└' + '─'.repeat(cols - 2) + '┘';
532
+ const themeChoices = listThemes().map(t => {
533
+ const addLine = t.diffAddBg(` + 1 | const theme = "${t.id}";`.padEnd(cols - 2));
534
+ const remLine = t.diffRemoveBg(` - 2 | const theme = "none";`.padEnd(cols - 2));
535
+ const preview = `${t.colorFn(topBorder)}\n${addLine}\n${remLine}\n${t.colorFn(bottomBorder)}\n\n${pc.dim('💡 You can change this anytime with /theme')}`;
536
+ return {
537
+ name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)}`,
538
+ value: t.id,
539
+ description: preview
540
+ };
541
+ });
542
+ try {
543
+ console.clear();
544
+ const selected = await select({
545
+ message: `${pc.bold('🎨 Pick a theme to personalize your workspace:')}`,
546
+ choices: themeChoices,
547
+ pageSize: 10
548
+ });
549
+ if (selected) {
550
+ config.theme = selected;
551
+ }
552
+ }
553
+ catch { }
554
+ config.onboarded = true;
555
+ await saveConfig(config);
556
+ }
557
+ if (config.theme) {
558
+ setActiveTheme(config.theme);
559
+ }
496
560
  enableDarkTheme(config.pureBlackTheme !== false);
497
561
  let history = new History();
498
562
  const sysPrompt = await buildSystemPrompt(planMode);
@@ -519,6 +583,7 @@ export async function main() {
519
583
  const tokenStats = `Context: ${formatTokens(currentTokens)} / ${formatTokens(maxTokens)} (${usagePercent}%) • ${costStr}`;
520
584
  const cols = process.stdout.columns || 80;
521
585
  const modeName = planMode ? 'PLAN' : 'AGENT';
586
+ const theme = getCurrentTheme();
522
587
  // Shorten model name if too long on narrow mobile screens
523
588
  let displayModel = config.model;
524
589
  if (cols < 75 && displayModel.length > 20) {
@@ -528,7 +593,7 @@ export async function main() {
528
593
  displayModel = displayModel.slice(0, 17) + '...';
529
594
  }
530
595
  }
531
- const badge = pc.bgCyan(pc.black(` devx | ${modeName} | ${displayModel} `));
596
+ const badge = theme.badgeFn(`devx | ${modeName} | ${displayModel}`);
532
597
  if (cols < 75) {
533
598
  // 2-line layout for mobile screens: perfectly aligned with clack box borders
534
599
  p.intro(`${badge}\n${pc.dim('│')} ${pc.dim(tokenStats)}`);
@@ -541,7 +606,7 @@ export async function main() {
541
606
  if (autoTriggerPrompt) {
542
607
  answer = autoTriggerPrompt;
543
608
  autoTriggerPrompt = '';
544
- console.log(pc.cyan('◆') + ' ' + pc.bold(pc.white(answer)));
609
+ console.log(theme.colorFn('◆') + ' ' + pc.bold(pc.white(answer)));
545
610
  }
546
611
  else {
547
612
  const inputStr = await askPrompt({
@@ -582,16 +647,44 @@ export async function main() {
582
647
  if (answer.startsWith('/')) {
583
648
  const parts = answer.split(' ');
584
649
  let cmd = parts[0];
650
+ async function handleThemeSelect(config) {
651
+ const currentTh = getCurrentTheme();
652
+ const themeChoices = listThemes().map(t => ({
653
+ name: `${t.emoji} ${t.boldFn(t.name.padEnd(18))} ${pc.dim(t.desc)} ${t.id === currentTh.id ? pc.green('(Active)') : ''}`,
654
+ value: t.id,
655
+ description: `Apply ${t.name} color palette (${t.hex}) to banners, prompts, and actions`
656
+ }));
657
+ try {
658
+ const selected = await select({
659
+ message: `${pc.bold('🎨 Select UI Theme / Выберите цветовую тему:')}`,
660
+ choices: themeChoices
661
+ });
662
+ if (selected) {
663
+ config.theme = selected;
664
+ await saveConfig(config);
665
+ const th = setActiveTheme(selected);
666
+ drawLogo();
667
+ p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
668
+ }
669
+ }
670
+ catch { }
671
+ }
585
672
  async function handleSettings(config) {
586
673
  while (true) {
587
674
  try {
588
675
  const maxIter = config.maxIterations || 100;
589
676
  const maxIterLabel = maxIter >= 9999 ? 'Unlimited' : `${maxIter} steps`;
677
+ const currentTh = getCurrentTheme();
590
678
  const choice = await select({
591
- message: `${pc.bold('⚙️ Settings')} ${pc.dim('(devx v1.1.1by ApvCode)')}`,
679
+ message: `${pc.bold('⚙️ Settings')} ${pc.dim(`(devx v1.2.0theme: ${currentTh.name})`)}`,
592
680
  choices: [
593
681
  {
594
- 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')}`,
595
688
  value: 'toggle_black_theme',
596
689
  description: config.pureBlackTheme !== false
597
690
  ? 'Apply deep OLED obsidian black background (#0a0a0c) like OpenCode'
@@ -619,12 +712,12 @@ export async function main() {
619
712
  : 'Disable update checking on startup (run /update manually instead)'
620
713
  },
621
714
  {
622
- name: `🔄 Max Agent Iterations: ${pc.cyan(maxIterLabel)}`,
715
+ name: `🔄 Max Agent Iterations: ${currentTh.colorFn(maxIterLabel)}`,
623
716
  value: 'change_max_iterations',
624
717
  description: 'Limit how many tool steps (file edits, terminal commands) agent can do per request'
625
718
  },
626
719
  {
627
- name: `${pc.cyan('✨ About devx')} ${pc.dim('(v1.1.1 by ApvCode)')}`,
720
+ name: `${currentTh.colorFn('✨ About devx')} ${pc.dim('(v1.2.0 by ApvCode)')}`,
628
721
  value: 'about',
629
722
  description: 'Terminal-Native AI Coding Agent created by ApvCode (https://github.com/apvcode/Termux-Dev)'
630
723
  },
@@ -635,8 +728,13 @@ export async function main() {
635
728
  }
636
729
  ]
637
730
  });
731
+ if (choice === 'change_theme') {
732
+ await handleThemeSelect(config);
733
+ continue;
734
+ }
638
735
  if (choice === 'about') {
639
- p.note(`⚡ devx v1.1.1 — 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` +
640
738
  `👤 Author: ApvCode (https://github.com/apvcode)\n` +
641
739
  `🌟 Repository: https://github.com/apvcode/Termux-Dev\n` +
642
740
  `📜 License: MIT License (2026)\n` +
@@ -665,13 +763,13 @@ export async function main() {
665
763
  if (choice === 'toggle_memory') {
666
764
  config.enableMemory = config.enableMemory === false ? true : false;
667
765
  await saveConfig(config);
668
- p.log.success(`Project memory: ${config.enableMemory ? pc.bold(pc.green('ON (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'))}`);
669
767
  continue;
670
768
  }
671
769
  if (choice === 'toggle_check_updates') {
672
770
  config.checkUpdates = config.checkUpdates === false ? true : false;
673
771
  await saveConfig(config);
674
- p.log.success(`Check for updates 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)'))}`);
675
773
  continue;
676
774
  }
677
775
  if (choice === 'change_max_iterations') {
@@ -699,18 +797,34 @@ export async function main() {
699
797
  process.stdin.resume();
700
798
  return config;
701
799
  }
702
- const VALID_COMMANDS = ['/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
+ ];
703
808
  if (!VALID_COMMANDS.includes(cmd)) {
704
809
  const SLASH_COMMANDS = [
705
810
  { name: '/new - Start a new clean chat session', value: '/new' },
706
811
  { name: '/resume - Resume a previous chat session', value: '/resume' },
707
812
  { name: '/session del - Select and delete saved sessions', value: '/session del' },
813
+ { name: '/theme - Switch UI color theme', value: '/theme' },
814
+ { name: '/doctor - Run system & environment health diagnostics', value: '/doctor' },
708
815
  { name: '/settings - Configure permissions & auto-approval', value: '/settings' },
709
816
  { name: '/update - Check and install updates from GitHub', value: '/update' },
710
817
  { name: '/model - Switch model for current provider', value: '/model' },
711
818
  { name: '/provider - Change AI provider (Google, OpenRouter...)', value: '/provider' },
712
819
  { name: '/plan - Switch to PLAN mode (architect)', value: '/plan' },
713
820
  { name: '/agent - Switch to AGENT mode (coder)', value: '/agent' },
821
+ { name: '/serve - Start local web server for web preview', value: '/serve' },
822
+ { name: '/memory - View or edit project memory bank', value: '/memory' },
823
+ { name: '/undo - Revert last file changes made by AI', value: '/undo' },
824
+ { name: '/diff - Show git diff of modified files', value: '/diff' },
825
+ { name: '/commit - AI-generated git commit message', value: '/commit' },
826
+ { name: '/status - Show git repository status', value: '/status' },
827
+ { name: '/compact - Compact conversation context', value: '/compact' },
714
828
  { name: '/config - View current configuration', value: '/config' },
715
829
  { name: '/clear - Clear message history', value: '/clear' },
716
830
  { name: '/help - Show commands overview', value: '/help' },
@@ -1043,8 +1157,8 @@ export async function main() {
1043
1157
  initialValue: true
1044
1158
  });
1045
1159
  if (!p.isCancel(confirmed) && confirmed) {
1046
- execSync('git add -A', { stdio: 'ignore' });
1047
- execSync(`git commit -m "${commitMsg.replace(/"/g, '\\"')}"`, { stdio: 'ignore' });
1160
+ execFileSync('git', ['add', '-A'], { stdio: 'ignore' });
1161
+ execFileSync('git', ['commit', '-m', commitMsg], { stdio: 'ignore' });
1048
1162
  p.log.success(pc.bold(pc.green(`✅ Committed: ${commitMsg}`)));
1049
1163
  }
1050
1164
  else {
@@ -1124,6 +1238,10 @@ export async function main() {
1124
1238
  }
1125
1239
  continue;
1126
1240
  }
1241
+ if (cmd === '/doctor') {
1242
+ await runDoctor(config);
1243
+ continue;
1244
+ }
1127
1245
  if (cmd === '/serve') {
1128
1246
  const sub = parts[1]?.toLowerCase();
1129
1247
  if (sub === 'stop') {
@@ -1138,10 +1256,7 @@ export async function main() {
1138
1256
  const customPort = parseInt(parts[1], 10) || 3000;
1139
1257
  try {
1140
1258
  const { port, localUrl, networkUrl } = await startServer(customPort);
1141
- p.log.success(pc.bold(pc.green(`🌐 Web Server running on port ${port}!`)));
1142
- console.log(pc.cyan(` • Local: ${localUrl}`));
1143
- console.log(pc.cyan(` • Network: ${networkUrl}`));
1144
- console.log(pc.dim(' (Use /serve stop to stop the server)\n'));
1259
+ await displayServerBanner(localUrl, networkUrl);
1145
1260
  }
1146
1261
  catch (err) {
1147
1262
  p.log.error(`Failed to start web server: ${err.message}`);
@@ -1149,6 +1264,27 @@ export async function main() {
1149
1264
  }
1150
1265
  continue;
1151
1266
  }
1267
+ if (cmd === '/theme' || cmd === '/themes') {
1268
+ const themeArg = answer.replace(/^\/(?:theme|themes)\s*/i, '').trim();
1269
+ if (themeArg) {
1270
+ const matched = findTheme(themeArg);
1271
+ if (matched) {
1272
+ config.theme = matched.id;
1273
+ await saveConfig(config);
1274
+ const th = setActiveTheme(matched.id);
1275
+ drawLogo();
1276
+ p.log.success(th.boldFn(`🎨 Theme switched to ${th.emoji} ${th.name}!`));
1277
+ }
1278
+ else {
1279
+ const themes = listThemes();
1280
+ p.log.warn(`Unknown theme: "${themeArg}". Available themes: ${themes.map(t => `${t.emoji} ${t.id}`).join(', ')}`);
1281
+ }
1282
+ }
1283
+ else {
1284
+ await handleThemeSelect(config);
1285
+ }
1286
+ continue;
1287
+ }
1152
1288
  if (cmd === '/memory') {
1153
1289
  const sub = parts[1]?.toLowerCase();
1154
1290
  if (sub === 'clear') {
@@ -1330,6 +1466,8 @@ export async function main() {
1330
1466
  }
1331
1467
  catch { }
1332
1468
  }
1469
+ const turnStartTime = Date.now();
1470
+ let toolsExecutedCount = 0;
1333
1471
  try {
1334
1472
  for await (const event of agent.run(abortController.signal)) {
1335
1473
  if (aborted)
@@ -1368,15 +1506,16 @@ export async function main() {
1368
1506
  }
1369
1507
  }
1370
1508
  else if (event.type === 'tool_generating') {
1509
+ const theme = getCurrentTheme();
1510
+ const label = formatToolGeneratingLabel(event.name, event.targetHint);
1371
1511
  if (!spinnerActive) {
1372
1512
  streamer.finish();
1373
1513
  finishThinking(false);
1374
- s.start(pc.cyan(`⚡ Calling ${event.name}...`));
1514
+ s.start(theme.boldFn(label));
1375
1515
  spinnerActive = true;
1376
1516
  }
1377
1517
  else {
1378
- const chars = event.bytes > 1000 ? `${(event.bytes / 1000).toFixed(1)}k chars` : `${event.bytes} chars`;
1379
- s.message(pc.cyan(`⚡ Generating ${event.name} (${chars})...`));
1518
+ s.message(theme.boldFn(label));
1380
1519
  }
1381
1520
  }
1382
1521
  else if (event.type === 'tool_start') {
@@ -1397,6 +1536,7 @@ export async function main() {
1397
1536
  }
1398
1537
  }
1399
1538
  else if (event.type === 'tool_end') {
1539
+ toolsExecutedCount++;
1400
1540
  if (spinnerActive) {
1401
1541
  s.stop();
1402
1542
  spinnerActive = false;
@@ -1408,28 +1548,50 @@ export async function main() {
1408
1548
  process.stdout.write(parsed.displayCard);
1409
1549
  }
1410
1550
  else if (parsed.action === 'edit' && parsed.diffLines) {
1551
+ const theme = getCurrentTheme();
1411
1552
  const cols = Math.min(process.stdout.columns || 80, 80);
1412
- const borderChar = '─';
1413
- const boxWidth = Math.max(20, Math.min(cols - 4, 66));
1414
- console.log(pc.dim('┌' + borderChar.repeat(boxWidth) + '┐'));
1415
- 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);
1416
1558
  for (let i = 0; i < maxShown; i++) {
1417
1559
  const line = parsed.diffLines[i];
1418
- if (line.includes(' - ')) {
1419
- console.log(pc.dim('│ ') + pc.red(line));
1420
- }
1421
- else if (line.includes(' + ')) {
1422
- 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
+ }
1423
1580
  }
1424
1581
  else {
1425
- 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(' │'));
1426
1585
  }
1427
1586
  }
1428
1587
  if (parsed.diffLines.length > maxShown) {
1429
- 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(' │'));
1430
1592
  }
1431
- console.log(pc.dim('└' + borderChar.repeat(boxWidth) + '┘'));
1432
- console.log(pc.green(` └─ ${parsed.summary}`));
1593
+ console.log(theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘'));
1594
+ console.log(theme.boldFn(` └─ ${parsed.summary}`));
1433
1595
  }
1434
1596
  else if (parsed.summary) {
1435
1597
  console.log(pc.green(` └─ ${parsed.summary}`));
@@ -1508,6 +1670,12 @@ export async function main() {
1508
1670
  process.stdout.write('\n');
1509
1671
  }
1510
1672
  globalSnapshotManager.finishTurn();
1673
+ const turnElapsedMs = Date.now() - turnStartTime;
1674
+ const shouldNotify = (turnElapsedMs >= 15000) || (toolsExecutedCount > 0) || (planMode && lastPlanReady !== null);
1675
+ if (shouldNotify && !aborted) {
1676
+ const secs = Math.round(turnElapsedMs / 1000);
1677
+ notifyDevice('devx', `Task completed in ${secs}s!`);
1678
+ }
1511
1679
  // Auto-save session
1512
1680
  await sessionManager.save(history.getMessages(), totalSessionCost, config.model, planMode);
1513
1681
  // Check if plan was finalized in PLAN mode