agentvibes 5.0.0 → 5.1.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.
@@ -23,13 +23,13 @@ import { SUPPORTED_LANGUAGES, t } from '../../i18n/strings.js';
23
23
  import {
24
24
  PROVIDERS,
25
25
  checkClaudeInstalled, checkCopilotInstalled, checkCodexInstalled,
26
- installClaudeMcp, removeClaudeMcp,
26
+ installClaudeMcp, removeClaudeMcp, uninstallClaude,
27
27
  installCopilotMcp, removeCopilotMcp,
28
28
  installCopilotInstructions, removeCopilotInstructions,
29
29
  installCodexMcp, removeCodexMcp,
30
30
  installCodexInstructions, installCodexHooks,
31
31
  removeCodexInstructions, removeCodexHooks,
32
- loadLlmConfigSync, saveLlmConfigSync,
32
+ loadLlmConfigSync, saveLlmConfigSync, resolveCfgPath,
33
33
  } from '../../services/llm-provider-service.js';
34
34
  import {
35
35
  getAvailableEngines, getEngineStatuses, checkEngineInstalled,
@@ -38,7 +38,7 @@ import { openReverbPicker, REVERB_PRESETS } from '../widgets/reverb-picker.js';
38
38
  import { openTrackPicker, openVolumeInput } from '../widgets/track-picker.js';
39
39
  import { formatTrackName } from '../widgets/format-utils.js';
40
40
  import { destroyList } from '../widgets/destroy-list.js';
41
- import { scanInstalledVoices, getVoiceMeta, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker } from './voices-tab.js';
41
+ import { scanInstalledVoices, getVoiceMeta, genderIconTag, PIPER_VOICES_DIR, SAMPLE_PHRASES, parseMultiSpeaker, getFavorites, toggleFavorite } from './voices-tab.js';
42
42
  import { attachBtnBlink } from './agents-tab.js';
43
43
  import { buildAudioEnv, detectWavPlayer } from '../audio-env.js';
44
44
  import { spawn } from 'node:child_process';
@@ -201,7 +201,9 @@ export function createSetupTab(screen, services) {
201
201
  // Wizard state
202
202
 
203
203
  let _screen = 0;
204
- let _lastScreen = -1;
204
+ let _lastScreen = -2;
205
+ let _pendingGlobalCfg = null; // Set when global config detected on first run
206
+ let _globalChoiceIdx = 0; // 0 = Load Global, 1 = Start Fresh
205
207
  const _getLang = () => languageService?.getLang() ?? 'en';
206
208
  const _tl = (key) => languageService?.t(key) ?? t('en', key);
207
209
  let _langIdx = 0;
@@ -458,10 +460,10 @@ export function createSetupTab(screen, services) {
458
460
  // Info box for provider details
459
461
  const infoBox = blessed.box({
460
462
  parent: box,
461
- top: 1,
462
- left: 2,
463
- width: '96%',
464
- bottom: 1,
463
+ top: 0,
464
+ left: 0,
465
+ width: '100%',
466
+ bottom: 0,
465
467
  hidden: true,
466
468
  scrollable: true,
467
469
  alwaysScroll: true,
@@ -469,6 +471,7 @@ export function createSetupTab(screen, services) {
469
471
  keys: true,
470
472
  vi: true,
471
473
  mouse: true,
474
+ valign: 'top',
472
475
  scrollbar: { ch: '|', style: { fg: 'cyan' } },
473
476
  style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
474
477
  });
@@ -650,9 +653,9 @@ export function createSetupTab(screen, services) {
650
653
 
651
654
  async function handleProviderRemove(provider) {
652
655
  if (provider.id === 'claude-code') {
653
- await removeClaudeMcp(targetDir);
656
+ const result = await uninstallClaude(targetDir);
654
657
  await refreshInstalledState();
655
- showRemoveInfo('claude-code');
658
+ showRemoveInfo('claude-code', result.removed || []);
656
659
  return;
657
660
  }
658
661
 
@@ -694,10 +697,20 @@ export function createSetupTab(screen, services) {
694
697
  let _closed = false;
695
698
  navigationService?.openModal();
696
699
 
700
+ const defaultPretext = {
701
+ 'claude-code': 'Claude Code here',
702
+ 'copilot': 'Copilot here',
703
+ 'codex': 'Codex here',
704
+ };
705
+
706
+ // Read global defaults for display
707
+ const globalEngine = providerService?.getActiveProvider?.() || 'piper';
708
+ const globalVoice = providerService?.getActiveVoiceId?.() || 'none';
709
+
697
710
  const draft = {
698
711
  ttsEngine: config.ttsEngine || '',
699
712
  voice: config.voice || '',
700
- pretext: config.pretext || '',
713
+ pretext: config.pretext || defaultPretext[llmKey] || '',
701
714
  reverbPreset: config.effects || 'off',
702
715
  bgTrack: config.bgTrack || '',
703
716
  bgVolume: config.bgVolume || '0.15',
@@ -708,7 +721,7 @@ export function createSetupTab(screen, services) {
708
721
  top: 'center',
709
722
  left: 'center',
710
723
  width: 72,
711
- height: 21,
724
+ height: 22,
712
725
  border: { type: 'line' },
713
726
  tags: true,
714
727
  label: ` {bold}{cyan-fg} ${provider.name} -- Audio Config {/cyan-fg}{/bold} `,
@@ -722,8 +735,8 @@ export function createSetupTab(screen, services) {
722
735
 
723
736
  // Field definitions
724
737
  const FIELDS = [
725
- { key: 'ttsEngine', label: 'TTS Engine', getValue: () => draft.ttsEngine || '(global default)' },
726
- { key: 'voice', label: 'Voice', getValue: () => draft.voice || '(global default)' },
738
+ { key: 'ttsEngine', label: 'TTS Engine', getValue: () => draft.ttsEngine || `(global: ${globalEngine})` },
739
+ { key: 'voice', label: 'Voice', getValue: () => draft.voice || `(global: ${globalVoice})` },
727
740
  { key: 'pretext', label: 'Pretext', getValue: () => draft.pretext || '(none)' },
728
741
  { key: 'reverb', label: 'Reverb', getValue: () => {
729
742
  const p = REVERB_PRESETS.find(r => r.value === draft.reverbPreset);
@@ -770,7 +783,7 @@ export function createSetupTab(screen, services) {
770
783
  left: 2,
771
784
  right: 2,
772
785
  tags: true,
773
- content: '{white-fg}[Up/Down] Navigate [Enter] Edit [Tab] Save/Cancel [Esc] Cancel{/white-fg}',
786
+ content: '{white-fg}[Up/Down] Navigate [Enter] Edit [Tab] Buttons [Esc] Close{/white-fg}',
774
787
  style: { bg: COLORS.contentBg },
775
788
  });
776
789
 
@@ -797,40 +810,139 @@ export function createSetupTab(screen, services) {
797
810
  return btn;
798
811
  }
799
812
 
800
- const saveBtn = _modalBtn('Save', 4, () => {
813
+ // Preview status line
814
+ const previewLine = blessed.text({
815
+ parent: modal,
816
+ bottom: 1,
817
+ left: 2,
818
+ right: 2,
819
+ tags: true,
820
+ content: '',
821
+ style: { bg: COLORS.contentBg },
822
+ });
823
+
824
+ let _previewModalProc = null;
825
+ function _killPreview() {
826
+ if (_previewModalProc) {
827
+ try { _previewModalProc.kill(); } catch {}
828
+ _previewModalProc = null;
829
+ }
830
+ }
831
+
832
+ function _playPreview() {
833
+ _killPreview();
834
+ previewLine.setContent('{cyan-fg}♪ Previewing...{/cyan-fg}');
835
+ screen.render();
836
+
837
+ // Save first so play-tts picks up current settings
838
+ _autoSave(true);
839
+
840
+ // Temporarily enable background music for preview if a track is configured
841
+ const hasBgTrack = !!draft.bgTrack;
842
+ let _bgRestore = null;
843
+ if (hasBgTrack) {
844
+ const avCfgPath = path.join(targetDir, '.agentvibes', 'config.json');
845
+ try {
846
+ const raw = fs.readFileSync(avCfgPath, 'utf8');
847
+ const cfg = JSON.parse(raw);
848
+ if (cfg.backgroundMusic && !cfg.backgroundMusic.enabled) {
849
+ cfg.backgroundMusic.enabled = true;
850
+ fs.writeFileSync(avCfgPath, JSON.stringify(cfg, null, 2) + '\n');
851
+ _bgRestore = () => { cfg.backgroundMusic.enabled = false; fs.writeFileSync(avCfgPath, JSON.stringify(cfg, null, 2) + '\n'); };
852
+ }
853
+ } catch {
854
+ // No .agentvibes/config.json — use legacy txt fallback
855
+ const bgEnabledFile = path.join(targetDir, '.claude', 'config', 'background-music-enabled.txt');
856
+ let bgWas = false;
857
+ try { bgWas = fs.readFileSync(bgEnabledFile, 'utf8').trim() === 'true'; } catch {}
858
+ if (!bgWas) {
859
+ try { fs.writeFileSync(bgEnabledFile, 'true', 'utf8'); } catch {}
860
+ _bgRestore = () => { try { fs.writeFileSync(bgEnabledFile, 'false', 'utf8'); } catch {} };
861
+ }
862
+ }
863
+ }
864
+
865
+ const hooksSubdir = process.platform === 'win32' ? 'hooks-windows' : 'hooks';
866
+ const isWin = process.platform === 'win32' && !process.env.WSL_DISTRO_NAME;
867
+ // Don't include pretext — play-tts already prepends it from the config
868
+ const sampleText = 'Here is a preview of your audio settings.';
869
+
870
+ let cmd, args;
871
+ if (isWin) {
872
+ const script = path.join(targetDir, '.claude', hooksSubdir, 'play-tts.ps1');
873
+ cmd = 'powershell';
874
+ args = ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, sampleText, '', '-llm', llmKey];
875
+ } else {
876
+ const script = path.join(targetDir, '.claude', hooksSubdir, 'play-tts.sh');
877
+ cmd = 'bash';
878
+ args = [script, sampleText, '', '--llm', llmKey];
879
+ }
880
+
881
+ const proc = spawn(cmd, args, {
882
+ stdio: 'ignore',
883
+ windowsHide: true,
884
+ env: { ...process.env, CLAUDE_PROJECT_DIR: targetDir },
885
+ });
886
+ _previewModalProc = proc;
887
+
888
+ const _restoreBg = () => { if (_bgRestore) _bgRestore(); };
889
+
890
+ proc.on('exit', () => {
891
+ _previewModalProc = null;
892
+ _restoreBg();
893
+ if (!_closed) { previewLine.setContent(''); screen.render(); }
894
+ });
895
+ proc.on('error', () => {
896
+ _previewModalProc = null;
897
+ _restoreBg();
898
+ if (!_closed) { previewLine.setContent('{red-fg}Preview failed{/red-fg}'); screen.render(); }
899
+ });
900
+ }
901
+
902
+ // Auto-save: persist draft to config immediately on any change
903
+ function _autoSave(silent) {
904
+ // Infer engine from voice — voice picker only shows Piper voices,
905
+ // so if a voice is set but no engine chosen, default to piper
906
+ const engine = draft.ttsEngine || (draft.voice ? 'piper' : '');
801
907
  saveLlmConfigSync(llmKey, {
802
908
  voice: draft.voice,
803
909
  pretext: draft.pretext,
804
910
  effects: draft.reverbPreset === 'off' ? '' : draft.reverbPreset,
805
911
  bgTrack: draft.bgTrack,
806
912
  bgVolume: draft.bgVolume,
807
- ttsEngine: draft.ttsEngine,
913
+ ttsEngine: engine,
808
914
  sourcePath: config.sourcePath,
809
915
  }, targetDir);
810
- _closeModal();
811
- _showSavedToast(provider.name);
812
- });
916
+ if (!silent) {
917
+ const cfgPath = config.sourcePath || resolveCfgPath(targetDir);
918
+ _showSavedToast('Settings', cfgPath);
919
+ }
920
+ }
813
921
 
814
- const resetBtn = _modalBtn('Reset', 16, () => {
922
+ const previewBtn = _modalBtn('Preview', 4, _playPreview);
923
+
924
+ const resetBtn = _modalBtn('Reset', 18, () => {
815
925
  draft.ttsEngine = '';
816
926
  draft.voice = '';
817
- draft.pretext = '';
927
+ draft.pretext = defaultPretext[llmKey] || '';
818
928
  draft.reverbPreset = 'off';
819
929
  draft.bgTrack = '';
820
930
  draft.bgVolume = '0.15';
931
+ _autoSave();
821
932
  fieldList.setItems(_fieldItems());
822
933
  fieldList.focus();
823
934
  screen.render();
824
935
  });
825
936
 
826
- const cancelBtn = _modalBtn('Cancel', 30, _closeModal);
937
+ const closeBtn = _modalBtn('Close', 30, _closeModal);
827
938
 
828
- const allBtns = [saveBtn, resetBtn, cancelBtn];
939
+ const allBtns = [previewBtn, resetBtn, closeBtn];
829
940
  const btnBlink = attachBtnBlink(allBtns, screen);
830
941
 
831
942
  function _closeModal() {
832
943
  if (_closed) return;
833
944
  _closed = true;
945
+ _killPreview();
834
946
  btnBlink.cleanup();
835
947
  navigationService?.closeModal();
836
948
  destroyList(modal, screen);
@@ -845,6 +957,7 @@ export function createSetupTab(screen, services) {
845
957
  if (!field) return;
846
958
 
847
959
  const _refreshField = () => {
960
+ _autoSave();
848
961
  fieldList.setItems(_fieldItems());
849
962
  fieldList.select(idx);
850
963
  fieldList.focus();
@@ -1021,7 +1134,6 @@ export function createSetupTab(screen, services) {
1021
1134
  navigationService?.openModal();
1022
1135
 
1023
1136
  let _allVoices = [];
1024
- let _filterText = '';
1025
1137
  let _previewProc = null;
1026
1138
  let _previewVoiceId = null;
1027
1139
  let _vpClosed = false;
@@ -1060,28 +1172,17 @@ export function createSetupTab(screen, services) {
1060
1172
  });
1061
1173
  vpModal.setFront();
1062
1174
 
1063
- // Search
1064
- blessed.text({
1065
- parent: vpModal, top: 1, left: 2,
1066
- content: 'Search:', style: { fg: COLORS.labelFg, bg: COLORS.contentBg },
1067
- });
1068
- const vpSearch = blessed.textbox({
1069
- parent: vpModal, top: 1, left: 11, width: 40, height: 1,
1070
- inputOnFocus: true, keys: true,
1071
- style: { fg: COLORS.valueFg, bg: 'blue', focus: { bg: 'cyan' } },
1072
- });
1073
-
1074
1175
  // Column header
1075
- const COL_N = 28;
1076
- const COL_G = 10;
1176
+ const COL_N = 30;
1177
+ const COL_G = 4;
1077
1178
  blessed.text({
1078
- parent: vpModal, top: 2, left: 6, tags: true,
1079
- content: `{cyan-fg}${'Name'.padEnd(COL_N)}${'Gender'.padEnd(COL_G)}Provider{/cyan-fg}`,
1179
+ parent: vpModal, top: 1, left: 6, tags: true,
1180
+ content: `{cyan-fg}${'Name'.padEnd(COL_N)}{/cyan-fg}{magenta-fg}♀{/magenta-fg}/{bright-cyan-fg}♂{/bright-cyan-fg} {cyan-fg}Provider{/cyan-fg}`,
1080
1181
  style: { bg: COLORS.contentBg },
1081
1182
  });
1082
1183
 
1083
1184
  const vpList = blessed.list({
1084
- parent: vpModal, top: 3, left: 2, right: 2, bottom: 5,
1185
+ parent: vpModal, top: 2, left: 2, right: 2, bottom: 5,
1085
1186
  keys: true, vi: true, mouse: true,
1086
1187
  border: { type: 'line' },
1087
1188
  scrollbar: { ch: '|', style: { fg: 'cyan' } },
@@ -1101,26 +1202,24 @@ export function createSetupTab(screen, services) {
1101
1202
 
1102
1203
  blessed.text({
1103
1204
  parent: vpModal, bottom: 2, left: 2, right: 2, tags: true,
1104
- content: '{white-fg}[↑↓/jk] Navigate [Enter] Select [Space] Preview [/] Search [Esc] Cancel{/white-fg}',
1205
+ content: '{white-fg}[↑↓] Nav [PgUp/PgDn] Page [Home/End] [a-z] Jump [Enter] Select [Space] Preview [*] Fav [Esc] Cancel{/white-fg}',
1105
1206
  style: { bg: COLORS.contentBg },
1106
1207
  });
1107
1208
 
1108
- function _getFiltered() {
1109
- if (!_filterText) return _allVoices;
1110
- const f = _filterText.toLowerCase();
1111
- return _allVoices.filter(v => v.toLowerCase().includes(f));
1112
- }
1113
-
1114
1209
  function _buildVoiceItems(voices) {
1210
+ const favs = getFavorites(configService);
1115
1211
  return voices.map(v => {
1116
1212
  const isActive = v === draft.voice;
1117
1213
  const isPrev = v === _previewVoiceId;
1214
+ const isFav = favs.includes(v);
1118
1215
  const dot = isPrev ? '♪' : (isActive ? '●' : ' ');
1216
+ const star = isFav ? '{yellow-fg}★{/yellow-fg}' : ' ';
1119
1217
  const meta = getVoiceMeta(v);
1120
1218
  const name = meta.displayName.length > COL_N
1121
1219
  ? meta.displayName.slice(0, COL_N - 1) + '…'
1122
1220
  : meta.displayName.padEnd(COL_N);
1123
- return ` ${dot} ${name}${meta.gender.padEnd(COL_G)}${meta.provider}`;
1221
+ // genderIconTag has invisible color tags — pad with literal spaces (1 visible char + 3 spaces = 4)
1222
+ return ` ${dot}${star} ${name}${genderIconTag(meta.gender)} ${meta.provider}`;
1124
1223
  });
1125
1224
  }
1126
1225
 
@@ -1129,8 +1228,10 @@ export function createSetupTab(screen, services) {
1129
1228
  const savedIdx = vpList.selected ?? 0;
1130
1229
  const savedScroll = vpList.childBase ?? 0;
1131
1230
  _allVoices = scanInstalledVoices();
1132
- const filtered = _getFiltered();
1133
- const items = _buildVoiceItems(filtered);
1231
+ // Sort by display name so the first-letter quick jump is intuitive
1232
+ _allVoices.sort((a, b) => getVoiceMeta(a).displayName.localeCompare(
1233
+ getVoiceMeta(b).displayName, undefined, { sensitivity: 'base' }));
1234
+ const items = _buildVoiceItems(_allVoices);
1134
1235
  vpList.setItems(items.length > 0 ? items : [' (no voices found)']);
1135
1236
  vpList.select(Math.min(savedIdx, items.length - 1));
1136
1237
  vpList.childBase = Math.min(savedScroll, Math.max(0, items.length - (vpList.height - 2)));
@@ -1198,25 +1299,45 @@ export function createSetupTab(screen, services) {
1198
1299
  piper.on('error', () => { _previewProc = null; _previewVoiceId = null; });
1199
1300
  }
1200
1301
 
1201
- vpSearch.on('keypress', () => {
1202
- setTimeout(() => { _filterText = vpSearch.getValue().trim(); _refreshVP(); }, 0);
1203
- });
1204
- vpSearch.key(['escape'], () => { vpList.focus(); screen.render(); });
1205
- vpList.key(['/'], () => { vpSearch.clearValue(); vpSearch.focus(); screen.render(); });
1206
1302
  vpList.key(['enter'], () => {
1207
- const filtered = _getFiltered();
1208
- const sel = filtered[vpList.selected];
1303
+ const sel = _allVoices[vpList.selected];
1209
1304
  if (sel) { draft.voice = sel; _closeVP(); }
1210
1305
  });
1211
1306
  vpList.key(['space'], () => {
1212
- const filtered = _getFiltered();
1213
- const sel = filtered[vpList.selected];
1307
+ const sel = _allVoices[vpList.selected];
1214
1308
  if (sel) _previewVoice(sel);
1215
1309
  });
1310
+ vpList.key(['*'], () => {
1311
+ const sel = _allVoices[vpList.selected];
1312
+ if (sel) { toggleFavorite(configService, sel); _refreshVP(); }
1313
+ });
1216
1314
  vpList.key(['escape', 'q'], _closeVP);
1217
1315
 
1316
+ // PageUp / PageDown / Home / End navigation
1317
+ const _pageSize = () => Math.max(1, (vpList.height ?? 10) - 2);
1318
+ vpList.key(['pageup'], () => { vpList.up(_pageSize()); screen.render(); });
1319
+ vpList.key(['pagedown'], () => { vpList.down(_pageSize()); screen.render(); });
1320
+ vpList.key(['home'], () => { vpList.select(0); screen.render(); });
1321
+ vpList.key(['end'], () => { vpList.select(Math.max(0, _allVoices.length - 1)); screen.render(); });
1322
+
1323
+ // First-letter quick jump: typing 'a' jumps to the first voice starting
1324
+ // with A. Block keys reserved by the list widget (vi nav, cancel) so
1325
+ // they don't get swallowed: q (cancel), j/k/g/h/l (vi navigation).
1326
+ const _vpJumpBlocked = new Set(['j', 'k', 'g', 'h', 'l', 'q']);
1327
+ vpList.on('keypress', (ch, key) => {
1328
+ if (!ch || key?.ctrl || key?.meta) return;
1329
+ if (!/^[a-zA-Z]$/.test(ch)) return;
1330
+ const target = ch.toLowerCase();
1331
+ if (_vpJumpBlocked.has(target)) return;
1332
+ const idx = _allVoices.findIndex(v => {
1333
+ const name = getVoiceMeta(v).displayName.toLowerCase();
1334
+ return name.startsWith(target);
1335
+ });
1336
+ if (idx >= 0) { vpList.select(idx); screen.render(); }
1337
+ });
1338
+
1218
1339
  _refreshVP();
1219
- const activeIdx = _getFiltered().indexOf(draft.voice);
1340
+ const activeIdx = _allVoices.indexOf(draft.voice);
1220
1341
  if (activeIdx >= 0) vpList.select(activeIdx);
1221
1342
  vpList.focus();
1222
1343
  screen.render();
@@ -1272,16 +1393,19 @@ export function createSetupTab(screen, services) {
1272
1393
 
1273
1394
  // ── Saved toast ───────────────────────────────────────────────────────────
1274
1395
 
1275
- function _showSavedToast(name) {
1396
+ function _showSavedToast(name, filePath) {
1397
+ const lines = [`{center}{green-fg}{bold}${name} saved!{/bold}{/green-fg}{/center}`];
1398
+ if (filePath) lines.push(`{center}{white-fg}${filePath}{/white-fg}{/center}`);
1399
+ const w = filePath ? Math.min(Math.max(filePath.length + 6, 30), 70) : 30;
1276
1400
  const toast = blessed.box({
1277
1401
  parent: screen,
1278
1402
  top: 'center',
1279
1403
  left: 'center',
1280
- width: 30,
1281
- height: 3,
1404
+ width: w,
1405
+ height: filePath ? 4 : 3,
1282
1406
  border: { type: 'line' },
1283
1407
  tags: true,
1284
- content: `{center}{green-fg}{bold}${name} saved!{/bold}{/green-fg}{/center}`,
1408
+ content: lines.join('\n'),
1285
1409
  style: { bg: COLORS.contentBg, border: { fg: 'green' } },
1286
1410
  });
1287
1411
  toast.setFront();
@@ -1354,10 +1478,11 @@ export function createSetupTab(screen, services) {
1354
1478
  lines.push('');
1355
1479
  lines.push(' {yellow-fg}4.{/yellow-fg} {bold}.claude/config/{/bold} (personality, verbosity, voice settings)');
1356
1480
  lines.push('');
1357
- lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1481
+ lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1358
1482
 
1359
1483
  infoBox.setContent(lines.join('\n'));
1360
1484
  infoBox.show();
1485
+ infoBox.setFront();
1361
1486
  infoBox.focus();
1362
1487
  infoBox.scrollTo(0);
1363
1488
  screen.render();
@@ -1382,10 +1507,11 @@ export function createSetupTab(screen, services) {
1382
1507
  lines.push(' {yellow-fg}1.{/yellow-fg} {bold}.vscode/mcp.json{/bold}');
1383
1508
  lines.push(' {yellow-fg}2.{/yellow-fg} {bold}.github/copilot-instructions.md{/bold}');
1384
1509
  lines.push('');
1385
- lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1510
+ lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1386
1511
 
1387
1512
  infoBox.setContent(lines.join('\n'));
1388
1513
  infoBox.show();
1514
+ infoBox.setFront();
1389
1515
  infoBox.focus();
1390
1516
  infoBox.scrollTo(0);
1391
1517
  screen.render();
@@ -1412,25 +1538,35 @@ export function createSetupTab(screen, services) {
1412
1538
  lines.push(' {yellow-fg}3.{/yellow-fg} {bold}AGENTS.md{/bold}');
1413
1539
  lines.push(' {yellow-fg}4.{/yellow-fg} {bold}.codex/hooks/{/bold}');
1414
1540
  lines.push('');
1415
- lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1541
+ lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1416
1542
 
1417
1543
  infoBox.setContent(lines.join('\n'));
1418
1544
  infoBox.show();
1545
+ infoBox.setFront();
1419
1546
  infoBox.focus();
1420
1547
  infoBox.scrollTo(0);
1421
1548
  screen.render();
1422
1549
  }
1423
1550
 
1424
- function showRemoveInfo(providerId) {
1551
+ function showRemoveInfo(providerId, removedItems) {
1425
1552
  providerView = 'info';
1426
1553
  hideAllProviderRows();
1427
1554
  contentBox.hide();
1428
1555
 
1429
1556
  const lines = [];
1430
1557
  if (providerId === 'claude-code') {
1431
- lines.push('{bold}{cyan-fg}Remove Claude Code Integration{/cyan-fg}{/bold}');
1558
+ lines.push('{bold}{cyan-fg}Claude Code -- Uninstalled{/cyan-fg}{/bold}');
1559
+ lines.push('');
1560
+ lines.push('{green-fg}AgentVibes fully removed from this project!{/green-fg}');
1432
1561
  lines.push('');
1433
- lines.push('To remove, run: {yellow-fg}npx agentvibes uninstall{/yellow-fg}');
1562
+ if (removedItems && removedItems.length > 0) {
1563
+ lines.push('{bold}{cyan-fg}Removed:{/cyan-fg}{/bold}');
1564
+ for (const item of removedItems) {
1565
+ lines.push(` {yellow-fg}•{/yellow-fg} ${item}`);
1566
+ }
1567
+ lines.push('');
1568
+ }
1569
+ lines.push('{white-fg}Re-install anytime with the Install button.{/white-fg}');
1434
1570
  } else if (providerId === 'github-copilot') {
1435
1571
  lines.push('{bold}{cyan-fg}GitHub Copilot -- Removed{/cyan-fg}{/bold}');
1436
1572
  lines.push('');
@@ -1441,10 +1577,11 @@ export function createSetupTab(screen, services) {
1441
1577
  lines.push('{green-fg}Successfully removed!{/green-fg}');
1442
1578
  }
1443
1579
  lines.push('');
1444
- lines.push('{white-fg}Press {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1580
+ lines.push('{white-fg}Press {bold}Enter{/bold} or {bold}Escape{/bold} to return to the provider list.{/white-fg}');
1445
1581
 
1446
1582
  infoBox.setContent(lines.join('\n'));
1447
1583
  infoBox.show();
1584
+ infoBox.setFront();
1448
1585
  infoBox.focus();
1449
1586
  infoBox.scrollTo(0);
1450
1587
  screen.render();
@@ -1460,7 +1597,7 @@ export function createSetupTab(screen, services) {
1460
1597
  screen.render();
1461
1598
  }
1462
1599
 
1463
- infoBox.key(['escape'], () => {
1600
+ infoBox.key(['escape', 'enter'], () => {
1464
1601
  showProviderListView();
1465
1602
  });
1466
1603
 
@@ -1597,8 +1734,41 @@ export function createSetupTab(screen, services) {
1597
1734
  }
1598
1735
 
1599
1736
  function _renderScreen3() {
1600
- // Mark setup as completed once user reaches the providers screen
1601
- try { configService.set('setupCompleted', true); } catch {}
1737
+ // Mark setup as completed write to targetDir in case configService
1738
+ // has a different projectRoot (e.g. npm link resolves differently).
1739
+ // Each step is wrapped individually so a partial failure (e.g. corrupt
1740
+ // local config file) does not block the others — and errors are logged
1741
+ // to stderr so the user can see why setup keeps re-running.
1742
+ try { configService.set('setupCompleted', true); }
1743
+ catch (e) { console.error('setupCompleted (project): ' + e.message); }
1744
+ try { configService.setGlobal?.('setupCompleted', true); }
1745
+ catch (e) { console.error('setupCompleted (global): ' + e.message); }
1746
+
1747
+ try {
1748
+ const localCfgDir = path.join(targetDir, '.agentvibes');
1749
+ const localCfgPath = path.join(localCfgDir, 'config.json');
1750
+ if (!fs.existsSync(localCfgPath)) {
1751
+ fs.mkdirSync(localCfgDir, { recursive: true });
1752
+ fs.writeFileSync(localCfgPath, JSON.stringify({ setupCompleted: true }, null, 2));
1753
+ } else {
1754
+ let existing = {};
1755
+ try {
1756
+ existing = JSON.parse(fs.readFileSync(localCfgPath, 'utf8'));
1757
+ } catch (e) {
1758
+ // Corrupt JSON — back up the old file and start fresh so the user
1759
+ // doesn't get stuck in an endless setup loop.
1760
+ console.error(`setupCompleted: ${localCfgPath} is corrupt (${e.message}); rewriting`);
1761
+ try { fs.renameSync(localCfgPath, localCfgPath + '.bak'); } catch {}
1762
+ existing = {};
1763
+ }
1764
+ if (!existing.setupCompleted) {
1765
+ existing.setupCompleted = true;
1766
+ fs.writeFileSync(localCfgPath, JSON.stringify(existing, null, 2));
1767
+ }
1768
+ }
1769
+ } catch (e) {
1770
+ console.error('setupCompleted (local file): ' + e.message);
1771
+ }
1602
1772
 
1603
1773
  // Show provider rows instead of contentBox
1604
1774
  contentBox.hide();
@@ -1659,6 +1829,7 @@ export function createSetupTab(screen, services) {
1659
1829
  setTimeout(() => {
1660
1830
  if (_screen !== targetScreen) return;
1661
1831
  switch (_screen) {
1832
+ case -1: _renderScreenGlobal(); break;
1662
1833
  case 0: _renderScreen0(); break;
1663
1834
  case 1: _renderScreen1(); break;
1664
1835
  case 2: _renderScreen2(); break;
@@ -1668,6 +1839,7 @@ export function createSetupTab(screen, services) {
1668
1839
  return;
1669
1840
  }
1670
1841
  switch (_screen) {
1842
+ case -1: _renderScreenGlobal(); break;
1671
1843
  case 0: _renderScreen0(); break;
1672
1844
  case 1: _renderScreen1(); break;
1673
1845
  case 2: _renderScreen2(); break;
@@ -1680,7 +1852,20 @@ export function createSetupTab(screen, services) {
1680
1852
  // =========================================================================
1681
1853
 
1682
1854
  screen.key(['enter'], () => {
1683
- if (box.hidden || _checking) return;
1855
+ if (box.hidden || _checking || navigationService?.isModalOpen()) return;
1856
+ if (_screen === -1) {
1857
+ // Global config choice screen
1858
+ if (_globalChoiceIdx === 0) {
1859
+ try { configService.saveAllToLocal(_pendingGlobalCfg); } catch {}
1860
+ _screen = 3;
1861
+ } else {
1862
+ _screen = 0;
1863
+ _langIdx = 0;
1864
+ }
1865
+ _pendingGlobalCfg = null;
1866
+ _showCurrentScreen();
1867
+ return;
1868
+ }
1684
1869
  if (_screen === 0) {
1685
1870
  if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
1686
1871
  _screen = 1;
@@ -1693,7 +1878,11 @@ export function createSetupTab(screen, services) {
1693
1878
  });
1694
1879
 
1695
1880
  screen.key(['escape'], () => {
1696
- if (box.hidden || _checking) return;
1881
+ if (box.hidden || _checking || navigationService?.isModalOpen()) return;
1882
+ if (_screen === -1) {
1883
+ setTimeout(() => navigationService?.switchTab('settings'), 0);
1884
+ return;
1885
+ }
1697
1886
  if (_screen === 3 && providerView === 'info') {
1698
1887
  showProviderListView();
1699
1888
  return;
@@ -1707,7 +1896,12 @@ export function createSetupTab(screen, services) {
1707
1896
  });
1708
1897
 
1709
1898
  screen.key(['up'], () => {
1710
- if (box.hidden) return;
1899
+ if (box.hidden || navigationService?.isModalOpen()) return;
1900
+ if (_screen === -1) {
1901
+ _globalChoiceIdx = 0;
1902
+ _renderScreenGlobal();
1903
+ return;
1904
+ }
1711
1905
  if (_screen === 0) {
1712
1906
  _langIdx = Math.max(0, _langIdx - 1);
1713
1907
  _renderScreen0();
@@ -1716,7 +1910,8 @@ export function createSetupTab(screen, services) {
1716
1910
  });
1717
1911
 
1718
1912
  screen.key(['left'], () => {
1719
- if (box.hidden || _checking) return;
1913
+ if (box.hidden || _checking || navigationService?.isModalOpen()) return;
1914
+ if (_screen === -1) return;
1720
1915
  if (_screen === 3) return; // Left handled by button nav
1721
1916
  if (_screen > 0) {
1722
1917
  _screen--;
@@ -1725,7 +1920,8 @@ export function createSetupTab(screen, services) {
1725
1920
  });
1726
1921
 
1727
1922
  screen.key(['right'], () => {
1728
- if (box.hidden || _checking) return;
1923
+ if (box.hidden || _checking || navigationService?.isModalOpen()) return;
1924
+ if (_screen === -1) return;
1729
1925
  if (_screen === 0) {
1730
1926
  if (languageService) languageService.setLang(SUPPORTED_LANGUAGES[_langIdx].value);
1731
1927
  _screen = 1;
@@ -1738,7 +1934,12 @@ export function createSetupTab(screen, services) {
1738
1934
  });
1739
1935
 
1740
1936
  screen.key(['down'], () => {
1741
- if (box.hidden) return;
1937
+ if (box.hidden || navigationService?.isModalOpen()) return;
1938
+ if (_screen === -1) {
1939
+ _globalChoiceIdx = 1;
1940
+ _renderScreenGlobal();
1941
+ return;
1942
+ }
1742
1943
  if (_screen === 0) {
1743
1944
  _langIdx = Math.min(SUPPORTED_LANGUAGES.length - 1, _langIdx + 1);
1744
1945
  _renderScreen0();
@@ -1746,6 +1947,49 @@ export function createSetupTab(screen, services) {
1746
1947
  }
1747
1948
  });
1748
1949
 
1950
+ // =========================================================================
1951
+ // Screen -1: Global Config Detection (pre-wizard)
1952
+ // =========================================================================
1953
+
1954
+ function _renderScreenGlobal() {
1955
+ const cfg = _pendingGlobalCfg || {};
1956
+ const cfgPath = configService?.getGlobalConfigPath?.() || '~/.agentvibes/config.json';
1957
+
1958
+ // Build settings preview
1959
+ const voice = cfg.voice || '(default)';
1960
+ const lang = cfg.language || 'en';
1961
+ const ttsEngine = cfg.ttsEngine || '(auto)';
1962
+ const verbosity = cfg.verbosity || 'high';
1963
+ const personality = cfg.personality || 'none';
1964
+
1965
+ const sel0 = _globalChoiceIdx === 0;
1966
+ const sel1 = _globalChoiceIdx === 1;
1967
+ const hi = '{magenta-bg}{white-fg}';
1968
+ const lo = '{/white-fg}{/magenta-bg}';
1969
+
1970
+ const lines = [
1971
+ _HDR('', 'Global Settings Found'),
1972
+ '',
1973
+ ` {white-fg}Location:{/white-fg} {yellow-fg}${cfgPath}{/yellow-fg}`,
1974
+ '',
1975
+ ` {cyan-fg}Voice:{/cyan-fg} {yellow-fg}${voice}{/yellow-fg}`,
1976
+ ` {cyan-fg}Language:{/cyan-fg} {yellow-fg}${lang}{/yellow-fg}`,
1977
+ ` {cyan-fg}TTS Engine:{/cyan-fg} {yellow-fg}${ttsEngine}{/yellow-fg}`,
1978
+ ` {cyan-fg}Verbosity:{/cyan-fg} {yellow-fg}${verbosity}{/yellow-fg}`,
1979
+ ` {cyan-fg}Personality:{/cyan-fg}{yellow-fg} ${personality}{/yellow-fg}`,
1980
+ '',
1981
+ ' {white-fg}What would you like to do for this project?{/white-fg}',
1982
+ '',
1983
+ ` ${sel0 ? hi : ''}> Load Global Settings${sel0 ? lo : ''} {white-fg}— use these settings for this project{/white-fg}`,
1984
+ ` ${sel1 ? hi : ''}> Start Fresh${sel1 ? lo : ''} {white-fg}— run the full setup wizard from scratch{/white-fg}`,
1985
+ '',
1986
+ ];
1987
+ contentBox.setContent(_c(lines));
1988
+ hintLine.setContent(' [Up/Down] Choose | [Enter] Select');
1989
+ box.focus();
1990
+ screen.render();
1991
+ }
1992
+
1749
1993
  // =========================================================================
1750
1994
  // Tab Component Contract
1751
1995
  // =========================================================================
@@ -1754,16 +1998,32 @@ export function createSetupTab(screen, services) {
1754
1998
  box,
1755
1999
 
1756
2000
  show() {
1757
- // If not first run, skip directly to Screen 3 (providers)
1758
- if (!_isFirstRun()) {
2001
+ _lastScreen = -1;
2002
+ providerView = 'list';
2003
+ box.show();
2004
+
2005
+ // Detect if AgentVibes is already installed in the target directory
2006
+ // (e.g. user ran install, closed TUI, came back)
2007
+ const alreadyInstalled = fs.existsSync(path.join(targetDir, '.claude', 'commands', 'agent-vibes'));
2008
+
2009
+ // Check: no local config but global exists with setupCompleted
2010
+ const hasLocal = configService?.hasLocalConfig?.();
2011
+ const globalCfg = configService?.getGlobalConfig?.() ?? {};
2012
+ if (!alreadyInstalled && !hasLocal && globalCfg.setupCompleted) {
2013
+ _pendingGlobalCfg = globalCfg;
2014
+ _screen = -1;
2015
+ _showCurrentScreen();
2016
+ screen.render();
2017
+ return;
2018
+ }
2019
+
2020
+ // If already installed or not first run, skip directly to Screen 3 (providers)
2021
+ if (alreadyInstalled || !_isFirstRun()) {
1759
2022
  _screen = 3;
1760
2023
  } else {
1761
2024
  _screen = 0;
1762
2025
  _langIdx = 0;
1763
2026
  }
1764
- _lastScreen = -1;
1765
- providerView = 'list';
1766
- box.show();
1767
2027
  _showCurrentScreen();
1768
2028
  screen.render();
1769
2029
  },