codexmate 0.0.55 → 0.0.56

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.
Files changed (31) hide show
  1. package/README.md +2 -0
  2. package/README.vi.md +2 -0
  3. package/README.zh.md +2 -0
  4. package/cli/local-bridge.js +221 -22
  5. package/cli.js +117 -5
  6. package/package.json +1 -1
  7. package/web-ui/app.js +2 -0
  8. package/web-ui/modules/app.computed.session.mjs +210 -0
  9. package/web-ui/modules/app.methods.claude-config.mjs +128 -12
  10. package/web-ui/modules/app.methods.codex-config.mjs +294 -65
  11. package/web-ui/modules/app.methods.navigation.mjs +4 -1
  12. package/web-ui/modules/app.methods.providers.mjs +15 -1
  13. package/web-ui/modules/app.methods.runtime.mjs +7 -2
  14. package/web-ui/modules/app.methods.session-actions.mjs +24 -0
  15. package/web-ui/modules/app.methods.startup-claude.mjs +34 -1
  16. package/web-ui/modules/i18n/locales/en.mjs +41 -2
  17. package/web-ui/modules/i18n/locales/ja.mjs +41 -2
  18. package/web-ui/modules/i18n/locales/vi.mjs +41 -8
  19. package/web-ui/modules/i18n/locales/zh-tw.mjs +41 -2
  20. package/web-ui/modules/i18n/locales/zh.mjs +41 -2
  21. package/web-ui/modules/provider-default-names.mjs +25 -0
  22. package/web-ui/partials/index/modal-health-check.html +69 -5
  23. package/web-ui/partials/index/panel-config-codex.html +2 -2
  24. package/web-ui/partials/index/panel-sessions.html +97 -17
  25. package/web-ui/res/web-ui-render.precompiled.js +345 -93
  26. package/web-ui/session-helpers.mjs +4 -1
  27. package/web-ui/styles/responsive.css +98 -0
  28. package/web-ui/styles/sessions-preview.css +212 -2
  29. package/web-ui/styles/sessions-toolbar-trash.css +61 -18
  30. package/web-ui/styles/skills-list.css +122 -0
  31. package/web-ui/styles/titles-cards.css +52 -0
@@ -24,6 +24,28 @@ function getResponseMessage(response, fallback) {
24
24
  return fallback;
25
25
  }
26
26
 
