@kikkimo/claude-launcher 3.0.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/CHANGELOG.md +28 -0
- package/README.md +7 -4
- package/claude-launcher +634 -38
- package/docs/README-zh.md +7 -4
- package/lib/api-manager.js +501 -67
- package/lib/i18n/locales/de.js +144 -6
- package/lib/i18n/locales/en.js +150 -6
- package/lib/i18n/locales/es.js +144 -6
- package/lib/i18n/locales/fr.js +144 -6
- package/lib/i18n/locales/it.js +144 -6
- package/lib/i18n/locales/ja.js +144 -6
- package/lib/i18n/locales/ko.js +144 -6
- package/lib/i18n/locales/pt.js +144 -6
- package/lib/i18n/locales/ru.js +144 -6
- package/lib/i18n/locales/zh-TW.js +144 -6
- package/lib/i18n/locales/zh.js +150 -6
- package/lib/launcher.js +46 -17
- package/lib/presets/providers.js +143 -39
- package/lib/ui/api-editor.js +668 -210
- package/lib/ui/i18n-labels.js +16 -0
- package/lib/ui/menu.js +19 -13
- package/lib/ui/screen.js +125 -125
- package/lib/utils/version-checker.js +6 -5
- package/lib/validators.js +102 -1
- package/package.json +2 -2
package/claude-launcher
CHANGED
|
@@ -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,
|
|
@@ -53,7 +55,8 @@ const { getPasswordInput } = require('./lib/auth/password-input');
|
|
|
53
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');
|
|
56
|
-
const { editApi } = require('./lib/ui/api-editor');
|
|
58
|
+
const { editApi, editApiEnvVarsById, resolveProviderName } = require('./lib/ui/api-editor');
|
|
59
|
+
const { i18nLabel } = require('./lib/ui/i18n-labels');
|
|
57
60
|
const i18n = require('./lib/i18n');
|
|
58
61
|
const screen = require('./lib/ui/screen');
|
|
59
62
|
const fs = require('fs');
|
|
@@ -221,52 +224,631 @@ function validateImportFile(filePath) {
|
|
|
221
224
|
// Main menu options - will be populated dynamically with i18n
|
|
222
225
|
let menuOptions = [];
|
|
223
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
|
+
|
|
224
307
|
/**
|
|
225
|
-
*
|
|
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
|
+
|
|
622
|
+
/**
|
|
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
|
|
226
669
|
*/
|
|
227
670
|
async function addNewThirdPartyApi() {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const hasExportPassword = apiManager.hasExportPassword();
|
|
234
|
-
|
|
235
|
-
const newApi = apiManager.addApi(
|
|
236
|
-
apiData.baseUrl,
|
|
237
|
-
apiData.authToken,
|
|
238
|
-
apiData.model,
|
|
239
|
-
apiData.name,
|
|
240
|
-
apiData.provider
|
|
241
|
-
);
|
|
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;
|
|
242
676
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
}
|
|
249
746
|
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
252
832
|
}
|
|
833
|
+
}
|
|
253
834
|
|
|
835
|
+
if (step === 0) {
|
|
836
|
+
screen.write(colors.yellow + i18n.tSync('messages.info.operation_cancelled') + colors.reset + '\n');
|
|
837
|
+
}
|
|
254
838
|
} catch (error) {
|
|
255
|
-
// Force cleanup stdin state to prevent navigation issues
|
|
256
839
|
forceStdinCleanup();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if (error.
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
}
|
|
263
848
|
} else {
|
|
264
|
-
|
|
265
|
-
showError(await i18n.t('errors.api.failed_add', error.message));
|
|
849
|
+
screen.write(colors.red + error.message + colors.reset + '\n');
|
|
266
850
|
}
|
|
267
851
|
}
|
|
268
|
-
|
|
269
|
-
await waitForKey(await i18n.t('messages.prompts.press_any_key'));
|
|
270
852
|
}
|
|
271
853
|
|
|
272
854
|
/**
|
|
@@ -1090,6 +1672,9 @@ async function showConfigManagement(restoreIndex = 0) {
|
|
|
1090
1672
|
const launchModeVal = config.apiLaunchMode === 'select'
|
|
1091
1673
|
? i18n.tSync('config.values.select_mode')
|
|
1092
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');
|
|
1093
1678
|
|
|
1094
1679
|
const menuOptions = [
|
|
1095
1680
|
padStringToWidth(i18n.tSync('menu.config.language'), labelWidth) + langName,
|
|
@@ -1097,6 +1682,7 @@ async function showConfigManagement(restoreIndex = 0) {
|
|
|
1097
1682
|
padStringToWidth(i18n.tSync('menu.config.model_upgrade_notification'), labelWidth) + upgradeNotifVal,
|
|
1098
1683
|
padStringToWidth(i18n.tSync('menu.config.telemetry'), labelWidth) + telemetryVal,
|
|
1099
1684
|
padStringToWidth(i18n.tSync('menu.config.api_launch_mode'), labelWidth) + launchModeVal,
|
|
1685
|
+
padStringToWidth(i18n.tSync('menu.config.no_flicker'), labelWidth) + noFlickerVal,
|
|
1100
1686
|
i18n.tSync('menu.config.back')
|
|
1101
1687
|
];
|
|
1102
1688
|
|
|
@@ -1108,6 +1694,7 @@ async function showConfigManagement(restoreIndex = 0) {
|
|
|
1108
1694
|
case 2: return i18n.tSync('hints.config.upgrade_notification');
|
|
1109
1695
|
case 3: return i18n.tSync('hints.config.telemetry');
|
|
1110
1696
|
case 4: return i18n.tSync('hints.config.launch_mode');
|
|
1697
|
+
case 5: return i18n.tSync('hints.config.no_flicker');
|
|
1111
1698
|
default: return null;
|
|
1112
1699
|
}
|
|
1113
1700
|
};
|
|
@@ -1155,7 +1742,13 @@ async function showConfigManagement(restoreIndex = 0) {
|
|
|
1155
1742
|
return showConfigManagement(4);
|
|
1156
1743
|
}
|
|
1157
1744
|
|
|
1158
|
-
case 5: //
|
|
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
|
|
1159
1752
|
case -1:
|
|
1160
1753
|
default:
|
|
1161
1754
|
return showMenu();
|
|
@@ -1334,7 +1927,10 @@ async function showMenu() {
|
|
|
1334
1927
|
const cfgTelemetry = hintConfig.disableTelemetry !== false
|
|
1335
1928
|
? i18n.tSync('config.values.off')
|
|
1336
1929
|
: i18n.tSync('config.values.on');
|
|
1337
|
-
const
|
|
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);
|
|
1338
1934
|
|
|
1339
1935
|
// Synchronous hint callback — must not use await
|
|
1340
1936
|
const hintCallback = (selectedIndex) => {
|