@kikkimo/claude-launcher 2.5.0 → 3.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.
package/claude-launcher CHANGED
@@ -25,7 +25,7 @@ function forceStdinCleanup() {
25
25
  } catch (error) {
26
26
  // Ignore cleanup errors but log for debugging
27
27
  if (process.env.DEBUG_STDIN) {
28
- console.error('[DEBUG] forceStdinCleanup error:', error.message);
28
+ screen.debug('[DEBUG] forceStdinCleanup error: ' + error.message);
29
29
  }
30
30
  }
31
31
  }
@@ -36,12 +36,14 @@ const Menu = require('./lib/ui/menu');
36
36
  const colors = require('./lib/ui/colors');
37
37
  const { checkForUpdates, forceCheckForUpdates } = require('./lib/utils/version-checker');
38
38
  const {
39
+ simpleInput,
39
40
  waitForKey,
40
41
  promptForThirdPartyApi,
41
42
  confirmAction,
42
43
  showSuccess,
43
44
  showError,
44
- showInfo
45
+ showInfo,
46
+ selectProvider,
45
47
  } = require('./lib/ui/prompts');
46
48
  const {
47
49
  launchClaudeDefault,
@@ -50,10 +52,13 @@ const {
50
52
  launchClaudeWithApi
51
53
  } = require('./lib/launcher');
52
54
  const { getPasswordInput } = require('./lib/auth/password-input');
53
- const { verifyExportPassword, setupNewPassword, changePassword: changePasswordModule } = require('./lib/auth/password-validator');
55
+ const { passwordGuard, verifyCurrentPassword, setupNewPassword, changePassword: changePasswordModule } = require('./lib/auth/password-validator');
54
56
  const { maskApiToken } = require('./lib/validators');
55
57
  const { showApiSelectionTable, confirmDeletion } = require('./lib/ui/interactive-table');
58
+ const { editApi, editApiEnvVarsById, resolveProviderName } = require('./lib/ui/api-editor');
59
+ const { i18nLabel } = require('./lib/ui/i18n-labels');
56
60
  const i18n = require('./lib/i18n');
61
+ const screen = require('./lib/ui/screen');
57
62
  const fs = require('fs');
58
63
  const path = require('path');
59
64
  const os = require('os');
@@ -66,6 +71,7 @@ const apiManager = new ApiManager();
66
71
  let globalMainMenu = null;
67
72
  let globalConfirmMenu = null;
68
73
  let globalApiManagementMenu = null;
74
+ let globalConfigMenu = null;
69
75
 
70
76
  /**
71
77
  * Initialize global menu objects to prevent recreation and screen flickering
@@ -80,6 +86,9 @@ function initializeGlobalMenus() {
80
86
  if (!globalApiManagementMenu) {
81
87
  globalApiManagementMenu = new Menu();
82
88
  }
89
+ if (!globalConfigMenu) {
90
+ globalConfigMenu = new Menu();
91
+ }
83
92
  }
84
93
 
85
94
  /**
@@ -132,7 +141,7 @@ function openFileWithDefault(filePath) {
132
141
 
133
142
  exec(command, (error) => {
134
143
  if (error) {
135
- console.log(colors.yellow + `Could not open file automatically: ${error.message}` + colors.reset);
144
+ screen.write(colors.yellow + `Could not open file automatically: ${error.message}` + colors.reset + '\n');
136
145
  }
137
146
  });
138
147
  }
@@ -215,71 +224,653 @@ function validateImportFile(filePath) {
215
224
  // Main menu options - will be populated dynamically with i18n
216
225
  let menuOptions = [];
217
226
 
227
+ // --- Add API flow helpers ---
228
+
229
+ /**
230
+ * Input helper for add-api steps. Supports Esc / "back" to go back,
231
+ * "exit"/"quit" to cancel, empty input uses defaultValue if provided.
232
+ * @param {string} promptText
233
+ * @param {Object} opts
234
+ * @param {string} opts.defaultValue - fallback when input is empty
235
+ * @param {boolean} opts.allowEmpty - if true, empty input returns ""
236
+ * @param {string} opts.stepLabel - step header e.g. "Step 2/6"
237
+ * @param {string[]} opts.hintLines - i18n keys for hint lines (default: exit + empty_restore)
238
+ * @param {Function} opts.validate - (value) => { valid, error? }
239
+ * @returns {string|null} - input value, or null to go back
240
+ */
241
+ async function inputStepValue(promptText, { defaultValue, allowEmpty, stepLabel, hintLines, validate } = {}) {
242
+ const lines = [''];
243
+ if (stepLabel) {
244
+ lines.push(colors.bright + stepLabel + colors.reset);
245
+ }
246
+ lines.push(colors.cyan + promptText + colors.reset);
247
+ lines.push('');
248
+ const hints = hintLines || ['prompt.exit_to_cancel', 'prompt.empty_to_restore'];
249
+ for (const h of hints) {
250
+ lines.push(colors.gray + i18n.tSync(h) + colors.reset);
251
+ }
252
+ lines.push('');
253
+ screen.render(lines);
254
+
255
+ while (true) {
256
+ const input = await simpleInput(colors.green + '> ' + colors.reset);
257
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
258
+ const cancelledMsg = await i18n.t('errors.general.cancelled_by_user');
259
+ throw new Error(cancelledMsg);
260
+ }
261
+ if (input.toLowerCase() === 'back') {
262
+ return null;
263
+ }
264
+ if (input === '') {
265
+ if (allowEmpty) return '';
266
+ if (defaultValue !== undefined) return defaultValue;
267
+ }
268
+ if (validate) {
269
+ const result = validate(input);
270
+ if (!result.valid) {
271
+ screen.write(colors.red + (result.error || 'Invalid input') + colors.reset + '\n');
272
+ continue;
273
+ }
274
+ }
275
+ return input;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Rebuild draft after user goes back and re-enters connection info.
281
+ * Preserves manual overrides from oldDraft for model/runtime/custom sections.
282
+ */
283
+ function rebuildDraftPreservingOverrides(oldDraft, providerId, baseUrl, authToken, model, name) {
284
+ const draft = ApiManager.buildApiDraft(providerId, baseUrl, authToken, model, name);
285
+ if (!oldDraft) return draft;
286
+
287
+ // Preserve model overrides: if user set a value different from auto, keep it
288
+ const oldAuto = oldDraft._autoModelEnvVars || {};
289
+ for (const k of Object.keys(oldDraft.modelEnvVars || {})) {
290
+ if (oldDraft.modelEnvVars[k] !== oldAuto[k]) {
291
+ draft.modelEnvVars[k] = oldDraft.modelEnvVars[k];
292
+ }
293
+ }
294
+ // Preserve runtime manual overrides
295
+ const oldRtSources = oldDraft._runtimeEnvSources || {};
296
+ for (const k of Object.keys(oldRtSources)) {
297
+ if (oldRtSources[k] === 'manual') {
298
+ draft.runtimeEnvVars[k] = oldDraft.runtimeEnvVars[k];
299
+ draft._runtimeEnvSources[k] = 'manual';
300
+ }
301
+ }
302
+ // Preserve custom vars
303
+ draft.customEnvVars = { ...(oldDraft.customEnvVars || {}) };
304
+ return draft;
305
+ }
306
+
307
+ /**
308
+ * Show duplicate API branch: enter existing config or go back.
309
+ * @returns {'enter_existing'|'go_back'}
310
+ */
311
+ async function showDuplicateBranch(existingApi) {
312
+ const menu = new Menu();
313
+ screen.render([
314
+ '', colors.yellow + i18n.tSync('add_api.duplicate_title') + colors.reset, '',
315
+ i18n.tSync('api.edit.field_name') + ': ' + existingApi.name,
316
+ i18n.tSync('api.details.provider') + ': ' + resolveProviderName(existingApi.provider),
317
+ i18n.tSync('api.details.url') + ': ' + existingApi.baseUrl,
318
+ i18n.tSync('api.details.model') + ': ' + existingApi.model,
319
+ '', colors.gray + i18n.tSync('add_api.duplicate_draft_discarded') + colors.reset, '',
320
+ i18n.tSync('action.please_choose'), '',
321
+ ]);
322
+ menu.setOptions([
323
+ i18n.tSync('add_api.duplicate_enter_config'),
324
+ i18n.tSync('add_api.duplicate_back'),
325
+ ]);
326
+ const choice = await menu.navigate();
327
+ if (choice === 0) return 'enter_existing';
328
+ return 'go_back';
329
+ }
330
+
331
+ // --- Draft editing sub-pages (no persistence, operate on draft object) ---
332
+
333
+ async function editDraftModelConfig(draft) {
334
+ const menu = new Menu();
335
+ const { PREDEFINED_MODEL_ENV_KEYS } = require('./lib/validators');
336
+ const { MODEL_CONFIG_LABELS } = require('./lib/api-manager');
337
+ const getLabel = (key) => i18nLabel("model", key, MODEL_CONFIG_LABELS);
338
+
339
+ while (true) {
340
+ const rows = PREDEFINED_MODEL_ENV_KEYS.map((key) => {
341
+ const currentVal = draft.modelEnvVars[key] || '';
342
+ const autoVal = draft._autoModelEnvVars[key] || '';
343
+ const displayVal = currentVal || autoVal || i18n.tSync('status.not_set');
344
+ const isOverridden = autoVal !== '' && currentVal !== autoVal;
345
+ const mark = isOverridden ? ' ' + colors.cyan + i18n.tSync('status.overridden') + colors.reset : '';
346
+ return { label: ' ' + getLabel(key), displayVal, mark };
347
+ });
348
+ const { getStringWidth, padStringToWidth } = require('./lib/utils/string-width');
349
+ const maxLabelW = Math.max(...rows.map(r => getStringWidth(r.label)));
350
+ const maxValueW = Math.max(...rows.map(r => getStringWidth(r.displayVal)));
351
+ const options = rows.map(r => {
352
+ return colors.reset + padStringToWidth(r.label, maxLabelW + 2) + ' ' + padStringToWidth(r.displayVal, maxValueW) + r.mark;
353
+ });
354
+ options.push(i18n.tSync('api.edit.back'));
355
+ menu.setOptions(options);
356
+ const modelHintFn = (idx) => {
357
+ if (idx >= PREDEFINED_MODEL_ENV_KEYS.length) return null;
358
+ const k = PREDEFINED_MODEL_ENV_KEYS[idx];
359
+ const dk = {
360
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'hints.model.sonnet_detail',
361
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'hints.model.opus_detail',
362
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'hints.model.haiku_detail',
363
+ CLAUDE_CODE_SUBAGENT_MODEL: 'hints.model.subagent_detail',
364
+ ANTHROPIC_CUSTOM_MODEL_OPTION: 'hints.model.custom_option_detail',
365
+ ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'hints.model.custom_name_detail',
366
+ };
367
+ return dk[k] ? i18n.tSync(dk[k]) : null;
368
+ };
369
+ const choice = await menu.navigate(null, modelHintFn, 'navigation.enter_to_edit');
370
+
371
+ if (choice === -1 || choice === PREDEFINED_MODEL_ENV_KEYS.length) return;
372
+
373
+ const key = PREDEFINED_MODEL_ENV_KEYS[choice];
374
+ const autoVal = draft._autoModelEnvVars[key] || '';
375
+ const label = getLabel(key);
376
+ const subMenu = new Menu();
377
+ let inSub = true;
378
+ while (inSub) {
379
+ const currentVal = draft.modelEnvVars[key] || '';
380
+ screen.render([
381
+ '', colors.cyan + label + colors.reset, '',
382
+ i18n.tSync('status.current_value') + ': ' + (currentVal || i18n.tSync('status.not_set')),
383
+ i18n.tSync('status.recommended_value') + ': ' + (autoVal || i18n.tSync('status.not_set')),
384
+ '', i18n.tSync('action.please_choose'), '',
385
+ ]);
386
+ subMenu.setOptions([
387
+ i18n.tSync('action.follow_recommended'),
388
+ i18n.tSync('action.custom_input'),
389
+ i18n.tSync('api.edit.back'),
390
+ ]);
391
+ const subChoice = await subMenu.navigate(null, null);
392
+ if (subChoice === -1 || subChoice === 2) { inSub = false; break; }
393
+ if (subChoice === 0) {
394
+ draft.modelEnvVars[key] = autoVal;
395
+ inSub = false;
396
+ } else if (subChoice === 1) {
397
+ const input = await inputStepValue(
398
+ i18n.tSync('api.edit.current_value', currentVal),
399
+ { allowEmpty: true }
400
+ );
401
+ if (input === null) continue; // "back"
402
+ draft.modelEnvVars[key] = (input === '') ? autoVal : input;
403
+ inSub = false;
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ async function editDraftRuntimeConfig(draft) {
410
+ const menu = new Menu();
411
+ const { PREDEFINED_RUNTIME_KEYS, TYPE_A_FIELDS, TYPE_B_FIELDS } = require('./lib/validators');
412
+ const { RUNTIME_CONFIG_LABELS } = require('./lib/api-manager');
413
+ const { getProvider } = require('./lib/presets/providers');
414
+ const getLabel = (key) => i18nLabel("runtime", key, RUNTIME_CONFIG_LABELS);
415
+ const providerConfig = getProvider(draft.provider);
416
+ const providerDefaults = providerConfig ? providerConfig.envVars || {} : {};
417
+
418
+ while (true) {
419
+ const runtimeKeys = [...PREDEFINED_RUNTIME_KEYS];
420
+ const rows = runtimeKeys.map(key => {
421
+ const val = draft.runtimeEnvVars[key] || '';
422
+ const providerVal = providerDefaults[key];
423
+ let display;
424
+ if (val === '') {
425
+ if (providerVal !== undefined) {
426
+ if (TYPE_A_FIELDS.includes(key) && providerVal === '1') {
427
+ display = i18n.tSync('status.enabled');
428
+ } else {
429
+ display = providerVal;
430
+ }
431
+ } else {
432
+ display = i18n.tSync('status.auto');
433
+ }
434
+ } else if (TYPE_A_FIELDS.includes(key) && val === 'off') {
435
+ display = i18n.tSync('status.disabled');
436
+ } else if (TYPE_A_FIELDS.includes(key) && val === '1') {
437
+ display = i18n.tSync('status.enabled');
438
+ } else {
439
+ display = val;
440
+ }
441
+ const isManual = (draft._runtimeEnvSources || {})[key] === 'manual';
442
+ const mark = isManual ? ' ' + colors.cyan + i18n.tSync('status.overridden') + colors.reset : '';
443
+ return { label: ' ' + getLabel(key), display, mark };
444
+ });
445
+ const { getStringWidth, padStringToWidth } = require('./lib/utils/string-width');
446
+ const maxLabelW = Math.max(...rows.map(r => getStringWidth(r.label)));
447
+ const maxValueW = Math.max(...rows.map(r => getStringWidth(r.display)));
448
+ const options = rows.map(r => {
449
+ return colors.reset + padStringToWidth(r.label, maxLabelW + 2) + ' ' + padStringToWidth(r.display, maxValueW) + r.mark;
450
+ });
451
+ options.push(i18n.tSync('api.edit.back'));
452
+ menu.setOptions(options);
453
+ const runtimeHintFn = (idx) => {
454
+ if (idx >= runtimeKeys.length) return null;
455
+ const k = runtimeKeys[idx];
456
+ const dk = {
457
+ API_TIMEOUT_MS: 'hints.runtime.timeout_detail',
458
+ CLAUDE_CODE_ATTRIBUTION_HEADER: 'hints.runtime.attribution_detail',
459
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 'hints.runtime.nonessential_detail',
460
+ CLAUDE_CODE_EFFORT_LEVEL: 'hints.runtime.effort_detail',
461
+ CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: 'hints.runtime.experimental_detail',
462
+ CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: 'hints.runtime.nonstreaming_detail',
463
+ };
464
+ if (!dk[k]) return null;
465
+ const val = draft.runtimeEnvVars[k] || '';
466
+ const isManual = (draft._runtimeEnvSources || {})[k] === 'manual';
467
+ let sourceText;
468
+ if (isManual && val !== '') {
469
+ sourceText = i18n.tSync('hints.runtime.source_manual');
470
+ } else if (providerDefaults[k] !== undefined) {
471
+ sourceText = i18n.tSync('hints.runtime.source_provider');
472
+ } else {
473
+ sourceText = i18n.tSync('hints.runtime.source_default');
474
+ }
475
+ return i18n.tSync(dk[k]) + '\n' + sourceText;
476
+ };
477
+ const choice = await menu.navigate(null, runtimeHintFn, 'navigation.enter_to_edit');
478
+
479
+ if (choice === -1 || choice === runtimeKeys.length) return;
480
+
481
+ const key = runtimeKeys[choice];
482
+ if (TYPE_A_FIELDS.includes(key) || TYPE_B_FIELDS.includes(key)) {
483
+ const values = TYPE_A_FIELDS.includes(key) ? ['', '1', 'off'] : ['', '1', '0'];
484
+ try {
485
+ const v = draft.runtimeEnvVars[key] || '';
486
+ const idx = values.indexOf(v);
487
+ const next = values[(idx + 1) % values.length];
488
+ ApiManager.applyDraftEnvChange(draft, 'runtime', key, next);
489
+ } catch (e) {
490
+ screen.write(colors.red + e.message + colors.reset + '\n');
491
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
492
+ }
493
+ } else {
494
+ const isEffortLevel = (key === 'CLAUDE_CODE_EFFORT_LEVEL');
495
+ const extraHints = isEffortLevel ? ['hints.runtime.effort_values'] : [];
496
+ const draftVal = draft.runtimeEnvVars[key] || '';
497
+ const draftProvVal = providerDefaults[key];
498
+ const draftDisplayVal = draftVal || draftProvVal || i18n.tSync('status.auto');
499
+ const input = await inputStepValue(
500
+ i18n.tSync('api.edit.current_value', draftDisplayVal),
501
+ { allowEmpty: true, hintLines: ['prompt.exit_to_cancel', ...extraHints] }
502
+ );
503
+ if (input === null) continue; // "back"
504
+ try {
505
+ ApiManager.applyDraftEnvChange(draft, 'runtime', key, input === '' ? '' : input);
506
+ } catch (e) {
507
+ screen.write(colors.red + e.message + colors.reset + '\n');
508
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
509
+ }
510
+ }
511
+ }
512
+ }
513
+
514
+ async function editDraftCustomVars(draft) {
515
+ const menu = new Menu();
516
+ while (true) {
517
+ const keys = Object.keys(draft.customEnvVars || {});
518
+ const options = keys.length > 0
519
+ ? keys.map(k => ' ' + k + ' = ' + draft.customEnvVars[k])
520
+ : [colors.gray + i18n.tSync('api.edit.no_custom_vars') + colors.reset];
521
+ options.push(i18n.tSync('action.add_variable'));
522
+ options.push(i18n.tSync('api.edit.back'));
523
+ menu.setOptions(options);
524
+ const choice = await menu.navigate(null, null, 'navigation.enter_to_edit');
525
+
526
+ if (choice === -1 || choice === options.length - 1) return;
527
+ if (choice === options.length - 2) {
528
+ const keyInput = await inputStepValue(i18n.tSync('api.edit.enter_custom_key'), { allowEmpty: false });
529
+ if (!keyInput || keyInput === null) continue;
530
+ const valInput = await inputStepValue(i18n.tSync('api.edit.enter_custom_value'), { allowEmpty: true });
531
+ if (valInput === null) continue;
532
+ try {
533
+ ApiManager.applyDraftEnvChange(draft, 'custom', keyInput, valInput || '');
534
+ } catch (e) {
535
+ screen.write(colors.red + e.message + colors.reset + '\n');
536
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
537
+ }
538
+ } else if (choice < keys.length) {
539
+ const key = keys[choice];
540
+ screen.render(['', colors.yellow + i18n.tSync('confirm.delete_variable') + colors.reset]);
541
+ const confirm = await simpleInput('> ');
542
+ if (confirm.toLowerCase() === 'y') {
543
+ ApiManager.deleteDraftCustomEnvVar(draft, key);
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ async function editDraftSection(draft, section) {
550
+ if (section === 'modify_model') await editDraftModelConfig(draft);
551
+ else if (section === 'modify_runtime') await editDraftRuntimeConfig(draft);
552
+ else if (section === 'modify_custom') await editDraftCustomVars(draft);
553
+ }
554
+
555
+ /**
556
+ * Show config confirmation page (step 6).
557
+ * @returns {'finish'|'back'|'modify_model'|'modify_runtime'|'modify_custom'}
558
+ */
559
+ async function showConfigConfirmation(draft) {
560
+ const menu = new Menu();
561
+ const { PREDEFINED_MODEL_ENV_KEYS, PREDEFINED_RUNTIME_KEYS } = require('./lib/validators');
562
+ const modelOverridden = PREDEFINED_MODEL_ENV_KEYS.filter(k => {
563
+ const v = draft.modelEnvVars[k] || '';
564
+ const a = draft._autoModelEnvVars[k];
565
+ return a !== undefined && v !== a;
566
+ }).length;
567
+ const runtimeOverridden = PREDEFINED_RUNTIME_KEYS.filter(k => {
568
+ const val = (draft.runtimeEnvVars || {})[k] || '';
569
+ return val !== '';
570
+ }).length;
571
+ const customCount = Object.keys(draft.customEnvVars).length;
572
+
573
+ const { getStringWidth, padStringToWidth } = require('./lib/utils/string-width');
574
+ const fieldLabels = [
575
+ i18n.tSync('api.edit.field_name'), i18n.tSync('api.details.provider'),
576
+ i18n.tSync('api.details.url'), i18n.tSync('api.details.model'),
577
+ ];
578
+ const labelW = Math.max(...fieldLabels.map(l => getStringWidth(l)));
579
+ const fieldValues = [
580
+ draft.name || i18n.tSync('status.not_set'), resolveProviderName(draft.provider),
581
+ draft.baseUrl, draft.model,
582
+ ];
583
+ function fieldLine(label, value) {
584
+ return ' ' + colors.gray + padStringToWidth(label, labelW) + colors.reset + ' ' + colors.cyan + value + colors.reset + '\n';
585
+ }
586
+ const overriddenMark = ' ' + colors.cyan + i18n.tSync('status.overridden') + colors.reset;
587
+ const versionInfo = colors.bright + colors.cyan + i18n.tSync('add_api.step_n_of_m', '6', '6') + ' · ' + i18n.tSync('add_api.confirm_config') + colors.reset + '\n'
588
+ + '\n'
589
+ + fieldLine(fieldLabels[0], fieldValues[0])
590
+ + fieldLine(fieldLabels[1], fieldValues[1])
591
+ + fieldLine(fieldLabels[2], fieldValues[2])
592
+ + fieldLine(fieldLabels[3], fieldValues[3])
593
+ + '\n'
594
+ + ' ' + colors.gray + i18n.tSync('add_api.confirm_page_prompt') + colors.reset + '\n'
595
+ + '\n'
596
+ + ' ' + i18n.tSync('page.model_config') + ' ' + colors.cyan + modelOverridden + '/' + PREDEFINED_MODEL_ENV_KEYS.length + colors.reset + ' ' + overriddenMark + '\n'
597
+ + ' ' + i18n.tSync('page.runtime_config') + ' ' + colors.cyan + runtimeOverridden + '/' + PREDEFINED_RUNTIME_KEYS.length + colors.reset + ' ' + overriddenMark + '\n'
598
+ + ' ' + i18n.tSync('page.custom_vars') + ' ' + colors.cyan + i18n.tSync('summary.x_items', String(customCount)) + colors.reset;
599
+
600
+ menu.setOptions([
601
+ i18n.tSync('action.finish_create'),
602
+ i18n.tSync('page.model_config'),
603
+ i18n.tSync('page.runtime_config'),
604
+ i18n.tSync('page.custom_vars'),
605
+ i18n.tSync('action.cancel_config'),
606
+ ]);
607
+ const hintFn = (idx) => {
608
+ if (idx === 0) return i18n.tSync('add_api.finish_hint');
609
+ if (idx === 1) return i18n.tSync('hints.model.desc');
610
+ if (idx === 2) return i18n.tSync('hints.runtime.desc');
611
+ if (idx === 3) return i18n.tSync('hints.custom.desc');
612
+ return null;
613
+ };
614
+ const choice = await menu.navigate(versionInfo, hintFn);
615
+ if (choice === -1 || choice === 4) return 'cancel';
616
+ if (choice === 0) return 'finish';
617
+ if (choice === 1) return 'modify_model';
618
+ if (choice === 2) return 'modify_runtime';
619
+ if (choice === 3) return 'modify_custom';
620
+ }
621
+
218
622
  /**
219
- * Add new third-party API
623
+ * Replay draft edits onto a newly created API.
624
+ * @returns {{ ok: string[], failed: string[] }}
625
+ */
626
+ function replayDraftToApi(apiManager, draft, newApi) {
627
+ const ok = [];
628
+ const failed = [];
629
+
630
+ // Model env vars: only write overridden values
631
+ for (const k of Object.keys(draft.modelEnvVars || {})) {
632
+ const v = draft.modelEnvVars[k];
633
+ const autoV = draft._autoModelEnvVars[k] || '';
634
+ if (v !== autoV) {
635
+ try {
636
+ apiManager.updateModelEnvVar(newApi.id, k, v);
637
+ ok.push('model/' + k);
638
+ } catch (e) {
639
+ failed.push('model/' + k + ': ' + e.message);
640
+ }
641
+ }
642
+ }
643
+ // Runtime env vars: only write manual values
644
+ for (const k of Object.keys(draft.runtimeEnvVars || {})) {
645
+ if ((draft._runtimeEnvSources || {})[k] === 'manual') {
646
+ try {
647
+ apiManager.updateRuntimeEnvVar(newApi.id, k, draft.runtimeEnvVars[k]);
648
+ ok.push('runtime/' + k);
649
+ } catch (e) {
650
+ failed.push('runtime/' + k + ': ' + e.message);
651
+ }
652
+ }
653
+ }
654
+ // Custom env vars
655
+ for (const [k, v] of Object.entries(draft.customEnvVars || {})) {
656
+ try {
657
+ apiManager.setCustomEnvVar(newApi.id, k, v);
658
+ ok.push('custom/' + k);
659
+ } catch (e) {
660
+ failed.push('custom/' + k + ': ' + e.message);
661
+ }
662
+ }
663
+ return { ok, failed };
664
+ }
665
+
666
+ /**
667
+ * Add new third-party API — 6-step wizard with state machine.
668
+ * Step 1: Provider → 2: Base URL → 3: Auth Token → 4: Model → 5: Name → 6: Confirm
220
669
  */
221
670
  async function addNewThirdPartyApi() {
222
- try {
223
- const apiData = await promptForThirdPartyApi();
224
-
225
- // Check if this is the first API
226
- const isFirstApi = apiManager.getApis().length === 0;
227
- const hasExportPassword = apiManager.hasExportPassword();
228
-
229
- const newApi = apiManager.addApi(
230
- apiData.baseUrl,
231
- apiData.authToken,
232
- apiData.model,
233
- apiData.name,
234
- apiData.provider
235
- );
671
+ const { getProvider, getSuggestedModels } = require('./lib/presets/providers');
672
+ let step = 1;
673
+ let providerId = null, providerMeta = null;
674
+ let baseUrl = null, authToken = null, model = null, name = '';
675
+ let draft = null;
236
676
 
237
- showSuccess(await i18n.t('messages.success.api_added'), [
238
- `Name: ${newApi.name}`,
239
- `${await i18n.t('api.details.provider')}: ${newApi.provider}`,
240
- `${await i18n.t('api.details.url')}: ${newApi.baseUrl}`,
241
- `${await i18n.t('api.details.model')}: ${newApi.model}`
242
- ]);
677
+ try {
678
+ while (step >= 1 && step <= 6) {
679
+ switch (step) {
680
+ case 1: { // Provider
681
+ const result = await selectProvider({
682
+ title: i18n.tSync('add_api.step_n_of_m', '1', '6'),
683
+ showNote: true,
684
+ });
685
+ if (!result) { step = 0; break; } // Esc = cancel
686
+ providerId = result.id;
687
+ providerMeta = getProvider(providerId);
688
+ baseUrl = null; authToken = null; model = null; name = '';
689
+ step = 2;
690
+ break;
691
+ }
692
+ case 2: { // Base URL
693
+ const defaultUrl = providerMeta ? providerMeta.baseUrl || '' : '';
694
+ const input = await inputStepValue(
695
+ i18n.tSync('api.edit.field_base_url') + (defaultUrl ? ' [' + defaultUrl + ']' : ''),
696
+ { defaultValue: defaultUrl, stepLabel: i18n.tSync('add_api.step_n_of_m', '2', '6') }
697
+ );
698
+ if (input === null) { step = 1; break; }
699
+ baseUrl = input;
700
+ step = 3;
701
+ break;
702
+ }
703
+ case 3: { // Auth Token
704
+ const input = await inputStepValue(
705
+ i18n.tSync('api.details.token'),
706
+ {
707
+ stepLabel: i18n.tSync('add_api.step_n_of_m', '3', '6'),
708
+ hintLines: ['prompt.exit_to_cancel'],
709
+ validate: (v) => {
710
+ if (!v || v.trim().length < 10) {
711
+ return { valid: false, error: i18n.tSync('errors.api.invalid_token', 'at least 10 characters required') };
712
+ }
713
+ return { valid: true };
714
+ },
715
+ }
716
+ );
717
+ if (input === null) { step = 2; break; }
718
+ authToken = input;
719
+ step = 4;
720
+ break;
721
+ }
722
+ case 4: { // Model
723
+ const suggested = getSuggestedModels(providerId);
724
+ const hintLines = suggested.length > 0
725
+ ? ['', colors.gray + ' ' + i18n.tSync('add_api.recommended_models') + ': ' + suggested.join(', ') + colors.reset, '']
726
+ : ['', ''];
727
+ const fullPrompt = i18n.tSync('api.details.model');
728
+ const lines = [''];
729
+ lines.push(colors.bright + i18n.tSync('add_api.step_n_of_m', '4', '6') + colors.reset);
730
+ lines.push(colors.cyan + fullPrompt + colors.reset);
731
+ lines.push(...hintLines);
732
+ lines.push(colors.gray + i18n.tSync('prompt.exit_to_cancel') + colors.reset);
733
+ lines.push(colors.gray + i18n.tSync('prompt.empty_to_restore') + colors.reset);
734
+ lines.push('');
735
+ screen.render(lines);
736
+ const input = await simpleInput(colors.green + '> ' + colors.reset);
737
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
738
+ throw new Error(await i18n.t('errors.general.cancelled_by_user'));
739
+ }
740
+ if (input.toLowerCase() === 'back') { step = 3; break; }
741
+ if (input === '' && suggested.length > 0) {
742
+ model = suggested[0];
743
+ } else {
744
+ model = input;
745
+ }
243
746
 
244
- if (isFirstApi) {
245
- showInfo(await i18n.t('messages.info.first_time_usage'));
747
+ // Duplicate check
748
+ const dup = apiManager.checkDuplicate(baseUrl, authToken, model);
749
+ if (dup.isDuplicate) {
750
+ const action = await showDuplicateBranch(dup.existing);
751
+ if (action === 'enter_existing') {
752
+ await editApiEnvVarsById(apiManager, { apiId: dup.existing.id });
753
+ return; // done
754
+ }
755
+ step = 2; break; // go back to modify connection info
756
+ }
757
+ step = 5;
758
+ break;
759
+ }
760
+ case 5: { // Name
761
+ const fullName = resolveProviderName(providerId);
762
+ const providerShortName = fullName.split(' (')[0].split(' -')[0];
763
+ // Count APIs sharing the same short-name prefix (handles moonshot / kimi_for_coding)
764
+ const sameNameCount = apiManager.config.apis.filter(a => {
765
+ const aFull = resolveProviderName(a.provider);
766
+ return aFull.split(' (')[0].split(' -')[0] === providerShortName;
767
+ }).length;
768
+ const autoName = providerShortName + ' #' + (sameNameCount + 1);
769
+ const input = await inputStepValue(
770
+ i18n.tSync('api.edit.field_name'),
771
+ { defaultValue: autoName, stepLabel: i18n.tSync('add_api.step_n_of_m', '5', '6') }
772
+ );
773
+ if (input === null) { step = 4; break; }
774
+ name = input;
775
+ // Build/rebuild draft preserving manual edits if returning from step 6
776
+ draft = rebuildDraftPreservingOverrides(draft, providerId, baseUrl, authToken, model, name);
777
+ step = 6;
778
+ break;
779
+ }
780
+ case 6: { // Config confirmation
781
+ const action = await showConfigConfirmation(draft);
782
+ if (action === 'finish') {
783
+ try {
784
+ const newApi = apiManager.addApi(baseUrl, authToken, model, name, providerId);
785
+ const replayResult = replayDraftToApi(apiManager, draft, newApi);
786
+ if (replayResult.failed.length > 0) {
787
+ screen.write(colors.yellow + i18n.tSync('add_api.partial_failure') + colors.reset + '\n');
788
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
789
+ await editApiEnvVarsById(apiManager, { apiId: newApi.id });
790
+ } else {
791
+ const isFirstApi = apiManager.getApis().length === 1;
792
+ showSuccess(await i18n.t('messages.success.api_added'), [
793
+ `Name: ${newApi.name}`,
794
+ `${await i18n.t('api.details.provider')}: ${resolveProviderName(newApi.provider)}`,
795
+ `${await i18n.t('api.details.url')}: ${newApi.baseUrl}`,
796
+ `${await i18n.t('api.details.model')}: ${newApi.model}`
797
+ ]);
798
+ if (isFirstApi) {
799
+ showInfo(await i18n.t('messages.info.first_time_usage'));
800
+ }
801
+ }
802
+ } catch (error) {
803
+ forceStdinCleanup();
804
+ if (error.code === 'DUPLICATE_API') {
805
+ const existing = apiManager.config.apis.find(a => a.id === error.existingApiId);
806
+ if (existing) {
807
+ screen.write(colors.yellow + i18n.tSync('add_api.duplicate_race_lost') + colors.reset + '\n');
808
+ const action = await showDuplicateBranch(existing);
809
+ if (action === 'enter_existing') {
810
+ await editApiEnvVarsById(apiManager, { apiId: existing.id });
811
+ }
812
+ return;
813
+ }
814
+ }
815
+ const cancelledMessage = await i18n.t('errors.general.cancelled_by_user');
816
+ if (error.message === cancelledMessage) {
817
+ screen.write(colors.yellow + await i18n.t('messages.info.operation_cancelled') + colors.reset + '\n');
818
+ } else {
819
+ showError(await i18n.t('errors.api.failed_add', error.message));
820
+ }
821
+ }
822
+ return; // Exit loop on all paths
823
+ } else if (action === 'cancel') {
824
+ return; // back to main menu
825
+ } else {
826
+ // modify_model, modify_runtime, modify_custom
827
+ await editDraftSection(draft, action);
828
+ // Loop back to step 6 (confirmation re-renders)
829
+ }
830
+ break;
831
+ }
246
832
  }
833
+ }
247
834
 
835
+ if (step === 0) {
836
+ screen.write(colors.yellow + i18n.tSync('messages.info.operation_cancelled') + colors.reset + '\n');
837
+ }
248
838
  } catch (error) {
249
- // Force cleanup stdin state to prevent navigation issues
250
839
  forceStdinCleanup();
251
-
252
- // Check if user cancelled the operation
253
- const cancelledMessage = await i18n.t('errors.general.cancelled_by_user');
254
- if (error.message === cancelledMessage) {
255
- // User cancelled - show neutral message instead of error
256
- console.log(colors.yellow + await i18n.t('messages.info.operation_cancelled') + colors.reset);
840
+ const cancelledMsg = i18n.tSync('errors.general.cancelled_by_user');
841
+ if (error.message === cancelledMsg) {
842
+ screen.write(colors.yellow + i18n.tSync('messages.info.operation_cancelled') + colors.reset + '\n');
843
+ } else if (error.code === 'DUPLICATE_API') {
844
+ const existing = apiManager.config.apis.find(a => a.id === error.existingApiId);
845
+ if (existing) {
846
+ screen.write(colors.yellow + i18n.tSync('add_api.duplicate_race_lost') + colors.reset + '\n');
847
+ }
257
848
  } else {
258
- // Actual error occurred
259
- showError(await i18n.t('errors.api.failed_add', error.message));
849
+ screen.write(colors.red + error.message + colors.reset + '\n');
260
850
  }
261
851
  }
262
-
263
- await waitForKey(await i18n.t('messages.prompts.press_any_key'));
264
852
  }
265
853
 
266
854
  /**
267
855
  * Remove third-party API menu with submenu
268
856
  */
269
857
  async function removeThirdPartyApi() {
270
- console.clear();
271
- console.log('');
272
- console.log(colors.bright + colors.orange + '🗑️ ' + await i18n.t('menu.remove_api.title') + colors.reset);
273
- console.log('');
274
-
275
858
  const apis = apiManager.getApis();
276
859
 
860
+ const lines = [
861
+ '',
862
+ colors.bright + colors.orange + '🗑️ ' + await i18n.t('menu.remove_api.title') + colors.reset,
863
+ '',
864
+ ];
865
+
277
866
  // Show current API count
278
867
  if (apis.length > 0) {
279
- console.log(colors.cyan + ' ' + await i18n.t('messages.info.current_api_count', apis.length) + colors.reset);
280
- console.log('');
868
+ lines.push(colors.cyan + ' ' + await i18n.t('messages.info.current_api_count', apis.length) + colors.reset);
869
+ lines.push('');
281
870
  }
282
871
 
872
+ screen.render(lines);
873
+
283
874
  const menuOptions = [
284
875
  await i18n.t('menu.remove_api.delete_single'),
285
876
  await i18n.t('menu.remove_api.clear_all'),
@@ -337,7 +928,6 @@ async function deleteSingleApi() {
337
928
  const selectedIndex = apis.findIndex(api => api.id === selectedApi.id);
338
929
  apiManager.removeApi(selectedIndex);
339
930
 
340
- console.clear();
341
931
  showSuccess(await i18n.t('messages.success.api_removed'), [
342
932
  `${await i18n.t('api.actions.removed_info', selectedApi.name)}`,
343
933
  `${await i18n.t('api.details.provider')}: ${selectedApi.provider}`
@@ -376,24 +966,23 @@ async function clearAllApis() {
376
966
  const count = apis.length;
377
967
 
378
968
  if (count === 0) {
379
- console.clear();
380
969
  showInfo(await i18n.t('messages.info.no_apis'));
381
970
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
382
971
  return;
383
972
  }
384
973
 
385
- console.clear();
386
- console.log('');
387
- console.log(colors.bright + colors.red + '⚠️ ' + await i18n.t('menu.remove_api.clear_all') + colors.reset);
388
- console.log('');
389
- console.log(colors.yellow + ' ' + await i18n.t('messages.prompts.confirm_clear_all', count) + colors.reset);
390
- console.log('');
974
+ screen.render([
975
+ '',
976
+ colors.bright + colors.red + '⚠️ ' + await i18n.t('menu.remove_api.clear_all') + colors.reset,
977
+ '',
978
+ colors.yellow + ' ' + await i18n.t('messages.prompts.confirm_clear_all', count) + colors.reset,
979
+ '',
980
+ ]);
391
981
 
392
982
  const input = await simpleInput(colors.cyan + ' ' + await i18n.t('messages.prompts.confirm_clear_all_input') + colors.reset);
393
983
 
394
984
  if (input === 'CLEAR') {
395
985
  const clearedCount = apiManager.clearAllApis();
396
- console.clear();
397
986
  showSuccess(await i18n.t('messages.info.all_apis_cleared', clearedCount));
398
987
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
399
988
  } else {
@@ -455,10 +1044,11 @@ function formatRelativeTime(timestamp) {
455
1044
  }
456
1045
 
457
1046
  async function viewStatistics() {
458
- console.clear();
459
- console.log('');
460
- console.log(colors.bright + colors.orange + '📊 ' + await i18n.t('statistics.title') + colors.reset);
461
- console.log('');
1047
+ screen.render([
1048
+ '',
1049
+ colors.bright + colors.orange + '📊 ' + await i18n.t('statistics.title') + colors.reset,
1050
+ '',
1051
+ ]);
462
1052
 
463
1053
  const menuOptions = [
464
1054
  await i18n.t('statistics.menu_view'),
@@ -480,7 +1070,7 @@ async function viewStatistics() {
480
1070
  const confirm = await simpleInput(colors.yellow + ' ' + await i18n.t('statistics.reset_confirm') + ' ' + colors.reset);
481
1071
  if (confirm.toLowerCase() === 'y') {
482
1072
  apiManager.resetStatistics();
483
- console.log(colors.green + ' ✓ ' + await i18n.t('statistics.reset_success') + colors.reset);
1073
+ screen.write(colors.green + ' ✓ ' + await i18n.t('statistics.reset_success') + colors.reset + '\n');
484
1074
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
485
1075
  }
486
1076
  return viewStatistics();
@@ -498,38 +1088,38 @@ async function viewStatistics() {
498
1088
  async function showStatisticsDetails() {
499
1089
  const { padStringToWidth } = require('./lib/utils/string-width');
500
1090
 
501
- console.clear();
502
- console.log('');
503
- console.log(colors.bright + colors.orange + '📊 ' + await i18n.t('statistics.title') + colors.reset);
504
- console.log('');
505
-
506
1091
  const stats = apiManager.getEnhancedStatistics();
507
1092
 
508
- // Summary section
509
- console.log(colors.cyan + ' ' + i18n.tSync('ui.general.summary') + ':' + colors.reset);
510
- console.log(colors.gray + ` ${await i18n.t('statistics.total_apis', stats.totalApis)}` + colors.reset);
511
- console.log(colors.gray + ` ${await i18n.t('statistics.active_api', stats.activeApiName)}` + colors.reset);
512
- console.log(colors.gray + ` ${await i18n.t('statistics.most_used', stats.mostUsedApi)}` + colors.reset);
513
- console.log(colors.gray + ` ${await i18n.t('statistics.total_usage', stats.totalUsage)}` + colors.reset);
514
- console.log(colors.gray + ` ${await i18n.t('statistics.success_rate', stats.successRate)}` + colors.reset);
515
- console.log('');
1093
+ const lines = [
1094
+ '',
1095
+ colors.bright + colors.orange + '📊 ' + await i18n.t('statistics.title') + colors.reset,
1096
+ '',
1097
+ // Summary section
1098
+ colors.cyan + ' ' + i18n.tSync('ui.general.summary') + ':' + colors.reset,
1099
+ colors.gray + ` ${await i18n.t('statistics.total_apis', stats.totalApis)}` + colors.reset,
1100
+ colors.gray + ` ${await i18n.t('statistics.active_api', stats.activeApiName)}` + colors.reset,
1101
+ colors.gray + ` ${await i18n.t('statistics.most_used', stats.mostUsedApi)}` + colors.reset,
1102
+ colors.gray + ` ${await i18n.t('statistics.total_usage', stats.totalUsage)}` + colors.reset,
1103
+ colors.gray + ` ${await i18n.t('statistics.success_rate', stats.successRate)}` + colors.reset,
1104
+ '',
1105
+ ];
516
1106
 
517
1107
  if (stats.apiStats.length > 0) {
518
- console.log(colors.cyan + ' ' + i18n.tSync('ui.general.configured_apis') + ':' + colors.reset);
519
- console.log('');
1108
+ lines.push(colors.cyan + ' ' + i18n.tSync('ui.general.configured_apis') + ':' + colors.reset);
1109
+ lines.push('');
520
1110
 
521
1111
  // Table header
522
- console.log(colors.dim + ' ' +
1112
+ lines.push(colors.dim + ' ' +
523
1113
  padStringToWidth(await i18n.t('statistics.header_name'), 20) +
524
1114
  padStringToWidth(await i18n.t('statistics.header_usage'), 10) +
525
1115
  padStringToWidth(await i18n.t('statistics.header_success'), 10) +
526
1116
  await i18n.t('statistics.header_last_used') +
527
1117
  colors.reset);
528
- console.log(colors.dim + ' ' + '─'.repeat(60) + colors.reset);
1118
+ lines.push(colors.dim + ' ' + '─'.repeat(60) + colors.reset);
529
1119
 
530
1120
  for (const api of stats.apiStats) {
531
1121
  const lastUsedText = formatRelativeTime(api.lastUsed);
532
- console.log(colors.gray + ' ' +
1122
+ lines.push(colors.gray + ' ' +
533
1123
  padStringToWidth(api.name, 20) +
534
1124
  padStringToWidth(String(api.usageCount), 10) +
535
1125
  padStringToWidth(api.successRate, 10) +
@@ -537,10 +1127,11 @@ async function showStatisticsDetails() {
537
1127
  colors.reset);
538
1128
  }
539
1129
  } else {
540
- console.log(colors.gray + ' ' + await i18n.t('statistics.no_usage') + colors.reset);
1130
+ lines.push(colors.gray + ' ' + await i18n.t('statistics.no_usage') + colors.reset);
541
1131
  }
542
1132
 
543
- console.log('');
1133
+ lines.push('');
1134
+ screen.render(lines);
544
1135
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
545
1136
  }
546
1137
 
@@ -551,36 +1142,36 @@ async function showStatisticsDetails() {
551
1142
  async function handleFirstTimePasswordSetup() {
552
1143
  while (true) {
553
1144
  // Clear screen and show header
554
- console.clear();
555
- console.log('');
556
- console.log(colors.bright + colors.yellow + '🔐 ' + i18n.tSync('password.setup.first_time_title') + colors.reset);
557
- console.log('');
558
-
559
- // Show information
560
- console.log(colors.cyan + i18n.tSync('password.setup.why_needed') + colors.reset);
1145
+ const lines = [
1146
+ '',
1147
+ colors.bright + colors.yellow + '🔐 ' + i18n.tSync('password.setup.first_time_title') + colors.reset,
1148
+ '',
1149
+ // Show information
1150
+ colors.cyan + i18n.tSync('password.setup.why_needed') + colors.reset,
1151
+ ];
561
1152
  const whyNeededItems = i18n.tSync('password.setup.why_needed_items');
562
1153
  if (Array.isArray(whyNeededItems)) {
563
1154
  whyNeededItems.forEach(item => {
564
- console.log(colors.gray + '• ' + item + colors.reset);
1155
+ lines.push(colors.gray + '• ' + item + colors.reset);
565
1156
  });
566
1157
  }
567
- console.log('');
568
- console.log(colors.cyan + '🔒 ' + i18n.tSync('password.setup.new_security_title') + colors.reset);
1158
+ lines.push('');
1159
+ lines.push(colors.cyan + '🔒 ' + i18n.tSync('password.setup.new_security_title') + colors.reset);
569
1160
  const securityItems = i18n.tSync('password.setup.security_items');
570
1161
  if (Array.isArray(securityItems)) {
571
1162
  securityItems.forEach(item => {
572
- console.log(colors.gray + '• ' + item + colors.reset);
1163
+ lines.push(colors.gray + '• ' + item + colors.reset);
573
1164
  });
574
1165
  }
575
- console.log('');
576
- console.log(colors.yellow + i18n.tSync('password.setup.options_title') + colors.reset);
577
- console.log(colors.gray + '• ' + i18n.tSync('password.setup.option_set') + colors.reset);
578
- console.log(colors.gray + '• ' + i18n.tSync('password.setup.option_skip') + colors.reset);
579
- console.log('');
580
- console.log(colors.red + i18n.tSync('password.setup.warning_skip') + colors.reset);
581
- console.log('');
582
-
583
- console.log(colors.gray + '按任意键继续...' + colors.reset);
1166
+ lines.push('');
1167
+ lines.push(colors.yellow + i18n.tSync('password.setup.options_title') + colors.reset);
1168
+ lines.push(colors.gray + '• ' + i18n.tSync('password.setup.option_set') + colors.reset);
1169
+ lines.push(colors.gray + '• ' + i18n.tSync('password.setup.option_skip') + colors.reset);
1170
+ lines.push('');
1171
+ lines.push(colors.red + i18n.tSync('password.setup.warning_skip') + colors.reset);
1172
+ lines.push('');
1173
+ lines.push(colors.gray + '按任意键继续...' + colors.reset);
1174
+ screen.render(lines);
584
1175
 
585
1176
  // Wait for user to read the information
586
1177
  await new Promise((resolve) => {
@@ -656,14 +1247,16 @@ async function promptForPasswordSetup() {
656
1247
  * Confirm skip password setup
657
1248
  */
658
1249
  async function confirmSkipPassword() {
659
- console.log('');
660
- console.log(colors.bright + colors.red + '⚠️ ' + i18n.tSync('errors.password.confirm_skip_title') + colors.reset);
661
- console.log('');
662
- console.log(colors.gray + i18n.tSync('ui.general.after_skipping_password_setup') + colors.reset);
663
- console.log(colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[0] + colors.reset);
664
- console.log(colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[1] + colors.reset);
665
- console.log(colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[2] + colors.reset);
666
- console.log('');
1250
+ screen.render([
1251
+ '',
1252
+ colors.bright + colors.red + '⚠️ ' + i18n.tSync('errors.password.confirm_skip_title') + colors.reset,
1253
+ '',
1254
+ colors.gray + i18n.tSync('ui.general.after_skipping_password_setup') + colors.reset,
1255
+ colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[0] + colors.reset,
1256
+ colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[1] + colors.reset,
1257
+ colors.gray + '• ' + i18n.tSync('ui.general.password_skip_consequences')[2] + colors.reset,
1258
+ '',
1259
+ ]);
667
1260
 
668
1261
  // Ensure global menus are initialized
669
1262
  initializeGlobalMenus();
@@ -678,12 +1271,12 @@ async function confirmSkipPassword() {
678
1271
  if (choice === 0) {
679
1272
  try {
680
1273
  apiManager.skipPasswordSetup();
681
- console.log(colors.yellow + i18n.tSync('errors.password.setup_skipped') + colors.reset);
1274
+ screen.write(colors.yellow + i18n.tSync('errors.password.setup_skipped') + colors.reset + '\n');
682
1275
  await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
683
1276
  return true;
684
1277
  } catch (error) {
685
1278
  forceStdinCleanup();
686
- console.log(colors.red + `Operation failed: ${error.message}` + colors.reset);
1279
+ screen.write(colors.red + `Operation failed: ${error.message}` + colors.reset + '\n');
687
1280
  await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
688
1281
  return false;
689
1282
  }
@@ -699,147 +1292,114 @@ async function showApiManagementMenu() {
699
1292
  // Force cleanup stdin state before showing API management menu
700
1293
  forceStdinCleanup();
701
1294
 
702
- console.clear();
703
- console.log('');
704
- console.log(colors.bright + colors.orange + '📋 ' + await i18n.t('menu.api_management.title') + colors.reset);
705
- console.log('');
1295
+ screen.render([
1296
+ '',
1297
+ colors.bright + colors.orange + '📋 ' + await i18n.t('menu.api_management.title') + colors.reset,
1298
+ '',
1299
+ ]);
706
1300
 
707
1301
  // Check if this is first time usage and prompt for password setup
708
1302
  if (apiManager.isFirstTimeUsage()) {
709
1303
  const passwordChoice = await handleFirstTimePasswordSetup();
710
1304
  if (!passwordChoice) {
711
- // User chose to skip or canceled, return to main menu
712
1305
  return showMenu();
713
1306
  }
714
1307
  }
715
1308
 
716
1309
  // Build menu options based on password setup status
717
1310
  const menuOptions = [
718
- await i18n.t('menu.api_management.add_new'), // 0
719
- await i18n.t('menu.api_management.remove'), // 1
720
- await i18n.t('menu.api_management.switch'), // 2
721
- await i18n.t('menu.api_management.statistics') // 3
1311
+ await i18n.t('menu.api_management.add_new'), // 0
1312
+ await i18n.t('menu.api_management.edit'), // 1
1313
+ await i18n.t('menu.api_management.remove'), // 2
1314
+ await i18n.t('menu.api_management.switch'), // 3
1315
+ await i18n.t('menu.api_management.statistics'), // 4
1316
+ await i18n.t('menu.api_management.manual_upgrade') // 5
722
1317
  ];
723
1318
 
724
1319
  // Add import/export options only if password is set
725
1320
  if (apiManager.canUseImportExport()) {
726
- menuOptions.push(await i18n.t('menu.api_management.export')); // 4
727
- menuOptions.push(await i18n.t('menu.api_management.import')); // 5
728
- menuOptions.push(await i18n.t('menu.api_management.change_password')); // 6
1321
+ menuOptions.push(await i18n.t('menu.api_management.export')); // 6
1322
+ menuOptions.push(await i18n.t('menu.api_management.import')); // 7
1323
+ menuOptions.push(await i18n.t('menu.api_management.change_password')); // 8
729
1324
  }
730
1325
 
731
- // Add model upgrade settings (always available)
732
- menuOptions.push(await i18n.t('model_upgrade.settings_title')); // 4 or 7 (depending on import/export)
733
-
734
- menuOptions.push(await i18n.t('menu.api_management.back')); // 5 or 8
1326
+ menuOptions.push(await i18n.t('menu.api_management.back')); // last
735
1327
 
736
1328
  // Ensure global menus are initialized
737
1329
  initializeGlobalMenus();
738
1330
 
739
1331
  globalApiManagementMenu.setOptions(menuOptions);
740
1332
 
741
- const choice = await globalApiManagementMenu.navigate();
1333
+ const hintCallback = (index) => {
1334
+ if (!apiManager.hasExportPassword()) return null;
1335
+ const passwordHints = {};
1336
+ passwordHints[1] = i18n.tSync('hints.edit_password_required');
1337
+ passwordHints[2] = i18n.tSync('hints.remove_password_required');
1338
+ if (apiManager.canUseImportExport()) {
1339
+ passwordHints[6] = i18n.tSync('hints.export_password_required');
1340
+ passwordHints[7] = i18n.tSync('hints.import_password_required');
1341
+ }
1342
+ return passwordHints[index] || null;
1343
+ };
1344
+
1345
+ const choice = await globalApiManagementMenu.navigate(null, hintCallback);
742
1346
 
743
1347
  // Handle menu choices based on current menu options
744
1348
  if (choice === 0) { // Add New API
745
1349
  await addNewThirdPartyApi();
746
1350
  return showMenu();
747
- } else if (choice === 1) { // Remove API
748
- await removeThirdPartyApi();
749
- return showMenu();
750
- } else if (choice === 2) { // Switch Active API
1351
+ }
1352
+
1353
+ if (choice === 1) { // Edit API
1354
+ if (await passwordGuard(apiManager, 'edit')) {
1355
+ await editApi(apiManager);
1356
+ }
1357
+ return showApiManagementMenu();
1358
+ }
1359
+
1360
+ if (choice === 2) { // Remove API
1361
+ if (await passwordGuard(apiManager, 'delete')) {
1362
+ await removeThirdPartyApi();
1363
+ }
1364
+ return showApiManagementMenu();
1365
+ }
1366
+
1367
+ if (choice === 3) { // Switch Active API
751
1368
  await switchThirdPartyApi();
752
1369
  return showMenu();
753
- } else if (choice === 3) { // View API Statistics
1370
+ }
1371
+
1372
+ if (choice === 4) { // View API Statistics
754
1373
  await viewStatistics();
755
1374
  return showMenu();
756
- } else if (apiManager.canUseImportExport()) {
757
- // With import/export enabled: indices 4-8
758
- if (choice === 4) { // Export Configuration
1375
+ }
1376
+
1377
+ if (choice === 5) { // Manual Model Upgrade
1378
+ await performManualUpgrade();
1379
+ return showApiManagementMenu();
1380
+ }
1381
+
1382
+ if (apiManager.canUseImportExport()) {
1383
+ if (choice === 6) { // Export Configuration
759
1384
  await exportConfiguration();
760
- return showMenu();
761
- } else if (choice === 5) { // Import Configuration
1385
+ return showApiManagementMenu();
1386
+ }
1387
+
1388
+ if (choice === 7) { // Import Configuration
762
1389
  await importConfiguration();
763
- return showMenu();
764
- } else if (choice === 6) { // Change Password
765
- await changePassword();
766
- return showMenu();
767
- } else if (choice === 7) { // Model Upgrade Settings (NEW)
768
- return await showModelUpgradeSettings();
769
- } else if (choice === 8) { // Back to Main Menu
770
- return showMenu();
1390
+ return showApiManagementMenu();
771
1391
  }
772
- } else {
773
- // Without import/export: indices 4-5
774
- if (choice === 4) { // Model Upgrade Settings (NEW)
775
- return await showModelUpgradeSettings();
776
- } else if (choice === 5) { // Back to Main Menu
1392
+
1393
+ if (choice === 8) { // Change Password
1394
+ await changePassword();
777
1395
  return showMenu();
778
1396
  }
779
1397
  }
780
1398
 
781
- // Default fallback to main menu
1399
+ // Back to Main Menu (last item) or default fallback
782
1400
  return showMenu();
783
1401
  }
784
1402
 
785
- /**
786
- * Show model upgrade settings menu
787
- */
788
- async function showModelUpgradeSettings() {
789
- const versionChecker = require('./lib/utils/version-checker');
790
- const upgradeChecker = require('./lib/utils/model-upgrade-checker');
791
-
792
- console.clear();
793
- console.log('');
794
- console.log(colors.bright + colors.orange + '⚙️ ' + await i18n.t('model_upgrade.settings_title') + colors.reset);
795
- console.log('');
796
-
797
- const config = await versionChecker.loadConfig();
798
- const isAutoOn = config.autoModelUpgrade === true;
799
-
800
- console.log(colors.cyan + ' ' + await i18n.t('model_upgrade.current_config') + ':' + colors.reset);
801
- console.log(colors.gray + ' ' + await i18n.t('model_upgrade.auto_upgrade_label') + ': ' +
802
- (isAutoOn
803
- ? colors.green + await i18n.t('model_upgrade.auto_upgrade_on')
804
- : colors.dim + await i18n.t('model_upgrade.auto_upgrade_off')
805
- ) + colors.reset);
806
- console.log('');
807
-
808
- const menuOptions = [
809
- isAutoOn
810
- ? await i18n.t('model_upgrade.menu_toggle_auto_on')
811
- : await i18n.t('model_upgrade.menu_toggle_auto_off'),
812
- await i18n.t('model_upgrade.menu_manual_upgrade'),
813
- await i18n.t('model_upgrade.menu_back')
814
- ];
815
-
816
- initializeGlobalMenus();
817
- globalApiManagementMenu.setOptions(menuOptions);
818
- const choice = await globalApiManagementMenu.navigate();
819
-
820
- switch (choice) {
821
- case 0: // Toggle auto upgrade
822
- await versionChecker.setAutoModelUpgrade(!isAutoOn);
823
- console.log('');
824
- console.log(colors.green + '✓ ' + await i18n.t('model_upgrade.auto_upgrade_label') + ': ' +
825
- (!isAutoOn
826
- ? await i18n.t('model_upgrade.auto_upgrade_on')
827
- : await i18n.t('model_upgrade.auto_upgrade_off')
828
- ) + colors.reset);
829
- await waitForKey(await i18n.t('messages.prompts.press_any_key'));
830
- return showModelUpgradeSettings();
831
-
832
- case 1: // Manual upgrade
833
- await performManualUpgrade();
834
- return showModelUpgradeSettings();
835
-
836
- case 2: // Back
837
- case -1:
838
- default:
839
- return showApiManagementMenu();
840
- }
841
- }
842
-
843
1403
  /**
844
1404
  * Perform manual upgrade for all APIs with interactive confirmation
845
1405
  */
@@ -847,21 +1407,26 @@ async function performManualUpgrade() {
847
1407
  const { getLatestModel, getProvider } = require('./lib/presets/providers');
848
1408
  const { simpleInput } = require('./lib/ui/prompts');
849
1409
 
850
- console.clear();
851
- console.log('');
852
- console.log(colors.bright + colors.orange + '🔄 ' + await i18n.t('model_upgrade.manual_title') + colors.reset);
853
- console.log('');
854
-
855
1410
  const apis = apiManager.getApis();
856
1411
 
857
1412
  if (apis.length === 0) {
858
- console.log(colors.yellow + ' ' + await i18n.t('messages.info.no_apis') + colors.reset);
1413
+ screen.render([
1414
+ '',
1415
+ colors.bright + colors.orange + '🔄 ' + await i18n.t('model_upgrade.manual_title') + colors.reset,
1416
+ '',
1417
+ colors.yellow + ' ' + await i18n.t('messages.info.no_apis') + colors.reset,
1418
+ ]);
859
1419
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
860
1420
  return;
861
1421
  }
862
1422
 
863
- console.log(colors.gray + ' ' + await i18n.t('model_upgrade.manual_checking', apis.length) + colors.reset);
864
- console.log('');
1423
+ screen.render([
1424
+ '',
1425
+ colors.bright + colors.orange + '🔄 ' + await i18n.t('model_upgrade.manual_title') + colors.reset,
1426
+ '',
1427
+ colors.gray + ' ' + await i18n.t('model_upgrade.manual_checking', apis.length) + colors.reset,
1428
+ '',
1429
+ ]);
865
1430
 
866
1431
  let upgradedCount = 0;
867
1432
  let skippedUpToDate = 0;
@@ -872,87 +1437,164 @@ async function performManualUpgrade() {
872
1437
  const api = apis[i];
873
1438
  const latestModel = getLatestModel(api.model, api.provider);
874
1439
 
875
- console.log(colors.cyan + ' ─────────────────────────────────────────────────' + colors.reset);
876
- console.log(colors.bright + ` ${i + 1}/${apis.length} ${api.name}` + colors.reset);
877
- console.log(colors.gray + ' ' + await i18n.t('model_upgrade.manual_api_current', api.model) + colors.reset);
1440
+ screen.write(colors.cyan + ' ─────────────────────────────────────────────────' + colors.reset + '\n');
1441
+ screen.write(colors.bright + ` ${i + 1}/${apis.length} ${api.name}` + colors.reset + '\n');
1442
+ screen.write(colors.gray + ' ' + await i18n.t('model_upgrade.manual_api_current', api.model) + colors.reset + '\n');
878
1443
 
879
1444
  if (latestModel) {
880
- console.log(colors.green + ' ' + await i18n.t('model_upgrade.manual_api_latest', latestModel) + colors.reset);
881
- console.log('');
1445
+ screen.write(colors.green + ' ' + await i18n.t('model_upgrade.manual_api_latest', latestModel) + colors.reset + '\n');
1446
+ screen.write('\n');
882
1447
 
883
1448
  // Ask for confirmation
884
1449
  const answer = await simpleInput(colors.yellow + ' ' + await i18n.t('model_upgrade.manual_confirm') + ' ' + colors.reset);
885
1450
 
886
1451
  if (answer.toLowerCase() === 'y') {
887
- apiManager.updateApiModel(api.id, latestModel);
888
- console.log(colors.green + ' ✓ ' + await i18n.t('model_upgrade.manual_upgraded', api.model, latestModel) + colors.reset);
889
- upgradedCount++;
1452
+ try {
1453
+ apiManager.updateApiModel(api.id, latestModel);
1454
+ screen.write(colors.green + ' ✓ ' + await i18n.t('model_upgrade.manual_upgraded', api.model, latestModel) + colors.reset + '\n');
1455
+ upgradedCount++;
1456
+ } catch (error) {
1457
+ screen.write(colors.yellow + ' ⚠️ Skipped: ' + error.message + colors.reset + '\n');
1458
+ skippedByUser++;
1459
+ }
890
1460
  } else {
891
- console.log(colors.dim + ' ' + await i18n.t('model_upgrade.manual_skipped') + colors.reset);
1461
+ screen.write(colors.dim + ' ' + await i18n.t('model_upgrade.manual_skipped') + colors.reset + '\n');
892
1462
  skippedByUser++;
893
1463
  }
894
1464
  } else {
895
1465
  // No upgrade info available - check if model exists in provider
896
1466
  const provider = getProvider(api.provider);
897
1467
  if (provider && provider.models && provider.models.includes(api.model)) {
898
- // Model exists in provider, likely already latest or no alias defined
899
- console.log(colors.dim + ' ' + await i18n.t('model_upgrade.manual_api_uptodate') + colors.reset);
1468
+ screen.write(colors.dim + ' ' + await i18n.t('model_upgrade.manual_api_uptodate') + colors.reset + '\n');
900
1469
  skippedUpToDate++;
901
1470
  } else {
902
- console.log(colors.dim + ' ' + await i18n.t('model_upgrade.manual_api_no_info') + colors.reset);
1471
+ screen.write(colors.dim + ' ' + await i18n.t('model_upgrade.manual_api_no_info') + colors.reset + '\n');
903
1472
  skippedNoInfo++;
904
1473
  }
905
1474
  }
906
1475
 
907
- console.log('');
1476
+ screen.write('\n');
908
1477
  }
909
1478
 
910
- console.log(colors.cyan + ' ─────────────────────────────────────────────────' + colors.reset);
911
- console.log('');
912
- console.log(colors.green + ' ' + await i18n.t('model_upgrade.manual_complete') + colors.reset);
913
- console.log(colors.gray + ' ' + await i18n.t('model_upgrade.manual_stats_upgraded', upgradedCount) + colors.reset);
914
- console.log(colors.gray + ' ' + await i18n.t('model_upgrade.manual_stats_skipped',
1479
+ screen.write(colors.cyan + ' ─────────────────────────────────────────────────' + colors.reset + '\n');
1480
+ screen.write('\n');
1481
+ screen.write(colors.green + ' ' + await i18n.t('model_upgrade.manual_complete') + colors.reset + '\n');
1482
+ screen.write(colors.gray + ' ' + await i18n.t('model_upgrade.manual_stats_upgraded', upgradedCount) + colors.reset + '\n');
1483
+ screen.write(colors.gray + ' ' + await i18n.t('model_upgrade.manual_stats_skipped',
915
1484
  skippedUpToDate + skippedNoInfo + skippedByUser,
916
1485
  skippedUpToDate,
917
- skippedNoInfo) + colors.reset);
918
- console.log('');
1486
+ skippedNoInfo) + colors.reset + '\n');
1487
+ screen.write('\n');
919
1488
 
920
1489
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
921
1490
  }
922
1491
 
923
1492
  /**
924
- * Handle third-party API launch
1493
+ * Show API selection menu for select launch mode
925
1494
  */
926
- async function handleThirdPartyApiLaunch(skipPermissions = false) {
927
- try {
928
- const activeApi = apiManager.getActiveApi();
1495
+ async function showApiSelectMenu(skipPermissions = false) {
1496
+ const { padStringToWidth, getStringWidth } = require('./lib/utils/string-width');
1497
+ const { getProvider } = require('./lib/presets/providers');
929
1498
 
930
- if (!activeApi) {
931
- console.clear();
932
- showInfo(i18n.tSync('launch.no_active_api'), [
933
- i18n.tSync('launch.no_active_api_desc'),
934
- i18n.tSync('launch.add_configure_first')
935
- ]);
1499
+ const apis = apiManager.getApis();
936
1500
 
937
- await waitForKey(i18n.tSync('launch.press_key_return'));
938
- return showMenu();
939
- }
1501
+ if (apis.length === 0) {
1502
+ showInfo(i18n.tSync('hints.no_api_configured'), []);
1503
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
1504
+ return showMenu();
1505
+ }
940
1506
 
941
- // Record successful launch BEFORE launching (since process exits after)
942
- apiManager.recordSuccessfulLaunch();
1507
+ const activeApi = apiManager.getActiveApi();
943
1508
 
944
- launchClaudeWithApi(activeApi, skipPermissions);
1509
+ // Build menu items with two-column alignment
1510
+ // Compute max name width (including ● prefix) for consistent column alignment
1511
+ let maxNameWidth = 0;
1512
+ apis.forEach(api => {
1513
+ const w = getStringWidth('● ' + api.name);
1514
+ if (w > maxNameWidth) maxNameWidth = w;
1515
+ });
945
1516
 
946
- } catch (error) {
947
- // Record failed launch
948
- apiManager.recordFailedLaunch(error.message);
1517
+ const menuItems = apis.map((api) => {
1518
+ const prefix = (activeApi && activeApi.id === api.id) ? '● ' : ' ';
1519
+ const nameCol = prefix + api.name;
1520
+ return padStringToWidth(nameCol, maxNameWidth + 4) + api.model;
1521
+ });
1522
+ menuItems.push(i18n.tSync('menu.api_select.back'));
1523
+
1524
+ // Hint callback for each API item
1525
+ const hintCallback = (selectedIndex) => {
1526
+ if (selectedIndex >= apis.length) return null; // "Back" item
1527
+ const api = apis[selectedIndex];
1528
+ const providerConfig = getProvider(api.provider);
1529
+ const providerName = providerConfig ? providerConfig.name : (api.provider || 'Custom');
1530
+ const lastUsed = formatRelativeTime(api.lastUsed || null);
1531
+ const usageCount = api.usageCount || 0;
1532
+ return i18n.tSync('hints.api_select.info', api.name) + '\n' +
1533
+ i18n.tSync('hints.api_select.detail', providerName, api.model) + '\n' +
1534
+ i18n.tSync('hints.api_select.usage', usageCount, lastUsed);
1535
+ };
949
1536
 
950
- showError('Failed to launch with third-party API', [error.message]);
1537
+ screen.render([
1538
+ '',
1539
+ colors.bright + colors.orange + '🔗 ' + i18n.tSync('menu.api_select.title') + colors.reset,
1540
+ '',
1541
+ ]);
951
1542
 
952
- setTimeout(() => {
953
- showMenu();
954
- }, 2000);
1543
+ const selectMenu = new Menu();
1544
+ selectMenu.setOptions(menuItems);
1545
+ const choice = await selectMenu.navigate(null, hintCallback);
1546
+
1547
+ if (choice === -1 || choice === apis.length) {
1548
+ // Back or Esc
1549
+ return showMenu();
1550
+ }
1551
+
1552
+ // User selected an API
1553
+ const selectedApi = apis[choice];
1554
+ apiManager.setActiveApi(choice);
1555
+ apiManager.recordLaunchAttempt();
1556
+ screen.exitForHandoff();
1557
+ launchClaudeWithApi(selectedApi, skipPermissions, {
1558
+ rollbackFn: (errorMessage) => apiManager.rollbackLaunchAttempt(errorMessage)
1559
+ });
1560
+ }
1561
+
1562
+ /**
1563
+ * Handle third-party API launch
1564
+ */
1565
+ async function handleThirdPartyApiLaunch(skipPermissions = false) {
1566
+ const { loadConfigSync } = require('./lib/utils/version-checker');
1567
+ const config = loadConfigSync();
1568
+
1569
+ if (config.apiLaunchMode === 'select') {
1570
+ return showApiSelectMenu(skipPermissions);
1571
+ }
1572
+
1573
+ // Direct mode
1574
+ const apis = apiManager.getApis();
1575
+
1576
+ if (apis.length === 0) {
1577
+ showInfo(i18n.tSync('hints.no_api_configured'), []);
1578
+ await waitForKey(i18n.tSync('messages.prompts.press_any_key'));
1579
+ return showMenu();
1580
+ }
1581
+
1582
+ const activeApi = apiManager.getActiveApi();
1583
+
1584
+ if (activeApi === null) {
1585
+ showInfo(i18n.tSync('launch.no_active_api'), [
1586
+ i18n.tSync('launch.no_active_api_desc'),
1587
+ i18n.tSync('launch.add_configure_first')
1588
+ ]);
1589
+ await waitForKey(i18n.tSync('launch.press_key_return'));
1590
+ return showMenu();
955
1591
  }
1592
+
1593
+ apiManager.recordLaunchAttempt();
1594
+ screen.exitForHandoff();
1595
+ launchClaudeWithApi(activeApi, skipPermissions, {
1596
+ rollbackFn: (errorMessage) => apiManager.rollbackLaunchAttempt(errorMessage)
1597
+ });
956
1598
  }
957
1599
 
958
1600
  /**
@@ -961,14 +1603,17 @@ async function handleThirdPartyApiLaunch(skipPermissions = false) {
961
1603
  async function executeSelection(selectedIndex) {
962
1604
  switch (selectedIndex) {
963
1605
  case 0: // Launch Claude Code
1606
+ screen.exitForHandoff();
964
1607
  launchClaudeDefault();
965
1608
  break;
966
1609
 
967
1610
  case 1: // Launch Claude Code (Skip Permissions)
1611
+ screen.exitForHandoff();
968
1612
  launchClaudeSkipPermissions();
969
1613
  break;
970
1614
 
971
1615
  case 2: // Launch Claude Code (Enable Auto Mode)
1616
+ screen.exitForHandoff();
972
1617
  launchClaudeAutoMode();
973
1618
  break;
974
1619
 
@@ -983,15 +1628,16 @@ async function executeSelection(selectedIndex) {
983
1628
  case 5: // 3rd-party API Management
984
1629
  return await showApiManagementMenu();
985
1630
 
986
- case 6: // Language Settings
987
- return await showLanguageSettings();
1631
+ case 6: // Configuration Management
1632
+ return await showConfigManagement();
988
1633
 
989
1634
  case 7: // Version Update Check
990
1635
  return await showVersionUpdateCheck();
991
1636
 
992
1637
  case 8: // Exit
993
- console.log('');
994
- console.log(colors.green + '👋 ' + await i18n.t('menu.main.exit') + '!' + colors.reset);
1638
+ screen.write('\n');
1639
+ screen.write(colors.green + '👋 ' + await i18n.t('menu.main.exit') + '!' + colors.reset + '\n');
1640
+ screen.exit();
995
1641
  process.exit(0);
996
1642
  break;
997
1643
 
@@ -1001,6 +1647,114 @@ async function executeSelection(selectedIndex) {
1001
1647
  }
1002
1648
  }
1003
1649
 
1650
+ /**
1651
+ * Show configuration management submenu
1652
+ */
1653
+ async function showConfigManagement(restoreIndex = 0) {
1654
+ const versionChecker = require('./lib/utils/version-checker');
1655
+ const { getStringWidth, padStringToWidth } = require('./lib/utils/string-width');
1656
+
1657
+ const config = await versionChecker.loadConfig();
1658
+
1659
+ // Build labels with padding to align current values
1660
+ const labelWidth = 40;
1661
+
1662
+ const langName = i18n.getCurrentLanguageName();
1663
+ const autoUpgradeVal = config.autoModelUpgrade
1664
+ ? i18n.tSync('config.values.on')
1665
+ : i18n.tSync('config.values.off');
1666
+ const upgradeNotifVal = config.showModelUpgradeNotification !== false
1667
+ ? i18n.tSync('config.values.on')
1668
+ : i18n.tSync('config.values.off');
1669
+ const telemetryVal = config.disableTelemetry !== false
1670
+ ? i18n.tSync('config.values.recommended_off')
1671
+ : i18n.tSync('config.values.on');
1672
+ const launchModeVal = config.apiLaunchMode === 'select'
1673
+ ? i18n.tSync('config.values.select_mode')
1674
+ : i18n.tSync('config.values.direct_mode');
1675
+ const noFlickerVal = config.noFlicker !== false
1676
+ ? i18n.tSync('config.values.recommended_on')
1677
+ : i18n.tSync('config.values.off');
1678
+
1679
+ const menuOptions = [
1680
+ padStringToWidth(i18n.tSync('menu.config.language'), labelWidth) + langName,
1681
+ padStringToWidth(i18n.tSync('menu.config.auto_model_upgrade'), labelWidth) + autoUpgradeVal,
1682
+ padStringToWidth(i18n.tSync('menu.config.model_upgrade_notification'), labelWidth) + upgradeNotifVal,
1683
+ padStringToWidth(i18n.tSync('menu.config.telemetry'), labelWidth) + telemetryVal,
1684
+ padStringToWidth(i18n.tSync('menu.config.api_launch_mode'), labelWidth) + launchModeVal,
1685
+ padStringToWidth(i18n.tSync('menu.config.no_flicker'), labelWidth) + noFlickerVal,
1686
+ i18n.tSync('menu.config.back')
1687
+ ];
1688
+
1689
+ // Synchronous hint callback for config items
1690
+ const hintCallback = (selectedIndex) => {
1691
+ switch (selectedIndex) {
1692
+ case 0: return i18n.tSync('hints.config.language', langName);
1693
+ case 1: return i18n.tSync('hints.config.auto_upgrade');
1694
+ case 2: return i18n.tSync('hints.config.upgrade_notification');
1695
+ case 3: return i18n.tSync('hints.config.telemetry');
1696
+ case 4: return i18n.tSync('hints.config.launch_mode');
1697
+ case 5: return i18n.tSync('hints.config.no_flicker');
1698
+ default: return null;
1699
+ }
1700
+ };
1701
+
1702
+ screen.render([
1703
+ '',
1704
+ colors.bright + colors.orange + '⚙️ ' + i18n.tSync('menu.config.title') + colors.reset,
1705
+ '',
1706
+ ]);
1707
+
1708
+ initializeGlobalMenus();
1709
+ globalConfigMenu.setOptions(menuOptions);
1710
+ globalConfigMenu.selectedIndex = restoreIndex;
1711
+ const choice = await globalConfigMenu.navigate(null, hintCallback);
1712
+
1713
+ switch (choice) {
1714
+ case 0: // Language Settings
1715
+ return await showLanguageSettings();
1716
+
1717
+ case 1: { // Auto Model Upgrade toggle
1718
+ const newVal = !config.autoModelUpgrade;
1719
+ config.autoModelUpgrade = newVal;
1720
+ await versionChecker.saveConfig(config);
1721
+ return showConfigManagement(1);
1722
+ }
1723
+
1724
+ case 2: { // Model Upgrade Notification toggle
1725
+ const newVal = config.showModelUpgradeNotification === false ? true : false;
1726
+ config.showModelUpgradeNotification = newVal;
1727
+ await versionChecker.saveConfig(config);
1728
+ return showConfigManagement(2);
1729
+ }
1730
+
1731
+ case 3: { // Telemetry toggle
1732
+ const newVal = config.disableTelemetry === false ? true : false;
1733
+ config.disableTelemetry = newVal;
1734
+ await versionChecker.saveConfig(config);
1735
+ return showConfigManagement(3);
1736
+ }
1737
+
1738
+ case 4: { // API Launch Mode toggle
1739
+ const newVal = config.apiLaunchMode === 'select' ? 'direct' : 'select';
1740
+ config.apiLaunchMode = newVal;
1741
+ await versionChecker.saveConfig(config);
1742
+ return showConfigManagement(4);
1743
+ }
1744
+
1745
+ case 5: { // noFlicker toggle
1746
+ config.noFlicker = !config.noFlicker;
1747
+ await versionChecker.saveConfig(config);
1748
+ return showConfigManagement(5);
1749
+ }
1750
+
1751
+ case 6: // Back
1752
+ case -1:
1753
+ default:
1754
+ return showMenu();
1755
+ }
1756
+ }
1757
+
1004
1758
  /**
1005
1759
  * Show main menu
1006
1760
  */
@@ -1047,6 +1801,9 @@ async function showMenu() {
1047
1801
  try {
1048
1802
  const upgradeChecker = require('./lib/utils/model-upgrade-checker');
1049
1803
  const autoUpgrade = await upgradeChecker.isAutoUpgradeEnabled();
1804
+ const versionChecker = require('./lib/utils/version-checker');
1805
+ const launcherConfig = await versionChecker.loadConfig();
1806
+ const showUpgradeNotif = launcherConfig.showModelUpgradeNotification !== false;
1050
1807
 
1051
1808
  if (autoUpgrade) {
1052
1809
  // Auto upgrade enabled: always check and upgrade (bypass cache)
@@ -1062,8 +1819,8 @@ async function showMenu() {
1062
1819
  }
1063
1820
  }
1064
1821
  }
1065
- } else {
1066
- // Auto upgrade disabled: use cache for notification
1822
+ } else if (showUpgradeNotif) {
1823
+ // Auto upgrade disabled but notification enabled: use cache for notification
1067
1824
  const result = await upgradeChecker.checkForModelUpgrades(apiManager);
1068
1825
  if (result.needsCheck && result.upgrades.length > 0) {
1069
1826
  const first = result.upgrades[0];
@@ -1102,40 +1859,109 @@ async function showMenu() {
1102
1859
  await i18n.t('menu.main.launch_api'),
1103
1860
  await i18n.t('menu.main.launch_api_skip'),
1104
1861
  await i18n.t('menu.main.api_management'),
1105
- await i18n.t('menu.main.language_settings'),
1862
+ await i18n.t('menu.main.config_management'),
1106
1863
  await i18n.t('menu.main.version_check'),
1107
1864
  await i18n.t('menu.main.exit')
1108
1865
  ];
1109
1866
 
1110
- // Pre-compute hint texts synchronously for menu callback
1867
+ // Pre-compute all hint data synchronously for menu callback
1111
1868
  const hintAutoMode = i18n.tSync('hints.auto_mode_info');
1869
+
1112
1870
  const activeApi = apiManager.getActiveApi();
1113
- let hintApiInfo = null;
1871
+ const apis = apiManager.getApis();
1872
+ const apiCount = apis.length;
1873
+ const { getProvider } = require('./lib/presets/providers');
1874
+
1875
+ // Pre-compute config for hints
1876
+ const hintConfig = require('./lib/utils/version-checker').loadConfigSync();
1877
+ const hintApiLaunchMode = hintConfig.apiLaunchMode || 'direct';
1878
+
1879
+ // Pre-compute active API details
1880
+ let activeApiName = i18n.tSync('hints.select_mode_active_none');
1881
+ let activeProviderName = '';
1882
+ let activeModel = '';
1883
+ let activeLastUsed = '';
1114
1884
  if (activeApi) {
1115
- const { getProvider } = require('./lib/presets/providers');
1116
1885
  const providerConfig = getProvider(activeApi.provider);
1117
- const providerName = providerConfig ? providerConfig.name : (activeApi.provider || 'Custom');
1118
- hintApiInfo = i18n.tSync('hints.active_api_info', providerName, activeApi.model);
1886
+ activeProviderName = providerConfig ? providerConfig.name : (activeApi.provider || 'Custom');
1887
+ activeModel = activeApi.model;
1888
+ activeApiName = activeApi.name;
1889
+ activeLastUsed = formatRelativeTime(activeApi.lastUsed || null);
1890
+ }
1891
+
1892
+ // Pre-compute API hints for indices 3, 4
1893
+ let hintApiLines = null;
1894
+ if (apiCount === 0) {
1895
+ hintApiLines = i18n.tSync('hints.no_api_configured');
1896
+ } else if (hintApiLaunchMode === 'direct') {
1897
+ if (activeApi) {
1898
+ hintApiLines =
1899
+ i18n.tSync('hints.direct_mode_desc') + '\n' +
1900
+ i18n.tSync('hints.direct_mode_api_info', activeApiName, activeProviderName) + '\n' +
1901
+ i18n.tSync('hints.direct_mode_api_detail', activeModel, activeLastUsed) + '\n' +
1902
+ i18n.tSync('hints.direct_mode_change');
1903
+ } else {
1904
+ hintApiLines =
1905
+ i18n.tSync('hints.direct_mode_no_active') + '\n' +
1906
+ i18n.tSync('hints.direct_mode_no_active_detail', apiCount) + '\n' +
1907
+ '' + '\n' +
1908
+ i18n.tSync('hints.direct_mode_change');
1909
+ }
1119
1910
  } else {
1120
- hintApiInfo = i18n.tSync('hints.no_active_api');
1911
+ // select mode
1912
+ hintApiLines =
1913
+ i18n.tSync('hints.select_mode_desc') + '\n' +
1914
+ i18n.tSync('hints.select_mode_change') + '\n' +
1915
+ '' + '\n' +
1916
+ i18n.tSync('hints.select_mode_api_count', apiCount, activeApiName);
1121
1917
  }
1122
1918
 
1919
+ // Pre-compute API management hint (index 5)
1920
+ const hintApiMgmt = i18n.tSync('hints.api_management_info', apiCount, activeApiName);
1921
+
1922
+ // Pre-compute config summary hint (index 6)
1923
+ const cfgLangName = i18n.getCurrentLanguageName();
1924
+ const cfgLaunchMode = hintApiLaunchMode === 'select'
1925
+ ? i18n.tSync('config.values.select_mode')
1926
+ : i18n.tSync('config.values.direct_mode');
1927
+ const cfgTelemetry = hintConfig.disableTelemetry !== false
1928
+ ? i18n.tSync('config.values.off')
1929
+ : i18n.tSync('config.values.on');
1930
+ const cfgNoFlicker = hintConfig.noFlicker !== false
1931
+ ? i18n.tSync('config.values.on')
1932
+ : i18n.tSync('config.values.off');
1933
+ const hintConfigSummary = i18n.tSync('hints.config_summary', cfgLangName, cfgLaunchMode, cfgTelemetry, cfgNoFlicker);
1934
+
1123
1935
  // Synchronous hint callback — must not use await
1124
1936
  const hintCallback = (selectedIndex) => {
1125
1937
  switch (selectedIndex) {
1126
- case 2: return hintAutoMode;
1127
- case 3: return hintApiInfo;
1128
- case 4: return hintApiInfo;
1129
- default: return null;
1938
+ case 0:
1939
+ case 1:
1940
+ return null;
1941
+ case 2:
1942
+ return hintAutoMode;
1943
+ case 3:
1944
+ case 4:
1945
+ return hintApiLines;
1946
+ case 5:
1947
+ return hintApiMgmt;
1948
+ case 6:
1949
+ return hintConfigSummary;
1950
+ case 7:
1951
+ case 8:
1952
+ return null;
1953
+ default:
1954
+ return null;
1130
1955
  }
1131
1956
  };
1132
1957
 
1133
1958
  globalMainMenu.setOptions(menuOptions);
1134
- const selection = await globalMainMenu.navigate(false, displayInfo || null, hintCallback);
1959
+ const selection = await globalMainMenu.navigate(displayInfo || null, hintCallback);
1135
1960
 
1136
1961
  if (selection === -1) {
1137
- console.log('');
1138
- console.log(colors.green + '👋 ' + await i18n.t('menu.main.exit') + '!' + colors.reset);
1962
+ screen.write('\n');
1963
+ screen.write(colors.green + '👋 ' + await i18n.t('menu.main.exit') + '!' + colors.reset + '\n');
1964
+ screen.exit();
1139
1965
  process.exit(0);
1140
1966
  } else {
1141
1967
  await executeSelection(selection);
@@ -1150,26 +1976,22 @@ async function showMenu() {
1150
1976
  * Export configuration with password encryption
1151
1977
  */
1152
1978
  async function exportConfiguration() {
1153
- console.clear();
1154
- console.log('');
1155
- console.log(colors.bright + colors.orange + '💾 ' + await i18n.t('import_export.export.title') + colors.reset);
1156
- console.log('');
1157
-
1158
- // Add export function description
1159
- console.log(colors.cyan + '📄 ' + i18n.tSync('import_export.export.description_title') + colors.reset);
1160
- console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[0] + colors.reset);
1161
- console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[1] + colors.reset);
1162
- console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[2] + colors.reset);
1163
- console.log(colors.gray + '' + i18n.tSync('import_export.export.description_items')[3] + colors.reset);
1164
- console.log('');
1979
+ screen.render([
1980
+ '',
1981
+ colors.bright + colors.orange + '💾 ' + await i18n.t('import_export.export.title') + colors.reset,
1982
+ '',
1983
+ // Export function description
1984
+ colors.cyan + '📄 ' + i18n.tSync('import_export.export.description_title') + colors.reset,
1985
+ colors.gray + ' ' + i18n.tSync('import_export.export.description_items')[0] + colors.reset,
1986
+ colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[1] + colors.reset,
1987
+ colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[2] + colors.reset,
1988
+ colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[3] + colors.reset,
1989
+ '',
1990
+ ]);
1165
1991
 
1166
1992
  // Verify password before export
1167
- const verified = await verifyExportPassword(apiManager, 'export');
1168
- if (!verified) {
1169
- console.log('');
1170
- await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1171
- return;
1172
- }
1993
+ const verified = await passwordGuard(apiManager, 'export');
1994
+ if (!verified) return;
1173
1995
 
1174
1996
  try {
1175
1997
  // Get export data
@@ -1183,62 +2005,59 @@ async function exportConfiguration() {
1183
2005
  // Write JSON file
1184
2006
  fs.writeFileSync(filePath, exportData, 'utf8');
1185
2007
 
1186
- console.log(colors.green + '✓ ' + i18n.tSync('import_export.export.success_title') + colors.reset);
1187
- console.log('');
1188
- console.log(colors.cyan + '📁 ' + i18n.tSync('import_export.export.details_title') + colors.reset);
1189
- console.log(colors.gray + ` • ` + i18n.tSync('import_export.export.details_file_saved', filePath) + colors.reset);
1190
- console.log(colors.gray + ` • ` + i18n.tSync('import_export.export.details_export_dir', exportDir) + colors.reset);
1191
- console.log(colors.gray + ` • ` + i18n.tSync('import_export.export.details_filename', filename) + colors.reset);
1192
- console.log('');
2008
+ screen.write(colors.green + '✓ ' + i18n.tSync('import_export.export.success_title') + colors.reset + '\n');
2009
+ screen.write('\n');
2010
+ screen.write(colors.cyan + '📁 ' + i18n.tSync('import_export.export.details_title') + colors.reset + '\n');
2011
+ screen.write(colors.gray + ` • ` + i18n.tSync('import_export.export.details_file_saved', filePath) + colors.reset + '\n');
2012
+ screen.write(colors.gray + ` • ` + i18n.tSync('import_export.export.details_export_dir', exportDir) + colors.reset + '\n');
2013
+ screen.write(colors.gray + ` • ` + i18n.tSync('import_export.export.details_filename', filename) + colors.reset + '\n');
2014
+ screen.write('\n');
1193
2015
 
1194
2016
  // Open file with default application
1195
- console.log(colors.yellow + '🔍 ' + i18n.tSync('import_export.export.opening_file') + colors.reset);
2017
+ screen.write(colors.yellow + '🔍 ' + i18n.tSync('import_export.export.opening_file') + colors.reset + '\n');
1196
2018
  openFileWithDefault(filePath);
1197
2019
 
1198
- console.log('');
1199
- console.log(colors.cyan + '💡 ' + i18n.tSync('import_export.export.tips_title') + colors.reset);
1200
- console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.tips_items')[0] + colors.reset);
1201
- console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.tips_items')[1] + colors.reset);
2020
+ screen.write('\n');
2021
+ screen.write(colors.cyan + '💡 ' + i18n.tSync('import_export.export.tips_title') + colors.reset + '\n');
2022
+ screen.write(colors.gray + ' • ' + i18n.tSync('import_export.export.tips_items')[0] + colors.reset + '\n');
2023
+ screen.write(colors.gray + ' • ' + i18n.tSync('import_export.export.tips_items')[1] + colors.reset + '\n');
1202
2024
 
1203
2025
  } catch (error) {
1204
2026
  forceStdinCleanup();
1205
- console.log(colors.red + `❌ Export failed: ${error.message}` + colors.reset);
2027
+ screen.write(colors.red + `❌ Export failed: ${error.message}` + colors.reset + '\n');
1206
2028
  }
1207
2029
 
1208
- console.log('');
1209
- await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
2030
+ screen.write('\n');
2031
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
1210
2032
  }
1211
2033
 
1212
2034
  /**
1213
2035
  * Import configuration from plaintext JSON
1214
2036
  */
1215
2037
  async function importConfiguration() {
1216
- console.clear();
1217
- console.log('');
1218
- console.log(colors.bright + colors.orange + '📥 ' + await i18n.t('import_export.import.title') + colors.reset);
1219
- console.log('');
1220
-
1221
- // Add import function description
1222
- console.log(colors.cyan + '📄 ' + i18n.tSync('ui.general.import_function_description') + colors.reset);
1223
- console.log(colors.gray + ' • ' + i18n.tSync('import_export.export.description_items')[0] + colors.reset);
1224
- console.log(colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[0] + colors.reset);
1225
- console.log(colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[1] + colors.reset);
1226
- console.log(colors.gray + '' + i18n.tSync('ui.general.import_description_items')[2] + colors.reset);
1227
- console.log('');
2038
+ screen.render([
2039
+ '',
2040
+ colors.bright + colors.orange + '📥 ' + await i18n.t('import_export.import.title') + colors.reset,
2041
+ '',
2042
+ // Import function description
2043
+ colors.cyan + '📄 ' + i18n.tSync('ui.general.import_function_description') + colors.reset,
2044
+ colors.gray + ' ' + i18n.tSync('import_export.export.description_items')[0] + colors.reset,
2045
+ colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[0] + colors.reset,
2046
+ colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[1] + colors.reset,
2047
+ colors.gray + ' • ' + i18n.tSync('ui.general.import_description_items')[2] + colors.reset,
2048
+ '',
2049
+ ]);
1228
2050
 
1229
2051
  // Verify password identity
1230
- const passwordVerified = await verifyExportPassword(apiManager, 'import');
1231
- if (!passwordVerified) {
1232
- await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1233
- return;
1234
- }
2052
+ const passwordVerified = await passwordGuard(apiManager, 'import');
2053
+ if (!passwordVerified) return;
1235
2054
 
1236
- console.log('');
1237
- console.log(colors.cyan + '📁 ' + i18n.tSync('ui.general.file_input_required') + colors.reset);
1238
- console.log(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[0] + colors.reset);
1239
- console.log(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[1] + colors.reset);
1240
- console.log(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[2] + colors.reset);
1241
- console.log('');
2055
+ screen.write('\n');
2056
+ screen.write(colors.cyan + '📁 ' + i18n.tSync('ui.general.file_input_required') + colors.reset + '\n');
2057
+ screen.write(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[0] + colors.reset + '\n');
2058
+ screen.write(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[1] + colors.reset + '\n');
2059
+ screen.write(colors.gray + ' • ' + i18n.tSync('ui.general.file_input_items')[2] + colors.reset + '\n');
2060
+ screen.write('\n');
1242
2061
 
1243
2062
  const { simpleInput } = require('./lib/ui/prompts');
1244
2063
 
@@ -1253,85 +2072,86 @@ async function importConfiguration() {
1253
2072
  const filePath = await simpleInput(colors.green + filePrompt + colors.reset);
1254
2073
 
1255
2074
  if (!filePath) {
1256
- console.log(colors.red + i18n.tSync('ui.general.file_path_empty') + colors.reset);
2075
+ screen.write(colors.red + i18n.tSync('ui.general.file_path_empty') + colors.reset + '\n');
1257
2076
  if (attempts < maxAttempts) {
1258
- console.log('');
2077
+ screen.write('\n');
1259
2078
  continue;
1260
2079
  } else {
1261
- console.log(colors.red + i18n.tSync('ui.general.max_attempts_import_cancelled') + colors.reset);
2080
+ screen.write(colors.red + i18n.tSync('ui.general.max_attempts_import_cancelled') + colors.reset + '\n');
1262
2081
  await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1263
2082
  return;
1264
2083
  }
1265
2084
  }
1266
2085
 
1267
2086
  // Validate file
1268
- console.log('');
1269
- console.log(colors.yellow + i18n.tSync('ui.general.validating_file') + colors.reset);
2087
+ screen.write('\n');
2088
+ screen.write(colors.yellow + i18n.tSync('ui.general.validating_file') + colors.reset + '\n');
1270
2089
  const validation = validateImportFile(filePath);
1271
2090
 
1272
2091
  if (!validation.valid) {
1273
- console.log(colors.red + '❌ ' + i18n.tSync('ui.general.file_validation_failed', validation.error) + colors.reset);
2092
+ screen.write(colors.red + '❌ ' + i18n.tSync('ui.general.file_validation_failed', validation.error) + colors.reset + '\n');
1274
2093
  if (attempts < maxAttempts) {
1275
- console.log(colors.yellow + i18n.tSync('ui.general.check_file_path_json') + colors.reset);
1276
- console.log('');
2094
+ screen.write(colors.yellow + i18n.tSync('ui.general.check_file_path_json') + colors.reset + '\n');
2095
+ screen.write('\n');
1277
2096
  continue;
1278
2097
  } else {
1279
- console.log(colors.red + i18n.tSync('ui.general.max_attempts_import_cancelled') + colors.reset);
2098
+ screen.write(colors.red + i18n.tSync('ui.general.max_attempts_import_cancelled') + colors.reset + '\n');
1280
2099
  await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1281
2100
  return;
1282
2101
  }
1283
2102
  }
1284
2103
 
1285
2104
  // File is valid, proceed with import
1286
- console.log(colors.green + i18n.tSync('ui.general.file_validation_successful') + colors.reset);
1287
- console.log('');
2105
+ screen.write(colors.green + i18n.tSync('ui.general.file_validation_successful') + colors.reset + '\n');
2106
+ screen.write('\n');
1288
2107
 
1289
2108
  try {
1290
2109
  // Import the validated configuration data
1291
2110
  const result = apiManager.importConfigAuthenticated(validation.data);
1292
2111
 
1293
- console.log(colors.green + i18n.tSync('ui.general.import_successful') + colors.reset);
1294
- console.log('');
1295
- console.log(colors.cyan + i18n.tSync('ui.general.import_statistics') + colors.reset);
2112
+ screen.write(colors.green + i18n.tSync('ui.general.import_successful') + colors.reset + '\n');
2113
+ screen.write('\n');
2114
+ screen.write(colors.cyan + i18n.tSync('ui.general.import_statistics') + colors.reset + '\n');
1296
2115
  const importItems = i18n.tSync('ui.general.import_stats_items');
1297
- console.log(colors.gray + ` • ` + importItems[0].replace('{0}', result.imported) + colors.reset);
1298
- console.log(colors.gray + ` • ` + importItems[1].replace('{1}', result.skipped) + colors.reset);
1299
- console.log(colors.gray + ` • ` + importItems[2] + colors.reset);
1300
- console.log(colors.gray + ` • ` + importItems[3].replace('{0}', path.resolve(filePath)) + colors.reset);
2116
+ screen.write(colors.gray + ` • ` + importItems[0].replace('{0}', result.imported) + colors.reset + '\n');
2117
+ screen.write(colors.gray + ` • ` + importItems[1].replace('{1}', result.skipped) + colors.reset + '\n');
2118
+ screen.write(colors.gray + ` • ` + importItems[2] + colors.reset + '\n');
2119
+ screen.write(colors.gray + ` • ` + importItems[3].replace('{0}', path.resolve(filePath)) + colors.reset + '\n');
1301
2120
 
1302
2121
  break; // Success, exit the loop
1303
2122
 
1304
2123
  } catch (error) {
1305
2124
  forceStdinCleanup();
1306
- console.log(colors.red + `❌ Import failed: ${error.message}` + colors.reset);
2125
+ screen.write(colors.red + `❌ Import failed: ${error.message}` + colors.reset + '\n');
1307
2126
  if (attempts < maxAttempts) {
1308
- console.log(colors.yellow + i18n.tSync('ui.general.import_tips')[0] + colors.reset);
1309
- console.log('');
2127
+ screen.write(colors.yellow + i18n.tSync('ui.general.import_tips')[0] + colors.reset + '\n');
2128
+ screen.write('\n');
1310
2129
  continue;
1311
2130
  } else {
1312
- console.log(colors.red + i18n.tSync('ui.general.max_attempts_import_failed') + colors.reset);
2131
+ screen.write(colors.red + i18n.tSync('ui.general.max_attempts_import_failed') + colors.reset + '\n');
1313
2132
  }
1314
2133
  }
1315
2134
  }
1316
2135
 
1317
- console.log('');
1318
- await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
2136
+ screen.write('\n');
2137
+ await waitForKey(i18n.tSync('ui.general.press_any_key_continue'));
1319
2138
  }
1320
2139
 
1321
2140
  /**
1322
2141
  * Change password
1323
2142
  */
1324
2143
  async function changePassword() {
1325
- console.clear();
1326
- console.log('');
1327
- console.log(colors.bright + colors.orange + '🔑 ' + i18n.tSync('errors.password.change_password_title') + colors.reset);
1328
- console.log('');
2144
+ screen.render([
2145
+ '',
2146
+ colors.bright + colors.orange + '🔑 ' + i18n.tSync('errors.password.change_password_title') + colors.reset,
2147
+ '',
2148
+ ]);
1329
2149
 
1330
2150
  // Use unified password change module
1331
2151
  const success = await changePasswordModule(apiManager);
1332
2152
 
1333
2153
  if (success) {
1334
- console.log('');
2154
+ screen.write('\n');
1335
2155
  }
1336
2156
 
1337
2157
  await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
@@ -1346,16 +2166,16 @@ async function showLanguageSettings() {
1346
2166
  const currentLanguage = i18n.getCurrentLanguage();
1347
2167
  const currentLanguageName = i18n.getCurrentLanguageName();
1348
2168
 
1349
- console.clear();
1350
- console.log('');
1351
- console.log(colors.bright + colors.orange + '🌍 ' + await i18n.t('menu.language.title') + colors.reset);
1352
- console.log('');
1353
-
1354
- // Show current language
1355
- console.log(colors.cyan + await i18n.t('menu.language.current', currentLanguageName) + colors.reset);
1356
- console.log('');
1357
- console.log(colors.yellow + await i18n.t('menu.language.select_prompt') + colors.reset);
1358
- console.log('');
2169
+ screen.render([
2170
+ '',
2171
+ colors.bright + colors.orange + '🌍 ' + await i18n.t('menu.language.title') + colors.reset,
2172
+ '',
2173
+ // Show current language
2174
+ colors.cyan + await i18n.t('menu.language.current', currentLanguageName) + colors.reset,
2175
+ '',
2176
+ colors.yellow + await i18n.t('menu.language.select_prompt') + colors.reset,
2177
+ '',
2178
+ ]);
1359
2179
 
1360
2180
  // Create menu options
1361
2181
  const languageOptions = [];
@@ -1388,14 +2208,14 @@ async function showLanguageSettings() {
1388
2208
  }
1389
2209
 
1390
2210
  // Switch language
1391
- console.clear();
1392
- console.log('');
1393
- console.log(colors.yellow + await i18n.t('status.switching_language') + colors.reset);
2211
+ screen.render([
2212
+ '',
2213
+ colors.yellow + await i18n.t('status.switching_language') + colors.reset,
2214
+ ]);
1394
2215
 
1395
2216
  try {
1396
2217
  await i18n.setLanguage(selectedLangCode);
1397
2218
 
1398
- console.clear();
1399
2219
  const newLanguageName = i18n.getCurrentLanguageName();
1400
2220
  showSuccess(await i18n.t('messages.success.language_changed'), [
1401
2221
  await i18n.t('menu.language.changed_success', newLanguageName)
@@ -1424,18 +2244,18 @@ async function showLanguageSettings() {
1424
2244
  */
1425
2245
  async function showVersionUpdateCheck() {
1426
2246
  try {
1427
- console.clear();
1428
- console.log('');
1429
- console.log(colors.bright + colors.orange + '🔄 ' + await i18n.t('version_check.title') + colors.reset);
1430
- console.log('');
1431
-
1432
- console.log(colors.cyan + await i18n.t('version_check.checking') + colors.reset);
1433
- console.log(colors.gray + await i18n.t('version_check.please_wait') + colors.reset);
1434
- console.log('');
2247
+ screen.render([
2248
+ '',
2249
+ colors.bright + colors.orange + '🔄 ' + await i18n.t('version_check.title') + colors.reset,
2250
+ '',
2251
+ colors.cyan + await i18n.t('version_check.checking') + colors.reset,
2252
+ colors.gray + await i18n.t('version_check.please_wait') + colors.reset,
2253
+ '',
2254
+ ]);
1435
2255
 
1436
2256
  // Show progress indicator
1437
2257
  const progressInterval = setInterval(() => {
1438
- process.stdout.write('.');
2258
+ screen.write('.');
1439
2259
  }, 500);
1440
2260
 
1441
2261
  try {
@@ -1444,46 +2264,46 @@ async function showVersionUpdateCheck() {
1444
2264
 
1445
2265
  // Stop progress indicator
1446
2266
  clearInterval(progressInterval);
1447
- console.log('\n');
2267
+ screen.write('\n\n');
1448
2268
 
1449
2269
  if (result.error) {
1450
2270
  // Handle errors (timeout, network, etc.)
1451
- console.log(colors.red + '❌ ' + await i18n.t('version_check.error') + colors.reset);
1452
- console.log(colors.red + ' ' + result.error + colors.reset);
1453
- console.log('');
1454
- console.log(colors.gray + await i18n.t('version_check.error_tips') + colors.reset);
2271
+ screen.write(colors.red + '❌ ' + await i18n.t('version_check.error') + colors.reset + '\n');
2272
+ screen.write(colors.red + ' ' + result.error + colors.reset + '\n');
2273
+ screen.write('\n');
2274
+ screen.write(colors.gray + await i18n.t('version_check.error_tips') + colors.reset + '\n');
1455
2275
  } else if (result.available) {
1456
2276
  // Update available
1457
- console.log(colors.yellow + '🎉 ' + await i18n.t('version_check.update_available') + colors.reset);
1458
- console.log('');
1459
- console.log(colors.cyan + ' ' + await i18n.t('version_check.current_version', result.currentVersion) + colors.reset);
1460
- console.log(colors.green + ' ' + await i18n.t('version_check.latest_version', result.latestVersion) + colors.reset);
1461
- console.log('');
1462
- console.log(colors.yellow + '💡 ' + await i18n.t('version_check.update_command') + colors.reset);
1463
- console.log(colors.yellow + ' npm update -g @kikkimo/claude-launcher' + colors.reset);
2277
+ screen.write(colors.yellow + '🎉 ' + await i18n.t('version_check.update_available') + colors.reset + '\n');
2278
+ screen.write('\n');
2279
+ screen.write(colors.cyan + ' ' + await i18n.t('version_check.current_version', result.currentVersion) + colors.reset + '\n');
2280
+ screen.write(colors.green + ' ' + await i18n.t('version_check.latest_version', result.latestVersion) + colors.reset + '\n');
2281
+ screen.write('\n');
2282
+ screen.write(colors.yellow + '💡 ' + await i18n.t('version_check.update_command') + colors.reset + '\n');
2283
+ screen.write(colors.yellow + ' npm update -g @kikkimo/claude-launcher' + colors.reset + '\n');
1464
2284
  } else {
1465
2285
  // Already up to date
1466
- console.log(colors.green + '✅ ' + await i18n.t('version_check.up_to_date') + colors.reset);
1467
- console.log('');
1468
- console.log(colors.cyan + ' ' + await i18n.t('version_check.current_version', result.currentVersion) + colors.reset);
1469
- console.log(colors.cyan + ' ' + await i18n.t('version_check.latest_version', result.latestVersion) + colors.reset);
2286
+ screen.write(colors.green + '✅ ' + await i18n.t('version_check.up_to_date') + colors.reset + '\n');
2287
+ screen.write('\n');
2288
+ screen.write(colors.cyan + ' ' + await i18n.t('version_check.current_version', result.currentVersion) + colors.reset + '\n');
2289
+ screen.write(colors.cyan + ' ' + await i18n.t('version_check.latest_version', result.latestVersion) + colors.reset + '\n');
1470
2290
  }
1471
2291
 
1472
2292
  } catch (error) {
1473
2293
  // Stop progress indicator
1474
2294
  clearInterval(progressInterval);
1475
- console.log('\n');
2295
+ screen.write('\n\n');
1476
2296
 
1477
- console.log(colors.red + '❌ ' + await i18n.t('version_check.unexpected_error') + colors.reset);
1478
- console.log(colors.red + ' ' + error.message + colors.reset);
2297
+ screen.write(colors.red + '❌ ' + await i18n.t('version_check.unexpected_error') + colors.reset + '\n');
2298
+ screen.write(colors.red + ' ' + error.message + colors.reset + '\n');
1479
2299
  }
1480
2300
 
1481
- console.log('');
2301
+ screen.write('\n');
1482
2302
  await waitForKey(await i18n.t('messages.prompts.press_any_key'));
1483
2303
  return showMenu();
1484
2304
 
1485
2305
  } catch (error) {
1486
- console.log(colors.red + '❌ Failed to check version: ' + error.message + colors.reset);
2306
+ screen.write(colors.red + '❌ Failed to check version: ' + error.message + colors.reset + '\n');
1487
2307
  await waitForKey(i18n.tSync('messages.prompts.press_any_key_menu'));
1488
2308
  return showMenu();
1489
2309
  }
@@ -1495,8 +2315,9 @@ async function showVersionUpdateCheck() {
1495
2315
  * Graceful shutdown handlers
1496
2316
  */
1497
2317
  process.on('SIGTERM', () => {
1498
- console.log('');
1499
- console.log(colors.green + i18n.tSync('ui.general.goodbye') + colors.reset);
2318
+ screen.write('\n');
2319
+ screen.write(colors.green + i18n.tSync('ui.general.goodbye') + colors.reset + '\n');
2320
+ screen.exit();
1500
2321
  process.exit(0);
1501
2322
  });
1502
2323
 
@@ -1509,43 +2330,34 @@ const stdinManager = require('./lib/utils/stdin-manager');
1509
2330
  let exiting = false;
1510
2331
 
1511
2332
  process.on('SIGINT', () => {
1512
- // During Claude run, ignore in launcher so child handles it
1513
- // Check this BEFORE setting exiting flag to avoid breaking reentrancy protection
1514
2333
  if (stdinManager.isSuspended && stdinManager.isSuspended()) {
1515
2334
  return;
1516
2335
  }
1517
-
1518
- // Prevent re-entrance - ensure cleanup runs only once
1519
- if (exiting) {
1520
- return;
1521
- }
2336
+ if (exiting) return;
1522
2337
  exiting = true;
1523
-
1524
- // Try to reset stdin state before handling
1525
2338
  try {
1526
2339
  if (process.stdin.isTTY) {
1527
2340
  process.stdin.setRawMode(false);
1528
2341
  process.stdin.pause();
1529
2342
  }
1530
- } catch (_) {
1531
- // Ignore errors during emergency cleanup
1532
- }
1533
-
1534
- // Use unified Ctrl+C handler from StdinManager (synchronous)
2343
+ } catch (_) {}
1535
2344
  try {
1536
- stdinManager.handleCtrlC();
1537
- } catch (_) {
1538
- // Ignore errors during Ctrl+C handling
1539
- }
1540
-
1541
- // Exit with standard SIGINT exit code (128 + 2 = 130)
1542
- // Note: If handleCtrlC() calls process.exit(0) for second Ctrl+C,
1543
- // this line won't be reached, which is expected behavior
2345
+ const shouldExit = stdinManager.handleCtrlC();
2346
+ if (shouldExit === false) {
2347
+ exiting = false;
2348
+ return;
2349
+ }
2350
+ } catch (_) {}
2351
+ screen.exit();
1544
2352
  process.exit(130);
1545
2353
  });
1546
2354
 
2355
+ process.on('uncaughtException', (err) => { screen.exit(); console.error(err); process.exit(1); });
2356
+ process.on('unhandledRejection', (err) => { screen.exit(); console.error(err); process.exit(1); });
2357
+
1547
2358
  // Initialize global menus and start the application
1548
2359
  initializeGlobalMenus();
1549
2360
 
1550
2361
  // Start the application
2362
+ screen.enter();
1551
2363
  showMenu();