27
+
28
+ function normalizeProviderName(value) {
29
+ return typeof value === 'string' ? value.trim() : '';
30
+ }
31
+
32
+ function providerIssueDetail(issues) {
33
+ if (!Array.isArray(issues) || !issues.length) return '';
34
+ return issues
35
+ .map((issue) => issue && (issue.message || issue.code || ''))
36
+ .filter(Boolean)
37
+ .join(';');
38
+ }
39
+
40
+ function remoteProviderDetail(remote) {
41
+ if (!remote || typeof remote !== 'object') return '';
42
+ const parts = [];
43
+ if (remote.endpoint) parts.push(remote.endpoint);
44
+ if (remote.statusCode) parts.push(String(remote.statusCode));
45
+ if (remote.message) parts.push(remote.message);
46
+ return parts.join(' · ');
47
+ }
48
+
27
49
  export function createCodexConfigMethods(options = {}) {
28
50
  const {
29
51
  api,
@@ -303,6 +325,7 @@ export function createCodexConfigMethods(options = {}) {
303
325
  async runHealthCheck(options = {}) {
304
326
  this.healthCheckLoading = true;
305
327
  this.healthCheckResult = null;
328
+ this.healthCheckFailedProviderSelections = {};
306
329
  this.healthCheckBatchTotal = 0;
307
330
  this.healthCheckBatchDone = 0;
308
331
  this.healthCheckBatchFailed = 0;
@@ -354,7 +377,13 @@ export function createCodexConfigMethods(options = {}) {
354
377
  }
355
378
 
356
379
  if (this.configMode === 'claude') {
357
- const entries = Object.entries(this.claudeConfigs || {});
380
+ const configs = this.claudeConfigs || {};
381
+ const currentName = typeof this.currentClaudeConfig === 'string' && configs[this.currentClaudeConfig]
382
+ ? this.currentClaudeConfig
383
+ : Object.keys(configs)[0];
384
+ const entries = currentName && configs[currentName]
385
+ ? [[currentName, configs[currentName]]]
386
+ : [];
358
387
  this.healthCheckBatchTotal = entries.length;
359
388
 
360
389
  const speedTasks = entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)
@@ -395,80 +424,44 @@ export function createCodexConfigMethods(options = {}) {
395
424
  speedTests: results
396
425
  }
397
426
  };
427
+ if (!silent) {
428
+ this.showHealthCheckModal = true;
429
+ }
398
430
  if (ok && !silent) {
399
- this.showMessage('检查通过', 'success');
431
+ this.showMessage(this.t('toast.check.success'), 'success');
400
432
  }
401
433
  return;
402
434
  }
403
435
 
404
- const shouldRunSpeedTests = this.configMode === 'codex';
405
- const speedTimeoutMs = shouldRunSpeedTests ? 3500 : 0;
406
- const providers = shouldRunSpeedTests
407
- ? (this.providersList || [])
408
- .map((provider) => typeof provider === 'string'
409
- ? provider.trim()
410
- : String((provider && provider.name) || '').trim())
411
- .filter(Boolean)
412
- : [];
413
- const currentProvider = String(this.currentProvider || '').trim();
414
- const orderedProviders = currentProvider && providers.includes(currentProvider)
415
- ? [currentProvider, ...providers.filter((name) => name !== currentProvider)]
416
- : providers;
417
- this.healthCheckBatchTotal = orderedProviders.length;
418
-
419
- const speedTasks = orderedProviders.map((provider) => this.runSpeedTest(provider, { silent: true, timeoutMs: speedTimeoutMs })
420
- .then((result) => {
421
- if (!result || result.ok !== true) {
422
- this.healthCheckBatchFailed += 1;
423
- }
424
- return { name: provider, result };
425
- })
426
- .catch((err) => {
427
- this.healthCheckBatchFailed += 1;
428
- return {
429
- name: provider,
430
- result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
431
- };
432
- })
433
- .finally(() => {
434
- this.healthCheckBatchDone += 1;
435
- })
436
- );
437
-
438
- const configTask = api('config-health-check', { remote: this.configMode === 'codex' });
439
- const [res, pairs] = await Promise.all([
440
- configTask,
441
- Promise.all(speedTasks)
436
+ const [configRes, providersRes] = await Promise.all([
437
+ api('config-health-check', { remote: true }),
438
+ api('providers-health', { remote: false })
442
439
  ]);
443
- if (hasResponseError(res)) {
440
+ if (providersRes && typeof providersRes === 'object' && !hasResponseError(providersRes)) {
441
+ this.providersHealthResult = providersRes;
442
+ }
443
+ if (hasResponseError(configRes)) {
444
444
  this.healthCheckResult = null;
445
445
  if (!silent) {
446
- this.showMessage(getResponseMessage(res, '检查失败'), 'error');
447
- }
448
- } else if (res && typeof res === 'object') {
449
- const issues = Array.isArray(res.issues) ? [...res.issues] : [];
450
- let remote = res.remote || null;
451
- if (shouldRunSpeedTests) {
452
- const results = {};
453
- for (const pair of pairs) {
454
- results[pair.name] = pair.result || null;
455
- const issue = this.buildSpeedTestIssue(pair.name, pair.result);
456
- if (issue) issues.push(issue);
457
- }
458
- remote = remote && typeof remote === 'object'
459
- ? { ...remote, speedTests: results }
460
- : { type: 'speed-test', speedTests: results };
446
+ this.showMessage(getResponseMessage(configRes, this.t('toast.check.fail')), 'error');
461
447
  }
462
-
463
- const ok = issues.length === 0;
448
+ } else if (configRes && typeof configRes === 'object') {
449
+ const issues = Array.isArray(configRes.issues) ? [...configRes.issues] : [];
450
+ const ok = typeof configRes.ok === 'boolean' ? configRes.ok : issues.length === 0;
464
451
  this.healthCheckResult = {
465
- ...res,
452
+ ...configRes,
466
453
  ok,
467
454
  issues,
468
- remote
455
+ remote: configRes.remote || null
469
456
  };
470
- if (ok && !silent) {
471
- this.showMessage('检查通过', 'success');
457
+ this.healthCheckBatchTotal = 1;
458
+ this.healthCheckBatchDone = 1;
459
+ this.healthCheckBatchFailed = ok ? 0 : 1;
460
+ if (!silent) {
461
+ this.showHealthCheckModal = true;
462
+ }
463
+ if (!silent) {
464
+ this.showMessage(this.t(ok ? 'toast.check.success' : 'toast.check.fail'), ok ? 'success' : 'error');
472
465
  }
473
466
  } else {
474
467
  this.healthCheckResult = null;
@@ -479,16 +472,252 @@ export function createCodexConfigMethods(options = {}) {
479
472
  } catch (e) {
480
473
  this.healthCheckResult = null;
481
474
  if (!(options && options.silent)) {
482
- this.showMessage('检查失败', 'error');
475
+ this.showMessage(this.t('toast.check.fail'), 'error');
483
476
  }
484
477
  } finally {
485
478
  this.healthCheckBatchTotal = this.healthCheckBatchTotal || 0;
486
479
  this.healthCheckBatchDone = Math.min(this.healthCheckBatchDone || 0, this.healthCheckBatchTotal || 0);
487
480
  this.healthCheckLoading = false;
488
- if (typeof this.runProvidersHealthCheck === 'function' && this.configMode === 'codex') {
489
- void this.runProvidersHealthCheck({ remote: true });
481
+ }
482
+ },
483
+
484
+ getHealthCheckFailedProviderItems() {
485
+ const result = this.healthCheckResult && typeof this.healthCheckResult === 'object'
486
+ ? this.healthCheckResult
487
+ : null;
488
+ if (!result) return [];
489
+ const remote = result.remote && typeof result.remote === 'object' ? result.remote : null;
490
+ const items = new Map();
491
+ const currentMode = this.configMode === 'claude' ? 'claude' : 'codex';
492
+ const pushItem = (name, detail, status = 'red', mode = currentMode) => {
493
+ const providerName = normalizeProviderName(name);
494
+ if (!providerName) return;
495
+ const key = `${mode}:${providerName}`;
496
+ const provider = (this.providersList || []).find((item) => item && item.name === providerName) || providerName;
497
+ const baseDeletable = mode === 'claude'
498
+ ? !!(this.claudeConfigs && this.claudeConfigs[providerName])
499
+ : (typeof this.shouldShowProviderDelete === 'function' ? this.shouldShowProviderDelete(provider) : true);
500
+ const writeAllowed = typeof this.isToolConfigWriteAllowed === 'function'
501
+ ? this.isToolConfigWriteAllowed(mode)
502
+ : true;
503
+ const deletable = baseDeletable && writeAllowed;
504
+ const blockedReason = !baseDeletable ? 'reserved' : (!writeAllowed ? 'readonly' : '');
505
+ const previous = items.get(key) || {};
506
+ items.set(key, {
507
+ key,
508
+ mode,
509
+ name: providerName,
510
+ status: previous.status || status || 'red',
511
+ detail: detail || previous.detail || '',
512
+ deletable,
513
+ blockedReason: previous.blockedReason || blockedReason,
514
+ selected: !!(this.healthCheckFailedProviderSelections && this.healthCheckFailedProviderSelections[key])
515
+ });
516
+ };
517
+
518
+ if (remote && remote.type === 'providers-health' && Array.isArray(remote.providers)) {
519
+ for (const provider of remote.providers) {
520
+ if (!provider || provider.status === 'green') continue;
521
+ pushItem(provider.provider, providerIssueDetail(provider.issues) || remoteProviderDetail(provider.remote), provider.status || 'red', 'codex');
490
522
  }
491
523
  }
524
+
525
+ if (remote && remote.type === 'remote-health-check' && (result.ok === false || remote.ok === false)) {
526
+ pushItem(remote.provider, remoteProviderDetail(remote), 'red', 'codex');
527
+ }
528
+
529
+ if (remote && remote.speedTests && typeof remote.speedTests === 'object') {
530
+ for (const [name, speedResult] of Object.entries(remote.speedTests)) {
531
+ if (speedResult && speedResult.ok === true) continue;
532
+ pushItem(name, speedResult && speedResult.error ? speedResult.error : this.t('config.health.fail'), 'red', this.configMode === 'claude' ? 'claude' : 'codex');
533
+ }
534
+ }
535
+
536
+ if (Array.isArray(result.issues)) {
537
+ for (const issue of result.issues) {
538
+ if (!issue || typeof issue !== 'object') continue;
539
+ const name = normalizeProviderName(issue.provider || issue.providerName || issue.name);
540
+ if (name) pushItem(name, issue.message || issue.code || '', 'red', currentMode);
541
+ }
542
+ }
543
+
544
+ return Array.from(items.values());
545
+ },
546
+
547
+ getSelectableHealthCheckFailedProviderItems() {
548
+ return this.getHealthCheckFailedProviderItems().filter((item) => item.deletable);
549
+ },
550
+
551
+ hasHealthCheckFailedProviderSelection() {
552
+ return this.getSelectableHealthCheckFailedProviderItems().some((item) => item.selected);
553
+ },
554
+
555
+ areAllHealthCheckFailedProvidersSelected() {
556
+ const selectable = this.getSelectableHealthCheckFailedProviderItems();
557
+ return selectable.length > 0 && selectable.every((item) => item.selected);
558
+ },
559
+
560
+ setAllHealthCheckFailedProviderSelections(checked) {
561
+ const selectable = this.getSelectableHealthCheckFailedProviderItems();
562
+ const next = { ...(this.healthCheckFailedProviderSelections || {}) };
563
+ for (const item of selectable) {
564
+ next[item.key] = !!checked;
565
+ }
566
+ this.healthCheckFailedProviderSelections = next;
567
+ },
568
+
569
+ toggleHealthCheckFailedProviderSelection(item, checked) {
570
+ if (!item || !item.key || !item.deletable) return;
571
+ this.healthCheckFailedProviderSelections = {
572
+ ...(this.healthCheckFailedProviderSelections || {}),
573
+ [item.key]: !!checked
574
+ };
575
+ },
576
+
577
+ async deleteSelectedHealthCheckFailedProviders() {
578
+ const selectedItems = this.getHealthCheckFailedProviderItems()
579
+ .filter((item) => item.selected && item.deletable);
580
+ if (!selectedItems.length) {
581
+ this.showMessage(this.t('toast.health.noFailedProviderSelection'), 'info');
582
+ return;
583
+ }
584
+ this.healthCheckFailedProviderDeleting = true;
585
+ const deleted = [];
586
+ try {
587
+ const claudeItems = selectedItems.filter((item) => item.mode === 'claude');
588
+ const codexItems = selectedItems.filter((item) => item.mode !== 'claude');
589
+
590
+ if (claudeItems.length) {
591
+ const configs = this.claudeConfigs && typeof this.claudeConfigs === 'object' ? this.claudeConfigs : {};
592
+ const names = claudeItems.map((item) => item.name).filter((name) => configs[name]);
593
+ if (names.length) {
594
+ const deletedCurrentClaude = names.includes(this.currentClaudeConfig);
595
+ const remainingNames = Object.keys(configs).filter((name) => !names.includes(name));
596
+ if (remainingNames.length === 0) {
597
+ throw new Error(this.t('toast.claude.keepOne'));
598
+ }
599
+ for (const name of names) {
600
+ const config = configs[name];
601
+ if (config && (config.providerCacheRef || config.source === 'provider-cache')) {
602
+ if (typeof this.deleteClaudeProviderCacheRef === 'function') {
603
+ const ok = await this.deleteClaudeProviderCacheRef(config);
604
+ if (!ok) throw new Error(this.t('toast.delete.fail'));
605
+ } else {
606
+ const cacheName = typeof config.providerCacheRef === 'string' && config.providerCacheRef.trim()
607
+ ? config.providerCacheRef.trim()
608
+ : name;
609
+ const res = await api('delete-provider-cache-record', { name: cacheName, group: 'claude' });
610
+ if (res && res.error) throw new Error(res.error);
611
+ }
612
+ }
613
+ if (typeof this.rememberDeletedClaudeSettingsImport === 'function') {
614
+ this.rememberDeletedClaudeSettingsImport(config);
615
+ }
616
+ delete configs[name];
617
+ deleted.push(name);
618
+ }
619
+ if (deletedCurrentClaude) {
620
+ this.currentClaudeConfig = typeof this.selectClaudeFallbackConfigName === 'function'
621
+ ? this.selectClaudeFallbackConfigName(names)
622
+ : remainingNames[0];
623
+ }
624
+ if (typeof this.saveClaudeConfigs === 'function') {
625
+ this.saveClaudeConfigs();
626
+ }
627
+ if (deletedCurrentClaude && typeof this.applyCurrentClaudeConfigSilently === 'function') {
628
+ await this.applyCurrentClaudeConfigSilently();
629
+ } else if (typeof this.refreshClaudeModelContext === 'function') {
630
+ this.refreshClaudeModelContext();
631
+ }
632
+ }
633
+ }
634
+
635
+ for (const item of codexItems) {
636
+ const res = await api('delete-provider', { name: item.name });
637
+ if (res && res.error) {
638
+ throw new Error(res.error);
639
+ }
640
+ this.providersList = (this.providersList || []).filter((provider) => provider && provider.name !== item.name);
641
+ if (this.currentModels && this.currentModels[item.name]) {
642
+ delete this.currentModels[item.name];
643
+ }
644
+ if (res && res.switched && res.provider) {
645
+ this.currentProvider = res.provider;
646
+ if (res.model) this.currentModel = res.model;
647
+ this.providersList = (this.providersList || []).map((provider) => ({
648
+ ...provider,
649
+ current: provider.name === res.provider
650
+ }));
651
+ }
652
+ deleted.push(item.name);
653
+ }
654
+ const deletedSet = new Set(deleted);
655
+ this.healthCheckFailedProviderSelections = {};
656
+ const result = this.healthCheckResult;
657
+ const remote = result && result.remote;
658
+ const remainingIssues = Array.isArray(result && result.issues)
659
+ ? result.issues.filter((issue) => {
660
+ const name = normalizeProviderName(issue && (issue.provider || issue.providerName || issue.name));
661
+ return !name || !deletedSet.has(name);
662
+ })
663
+ : [];
664
+ if (result && typeof result === 'object') {
665
+ if (remote && remote.type === 'providers-health' && Array.isArray(remote.providers)) {
666
+ const providers = remote.providers.filter((provider) => !deletedSet.has(provider && provider.provider));
667
+ const summary = {
668
+ total: providers.length,
669
+ green: providers.filter((p) => p && p.status === 'green').length,
670
+ yellow: providers.filter((p) => p && p.status === 'yellow').length,
671
+ red: providers.filter((p) => p && p.status === 'red').length
672
+ };
673
+ this.healthCheckResult = {
674
+ ...result,
675
+ ok: remainingIssues.length === 0 && summary.yellow === 0 && summary.red === 0,
676
+ remote: {
677
+ ...remote,
678
+ providers,
679
+ summary
680
+ },
681
+ issues: remainingIssues
682
+ };
683
+ this.healthCheckBatchTotal = summary.total;
684
+ this.healthCheckBatchDone = summary.total;
685
+ this.healthCheckBatchFailed = summary.yellow + summary.red;
686
+ } else if (remote && remote.type === 'speed-test' && remote.speedTests && typeof remote.speedTests === 'object') {
687
+ const speedTests = Object.fromEntries(
688
+ Object.entries(remote.speedTests).filter(([name]) => !deletedSet.has(normalizeProviderName(name)))
689
+ );
690
+ const total = Object.keys(speedTests).length;
691
+ const failed = Object.values(speedTests).filter((entry) => !entry || entry.ok !== true).length;
692
+ this.healthCheckResult = {
693
+ ...result,
694
+ ok: remainingIssues.length === 0 && failed === 0,
695
+ remote: {
696
+ ...remote,
697
+ speedTests
698
+ },
699
+ issues: remainingIssues
700
+ };
701
+ this.healthCheckBatchTotal = total;
702
+ this.healthCheckBatchDone = total;
703
+ this.healthCheckBatchFailed = failed;
704
+ } else {
705
+ const removedRemote = remote && remote.type === 'remote-health-check' && deletedSet.has(remote.provider);
706
+ this.healthCheckResult = {
707
+ ...result,
708
+ ok: remainingIssues.length === 0,
709
+ remote: removedRemote ? null : remote,
710
+ issues: remainingIssues
711
+ };
712
+ }
713
+ }
714
+ this.showMessage(this.t('toast.health.deleteFailedProvidersDone', { count: deleted.length }), 'success');
715
+ this.showHealthCheckModal = false;
716
+ } catch (e) {
717
+ this.showMessage(e && e.message ? e.message : this.t('toast.delete.fail'), 'error');
718
+ } finally {
719
+ this.healthCheckFailedProviderDeleting = false;
720
+ }
492
721
  },
493
722
 
494
723
  async runProvidersHealthCheck(options = {}) {
@@ -724,7 +724,10 @@
724
724
  ? this.activeSessionMessages.length
725
725
  : 0;
726
726
  if (total <= 0) return;
727
- this.sessionPreviewVisibleCount = total;
727
+ const initialBatchSize = Number.isFinite(this.sessionPreviewInitialBatchSize)
728
+ ? Math.max(1, Math.floor(this.sessionPreviewInitialBatchSize))
729
+ : 12;
730
+ this.sessionPreviewVisibleCount = Math.min(total, initialBatchSize);
728
731
  this.invalidateSessionTimelineMeasurementCache();
729
732
  },
730
733
 
@@ -1,3 +1,5 @@
1
+ import { nextCodexProviderName } from './provider-default-names.mjs';
2
+
1
3
  const PROVIDER_NAME_PATTERN = /^[a-zA-Z0-9._-]+$/;
2
4
  const RESERVED_PROXY_PROVIDER_NAME = 'codexmate-proxy';
3
5
  const RESERVED_LOCAL_PROVIDER_NAME = 'local';
@@ -327,6 +329,18 @@ export function createProvidersMethods(options = {}) {
327
329
  this.showAddModal = true;
328
330
  },
329
331
 
332
+ openAddProviderModal() {
333
+ this.newProvider = {
334
+ name: nextCodexProviderName(this.providersList),
335
+ url: '',
336
+ key: '',
337
+ model: '',
338
+ useTransform: false
339
+ };
340
+ this.showAddProviderKey = false;
341
+ this.showAddModal = true;
342
+ },
343
+
330
344
  async openEditModal(provider) {
331
345
  const requestId = Symbol('openEditModal');
332
346
  this._openEditModalRequestId = requestId;
@@ -536,7 +550,7 @@ export function createProvidersMethods(options = {}) {
536
550
  closeAddModal() {
537
551
  this.showAddModal = false;
538
552
  this.showAddProviderKey = false;
539
- this.newProvider = { name: '', url: '', key: '', model: '', useTransform: false };
553
+ this.newProvider = { name: nextCodexProviderName(this.providersList), url: '', key: '', model: '', useTransform: false };
540
554
  },
541
555
 
542
556
  toggleAddProviderKey() {
@@ -154,6 +154,10 @@ export function createRuntimeMethods(options = {}) {
154
154
  const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
155
155
  const apiKey = config && typeof config.apiKey === 'string' ? config.apiKey.trim() : '';
156
156
  const model = config && typeof config.model === 'string' ? config.model.trim() : '';
157
+ const targetApiRaw = config && typeof config.targetApi === 'string' ? config.targetApi.trim().toLowerCase() : '';
158
+ const targetApi = targetApiRaw === 'ollama'
159
+ ? 'ollama'
160
+ : (targetApiRaw === 'chat_completions' || targetApiRaw === 'chat-completions' || targetApiRaw === 'chat/completions' ? 'chat_completions' : 'responses');
157
161
  this.claudeSpeedLoading[name] = true;
158
162
  try {
159
163
  if (!baseUrl) {
@@ -161,7 +165,7 @@ export function createRuntimeMethods(options = {}) {
161
165
  this.claudeSpeedResults[name] = res;
162
166
  return res;
163
167
  }
164
- if (!apiKey) {
168
+ if (!apiKey && targetApi !== 'ollama') {
165
169
  const res = { ok: false, error: 'Missing API key' };
166
170
  this.claudeSpeedResults[name] = res;
167
171
  return res;
@@ -175,7 +179,8 @@ export function createRuntimeMethods(options = {}) {
175
179
  kind: 'claude',
176
180
  url: baseUrl,
177
181
  apiKey,
178
- model
182
+ model,
183
+ targetApi
179
184
  });
180
185
  if (res.error) {
181
186
  this.claudeSpeedResults[name] = { ok: false, error: res.error };
@@ -165,6 +165,30 @@ export function createSessionActionMethods(options = {}) {
165
165
  this.showMessage(this.t('toast.copy.fail'), 'error');
166
166
  },
167
167
 
168
+ async copySessionWorkspaceBrief() {
169
+ const summary = this.activeSessionWorkspaceSummary;
170
+ const text = summary && typeof summary.briefText === 'string'
171
+ ? summary.briefText.trim()
172
+ : '';
173
+ if (!text) {
174
+ this.showMessage(this.t('sessions.workspace.copy.empty'), 'error');
175
+ return;
176
+ }
177
+ const ok = this.fallbackCopyText(text);
178
+ if (ok) {
179
+ this.showMessage(this.t('sessions.workspace.copy.success'), 'success');
180
+ return;
181
+ }
182
+ try {
183
+ if (navigator.clipboard && window.isSecureContext) {
184
+ await navigator.clipboard.writeText(text);
185
+ this.showMessage(this.t('sessions.workspace.copy.success'), 'success');
186
+ return;
187
+ }
188
+ } catch (_) {}
189
+ this.showMessage(this.t('toast.copy.fail'), 'error');
190
+ },
191
+
168
192
  getSessionExportKey(session) {
169
193
  return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
170
194
  },
@@ -8,6 +8,31 @@ import {
8
8
  normalizeClaudeSettingsEnv,
9
9
  normalizeClaudeValue
10
10
  } from '../logic.mjs';
11
+ import { nextClaudeConfigName } from './provider-default-names.mjs';
12
+
13
+ const DELETED_CLAUDE_SETTINGS_IMPORTS_STORAGE_KEY = 'deletedClaudeSettingsImports';
14
+
15
+ function normalizeDeletedClaudeImportUrl(value) {
16
+ return typeof value === 'string' ? value.trim().replace(/\/+$/g, '') : '';
17
+ }
18
+
19
+ function shouldSuppressDeletedClaudeSettingsImport(env = {}) {
20
+ const normalized = normalizeClaudeSettingsEnv(env);
21
+ const baseUrl = normalizeDeletedClaudeImportUrl(normalized.baseUrl);
22
+ const model = normalizeClaudeValue(normalized.model);
23
+ if (!baseUrl || !model) return false;
24
+ try {
25
+ const raw = localStorage.getItem(DELETED_CLAUDE_SETTINGS_IMPORTS_STORAGE_KEY);
26
+ const parsed = raw ? JSON.parse(raw) : [];
27
+ const entries = Array.isArray(parsed) ? parsed : [];
28
+ return entries.some((entry) => entry
29
+ && typeof entry === 'object'
30
+ && normalizeDeletedClaudeImportUrl(entry.baseUrl) === baseUrl
31
+ && normalizeClaudeValue(entry.model) === model);
32
+ } catch (_) {
33
+ return false;
34
+ }
35
+ }
11
36
 
12
37
  export function createStartupClaudeMethods(options = {}) {
13
38
  const {
@@ -250,7 +275,7 @@ export function createStartupClaudeMethods(options = {}) {
250
275
  },
251
276
 
252
277
  shouldSuppressClaudeSettingsImport(env) {
253
- return isLikelyBuiltinClaudeProxySettingsEnv(env);
278
+ return isLikelyBuiltinClaudeProxySettingsEnv(env) || shouldSuppressDeletedClaudeSettingsImport(env);
254
279
  },
255
280
 
256
281
  findDuplicateClaudeConfigName(config) {
@@ -573,6 +598,14 @@ export function createStartupClaudeMethods(options = {}) {
573
598
  },
574
599
 
575
600
  openClaudeConfigModal() {
601
+ this.newClaudeConfig = {
602
+ name: nextClaudeConfigName(this.claudeConfigs),
603
+ apiKey: '',
604
+ externalCredentialType: '',
605
+ baseUrl: '',
606
+ model: '',
607
+ targetApi: 'responses'
608
+ };
576
609
  this.showAddClaudeConfigKey = false;
577
610
  this.showClaudeConfigModal = true;
578
611
  },
@@ -432,6 +432,8 @@ const en = Object.freeze({
432
432
  'toast.apply.fail': 'Failed to apply configuration',
433
433
  'toast.check.success': 'Check passed',
434
434
  'toast.check.fail': 'Check failed',
435
+ 'toast.health.noFailedProviderSelection': 'Select at least one failed provider first',
436
+ 'toast.health.deleteFailedProvidersDone': 'Deleted {count} failed provider(s)',
435
437
  'toast.noChanges': 'No changes detected',
436
438
  'toast.template.loadFail': 'Failed to load template',
437
439
  'toast.template.empty': 'Template cannot be empty',
@@ -660,6 +662,32 @@ const en = Object.freeze({
660
662
  'sessions.preview.rerender': 'Re-render',
661
663
  'sessions.preview.preparing': 'Preparing session content...',
662
664
  'sessions.preview.clipped': 'Showing the latest {count} messages only.',
665
+ 'sessions.workspace.kicker': 'Work memory',
666
+ 'sessions.workspace.title': 'Session reuse summary',
667
+ 'sessions.workspace.subtitle': 'Extracted signals, commands, files, links, and risks from {count} loaded messages.',
668
+ 'sessions.workspace.copy': 'Copy brief',
669
+ 'sessions.workspace.copy.title': 'Session workspace brief',
670
+ 'sessions.workspace.copy.source': 'Source',
671
+ 'sessions.workspace.copy.path': 'Path',
672
+ 'sessions.workspace.copy.empty': 'No session brief to copy yet',
673
+ 'sessions.workspace.copy.success': 'Workspace brief copied',
674
+ 'sessions.workspace.metric.messages': 'Messages',
675
+ 'sessions.workspace.metric.user': 'User',
676
+ 'sessions.workspace.metric.assistant': 'Assistant',
677
+ 'sessions.workspace.metric.commands': 'Commands',
678
+ 'sessions.workspace.metric.artifacts': 'Artifacts',
679
+ 'sessions.workspace.metric.risks': 'Risks / todos',
680
+ 'sessions.workspace.signals': 'Task signals',
681
+ 'sessions.workspace.commands': 'Reusable commands',
682
+ 'sessions.workspace.files': 'Related files',
683
+ 'sessions.workspace.links': 'Related links',
684
+ 'sessions.workspace.risks': 'Risks and todos',
685
+ 'sessions.workspace.nextSteps': 'Next steps',
686
+ 'sessions.workspace.empty': 'No strong signals yet. Continue with the raw messages below.',
687
+ 'sessions.workspace.noneCommands': 'No command signals',
688
+ 'sessions.workspace.noneFiles': 'No file signals',
689
+ 'sessions.workspace.noneLinks': 'No link signals',
690
+ 'sessions.workspace.noneRisks': 'No risks or todos detected',
663
691
  'sessions.preview.shownCount': 'Shown {shown} / {total}',
664
692
  'sessions.preview.loadMore': 'Load more (remaining {remain})',
665
693
  'sessions.preview.loadingMore': 'Loading older messages...',
@@ -895,7 +923,18 @@ const en = Object.freeze({
895
923
  'config.health.title': 'Config health check',
896
924
  'config.health.run': 'Run check',
897
925
  'config.health.running': 'Checking...',
898
- 'config.health.hint': 'Runs availability probes across all providers and refreshes latency badges.',
926
+ 'config.health.hint': 'Checks only the currently selected provider; unselected providers do not count as this check\'s errors.',
927
+ 'config.health.codexHint': 'Checks the current Codex route provider, endpoint, auth, and model through the local bridge.',
928
+ 'config.health.providersSummary': 'Providers: {total} total · {green} ok · {yellow} config issues · {red} failed',
929
+ 'config.health.failedProviders.title': 'Failed providers to delete',
930
+ 'config.health.failedProviders.selectAria': 'Select failed provider {name} for deletion',
931
+ 'config.health.failedProviders.clearSelection': 'Clear',
932
+ 'config.health.failedProviders.selectAll': 'Select all',
933
+ 'config.health.failedProviders.selectAllAria': 'Select all deletable failed providers',
934
+ 'config.health.failedProviders.notDeletable': 'Reserved/system provider; cannot be deleted here.',
935
+ 'config.health.failedProviders.writeRequired': 'Enable provider writes in this tab before deleting.',
936
+ 'config.health.failedProviders.deleteSelected': 'Remove selected configs',
937
+ 'config.health.failedProviders.deleting': 'Deleting...',
899
938
  'config.health.progress': '{done}/{total} done · {failed} failed',
900
939
  'config.health.ok': 'Passed',
901
940
  'config.health.fail': 'Failed',
@@ -1301,7 +1340,7 @@ const en = Object.freeze({
1301
1340
  'claude.health.title': 'Config health check',
1302
1341
  'claude.health.run': 'Run check',
1303
1342
  'claude.health.running': 'Checking...',
1304
- 'claude.health.hint': 'Runs availability probes for all Claude configs and refreshes the latency badges.',
1343
+ 'claude.health.hint': 'Checks only the currently selected Claude config; unselected configs do not count as this check\'s errors.',
1305
1344
  'claude.health.progress': '{done}/{total} done · {failed} failed',
1306
1345
  'claude.md.title': 'CLAUDE.md',
1307
1346
  'claude.md.open': 'Open CLAUDE.md',