codexmate 0.0.31 → 0.0.33

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 (37) hide show
  1. package/README.md +92 -308
  2. package/README.zh.md +94 -318
  3. package/cli/local-bridge.js +227 -0
  4. package/cli/update.js +162 -0
  5. package/cli.js +357 -112
  6. package/lib/cli-sessions.js +16 -6
  7. package/lib/win-tray.js +119 -0
  8. package/package.json +2 -2
  9. package/web-ui/app.js +4 -0
  10. package/web-ui/logic.sessions.mjs +17 -1
  11. package/web-ui/modules/app.computed.session.mjs +51 -315
  12. package/web-ui/modules/app.methods.agents.mjs +19 -0
  13. package/web-ui/modules/app.methods.claude-config.mjs +71 -2
  14. package/web-ui/modules/app.methods.codex-config.mjs +20 -0
  15. package/web-ui/modules/app.methods.providers.mjs +53 -7
  16. package/web-ui/modules/app.methods.session-actions.mjs +1 -1
  17. package/web-ui/modules/app.methods.session-browser.mjs +29 -1
  18. package/web-ui/modules/app.methods.startup-claude.mjs +4 -0
  19. package/web-ui/modules/i18n.dict.mjs +21 -3
  20. package/web-ui/partials/index/layout-header.html +1 -2
  21. package/web-ui/partials/index/modal-config-template-agents.html +12 -1
  22. package/web-ui/partials/index/modals-basic.html +14 -3
  23. package/web-ui/partials/index/panel-config-claude.html +57 -85
  24. package/web-ui/partials/index/panel-config-codex.html +60 -226
  25. package/web-ui/partials/index/panel-dashboard.html +0 -33
  26. package/web-ui/partials/index/panel-docs.html +21 -53
  27. package/web-ui/partials/index/panel-sessions.html +37 -20
  28. package/web-ui/partials/index/panel-trash.html +33 -38
  29. package/web-ui/partials/index/panel-usage.html +71 -304
  30. package/web-ui/styles/controls-forms.css +11 -0
  31. package/web-ui/styles/docs-panel.css +57 -83
  32. package/web-ui/styles/layout-shell.css +26 -24
  33. package/web-ui/styles/modals-core.css +33 -0
  34. package/web-ui/styles/responsive.css +5 -67
  35. package/web-ui/styles/sessions-list.css +274 -8
  36. package/web-ui/styles/sessions-toolbar-trash.css +185 -15
  37. package/web-ui/styles/sessions-usage.css +336 -788
@@ -55,6 +55,12 @@ function maskKeyLocal(key) {
55
55
  return key.substring(0, 4) + '...' + key.substring(key.length - 4);
56
56
  }
57
57
 
58
+ function maskKeyForEdit(key) {
59
+ if (!key) return '';
60
+ if (key.length <= 12) return key.substring(0, 4) + '...' + key.substring(key.length - 4);
61
+ return key.substring(0, 8) + '...' + key.substring(key.length - 4);
62
+ }
63
+
58
64
  function getProviderValidationForContext(vm, mode = 'add') {
59
65
  const draft = mode === 'edit' ? vm.editingProvider : vm.newProvider;
60
66
  const editingName = mode === 'edit' ? normalizeText(draft && draft.name) : '';
@@ -286,11 +292,15 @@ export function createProvidersMethods(options = {}) {
286
292
  },
287
293
 
288
294
  openCloneProviderModal(provider) {
295
+ const isTransform = !!(provider.codexmate_bridge || '').trim() || /\/bridge\/openai\//.test(provider.url || '');
296
+ const cloneUrl = isTransform && provider.upstreamUrl
297
+ ? normalizeProviderUrl(provider.upstreamUrl)
298
+ : normalizeProviderUrl(provider.url || '');
289
299
  this.newProvider = {
290
300
  name: '',
291
- url: normalizeProviderUrl(provider.url || ''),
301
+ url: cloneUrl,
292
302
  key: '',
293
- useTransform: !!(provider.codexmate_bridge || '').trim() || /\/bridge\/openai\//.test(provider.url || '')
303
+ useTransform: isTransform
294
304
  };
295
305
  this.showAddModal = true;
296
306
  },
@@ -312,15 +322,39 @@ export function createProvidersMethods(options = {}) {
312
322
  this.editingProvider = {
313
323
  name: provider.name,
314
324
  url: normalizeProviderUrl(provider.url || ''),
315
- key: '',
325
+ key: maskKeyForEdit(provider.key || ''),
316
326
  readOnly: !!provider.readOnly,
317
327
  nonEditable: typeof provider.nonEditable === 'boolean'
318
328
  ? provider.nonEditable
319
329
  : this.isNonDeletableProvider(provider),
320
330
  useTransform: isTransformProvider
321
331
  };
332
+ this._editProviderOriginalKey = '';
333
+ this._editProviderRealKeyLoaded = false;
334
+ this.showEditProviderKey = false;
322
335
  this.showEditModal = true;
323
336
 
337
+ // 后台加载真实密钥
338
+ try {
339
+ const res = await api('get-provider-key', { name: provider.name });
340
+ if (
341
+ this._openEditModalRequestId === requestId
342
+ && this.showEditModal
343
+ && this.editingProvider
344
+ && this.editingProvider.name === provider.name
345
+ && res && !res.error
346
+ ) {
347
+ this._editProviderOriginalKey = typeof res.key === 'string' ? res.key : '';
348
+ this._editProviderRealKeyLoaded = true;
349
+ // 如果用户未修改输入框,替换为真实密钥
350
+ if (this.editingProvider.key === maskKeyForEdit(provider.key || '')) {
351
+ this.editingProvider.key = this._editProviderOriginalKey;
352
+ }
353
+ }
354
+ } catch (_) {
355
+ // ignore
356
+ }
357
+
324
358
  if (isTransformProvider) {
325
359
  try {
326
360
  const res = await api('openai-bridge-get-provider', { name: provider.name });
@@ -357,8 +391,12 @@ export function createProvidersMethods(options = {}) {
357
391
  if (this.editingProvider && this.editingProvider.useTransform) {
358
392
  params.useTransform = true;
359
393
  }
360
- if (typeof this.editingProvider.key === 'string' && this.editingProvider.key.trim()) {
361
- params.key = this.editingProvider.key;
394
+ if (this._editProviderRealKeyLoaded) {
395
+ const currentKey = typeof this.editingProvider.key === 'string' ? this.editingProvider.key : '';
396
+ const originalKey = typeof this._editProviderOriginalKey === 'string' ? this._editProviderOriginalKey : '';
397
+ if (currentKey !== originalKey) {
398
+ params.key = currentKey;
399
+ }
362
400
  }
363
401
  try {
364
402
  const res = await api('update-provider', params);
@@ -370,11 +408,12 @@ export function createProvidersMethods(options = {}) {
370
408
  // 本地更新:更新列表中对应 provider 的 url 和 key
371
409
  this.providersList = this.providersList.map(p => {
372
410
  if (p.name === validation.name) {
411
+ const keyUpdated = typeof params.key === 'string';
373
412
  return {
374
413
  ...p,
375
414
  url: validation.url,
376
- key: params.key ? maskKeyLocal(params.key) : p.key,
377
- hasKey: params.key ? true : p.hasKey
415
+ key: keyUpdated ? maskKeyLocal(params.key) : p.key,
416
+ hasKey: keyUpdated ? !!params.key : p.hasKey
378
417
  };
379
418
  }
380
419
  return p;
@@ -389,9 +428,16 @@ export function createProvidersMethods(options = {}) {
389
428
 
390
429
  closeEditModal() {
391
430
  this.showEditModal = false;
431
+ this.showEditProviderKey = false;
432
+ this._editProviderOriginalKey = '';
433
+ this._editProviderRealKeyLoaded = false;
392
434
  this.editingProvider = { name: '', url: '', key: '', readOnly: false, nonEditable: false, useTransform: false };
393
435
  },
394
436
 
437
+ toggleEditProviderKey() {
438
+ this.showEditProviderKey = !this.showEditProviderKey;
439
+ },
440
+
395
441
  async resetConfig() {
396
442
  if (this.resetConfigLoading) return;
397
443
  this.resetConfigLoading = true;
@@ -203,7 +203,7 @@ export function createSessionActionMethods(options = {}) {
203
203
  quoteShellArg(value) {
204
204
  const text = typeof value === 'string' ? value : String(value || '');
205
205
  if (!text) return "''";
206
- if (/^[a-zA-Z0-9._-]+$/.test(text)) return text;
206
+ if (/^[a-zA-Z0-9._/:@~+=-]+$/.test(text)) return text;
207
207
  const escaped = text.replace(/'/g, "'\\''");
208
208
  return `'${escaped}'`;
209
209
  },
@@ -466,6 +466,29 @@ export function createSessionBrowserMethods(options = {}) {
466
466
  this.persistSessionPinnedMap();
467
467
  },
468
468
 
469
+ setSessionSource(value) {
470
+ if (this.sessionsLoading) return;
471
+ this.sessionFilterSource = value;
472
+ this.refreshSessionPathOptions(value);
473
+ this.persistSessionFilterCache();
474
+ syncSessionsFilterUrl(this);
475
+ this.loadSessions();
476
+ },
477
+
478
+ highlightQueryText(text) {
479
+ if (typeof text !== 'string' || !text) return text;
480
+ var tokens = this.queryTokens;
481
+ if (!tokens || tokens.length === 0) return text;
482
+ var result = text;
483
+ for (var i = 0; i < tokens.length; i++) {
484
+ var token = tokens[i];
485
+ var escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\ async onSessionSourceChange(event) {');
486
+ var re = new RegExp('(' + escaped + ')', 'gi');
487
+ result = result.replace(re, '<mark>$1</mark>');
488
+ }
489
+ return result;
490
+ },
491
+
469
492
  async onSessionSourceChange(event) {
470
493
  const rawValue = event && event.target && typeof event.target.value === 'string'
471
494
  ? event.target.value
@@ -817,7 +840,11 @@ export function createSessionBrowserMethods(options = {}) {
817
840
  ? Math.max(1, Math.min(rawLimit, 2000))
818
841
  : compareBoost;
819
842
  const loadedLimit = Number(this.sessionsUsageLoadedLimit || 0);
820
- if (this.sessionsUsageLoadedOnce && !options.forceRefresh && loadedLimit >= limit) {
843
+ const lastRange = typeof this.sessionsUsageLastLoadedRange === 'string'
844
+ ? this.sessionsUsageLastLoadedRange
845
+ : '';
846
+ const rangeChanged = lastRange && lastRange !== range;
847
+ if (this.sessionsUsageLoadedOnce && !options.forceRefresh && !rangeChanged && loadedLimit >= limit) {
821
848
  return;
822
849
  }
823
850
  this.sessionsUsageLoading = true;
@@ -844,6 +871,7 @@ export function createSessionBrowserMethods(options = {}) {
844
871
  if (loadSucceeded) {
845
872
  this.sessionsUsageLoadedOnce = true;
846
873
  this.sessionsUsageLoadedLimit = limit;
874
+ this.sessionsUsageLastLoadedRange = range;
847
875
  if (!this.sessionsUsageSelectedDayKey && Array.isArray(this.sessionUsageDailyTableRows) && this.sessionUsageDailyTableRows.length > 0) {
848
876
  this.sessionsUsageSelectedDayKey = this.sessionUsageDailyTableRows[0].key;
849
877
  }
@@ -60,6 +60,9 @@ export function createStartupClaudeMethods(options = {}) {
60
60
  return false;
61
61
  }
62
62
  this.currentProvider = statusRes.provider;
63
+ if (statusRes.version) {
64
+ this.appVersion = statusRes.version;
65
+ }
63
66
  this.currentModels = statusRes.currentModels && typeof statusRes.currentModels === 'object'
64
67
  ? { ...statusRes.currentModels }
65
68
  : {};
@@ -119,6 +122,7 @@ export function createStartupClaudeMethods(options = {}) {
119
122
  }
120
123
  this.providersList = listRes.providers;
121
124
  if (typeof this.loadLocalBridgeExcluded === 'function') { this.loadLocalBridgeExcluded(); }
125
+ if (typeof this.loadClaudeLocalBridgeStatus === 'function') { this.loadClaudeLocalBridgeStatus(); }
122
126
  if (statusRes.configReady === false) {
123
127
  this.showMessage('配置已加载', 'info');
124
128
  }
@@ -9,6 +9,7 @@ const DICT = Object.freeze({
9
9
  // Common
10
10
  'common.all': '全部',
11
11
  'common.copy': '复制',
12
+ 'common.paste': '粘贴',
12
13
  'common.edit': '编辑',
13
14
  'common.install': '安装',
14
15
  'common.update': '升级',
@@ -1036,11 +1037,16 @@ const DICT = Object.freeze({
1036
1037
  'claude.notConfigured': '未配置',
1037
1038
  'claude.action.edit': '编辑',
1038
1039
  'claude.action.delete': '删除',
1039
- 'claude.action.shareDisabled': '分享导入命令(暂时禁用)',
1040
+ 'claude.action.shareDisabled': '分享导入命令',
1040
1041
  'claude.action.editAria': '编辑 Claude 配置:{name}',
1041
1042
  'claude.action.deleteAria': '删除 Claude 配置:{name}',
1042
1043
  'claude.action.clone': '克隆',
1043
1044
  'claude.action.cloneAria': '克隆 Claude 配置:{name}',
1045
+ 'claude.localBridge.poolTitle': '轮询池',
1046
+ 'claude.localBridge.poolHint': '勾选参与负载均衡的提供商',
1047
+ 'claude.localBridge.noProviders': '暂无可用提供商,请先添加直连提供商',
1048
+ 'claude.localBridge.disabled': '未启用',
1049
+ 'claude.localBridge.enabled': '已启用',
1044
1050
 
1045
1051
  // OpenClaw config panel
1046
1052
  'openclaw.applyHint': '写入 ~/.openclaw/openclaw.json,支持 JSON5。',
@@ -1071,6 +1077,7 @@ const DICT = Object.freeze({
1071
1077
  // Common
1072
1078
  'common.all': 'すべて',
1073
1079
  'common.copy': 'コピー',
1080
+ 'common.paste': 'ペースト',
1074
1081
  'common.edit': '編集',
1075
1082
  'common.install': 'インストール',
1076
1083
  'common.update': 'アップグレード',
@@ -2085,11 +2092,16 @@ const DICT = Object.freeze({
2085
2092
  'claude.notConfigured': '未設定',
2086
2093
  'claude.action.edit': '編集',
2087
2094
  'claude.action.delete': '削除',
2088
- 'claude.action.shareDisabled': 'インポートコマンド共有(一時無効)',
2095
+ 'claude.action.shareDisabled': 'インポートコマンド共有',
2089
2096
  'claude.action.editAria': 'Claude 設定を編集:{name}',
2090
2097
  'claude.action.deleteAria': 'Claude 設定を削除:{name}',
2091
2098
  'claude.action.clone': 'クローン',
2092
2099
  'claude.action.cloneAria': 'Claude 設定をクローン:{name}',
2100
+ 'claude.localBridge.poolTitle': 'ラウンドロビンプール',
2101
+ 'claude.localBridge.poolHint': '負荷分散に参加するプロバイダを選択',
2102
+ 'claude.localBridge.noProviders': '利用可能なプロバイダがありません。まずプロバイダを追加してください。',
2103
+ 'claude.localBridge.disabled': '無効',
2104
+ 'claude.localBridge.enabled': '有効',
2093
2105
 
2094
2106
  // OpenClaw config panel
2095
2107
  'openclaw.applyHint': '~/.openclaw/openclaw.json に書き込みます。JSON5 対応。',
@@ -2121,6 +2133,7 @@ const DICT = Object.freeze({
2121
2133
  // Common
2122
2134
  'common.all': 'All',
2123
2135
  'common.copy': 'Copy',
2136
+ 'common.paste': 'Paste',
2124
2137
  'common.edit': 'Edit',
2125
2138
  'common.install': 'Install',
2126
2139
  'common.update': 'Update',
@@ -3144,11 +3157,16 @@ const DICT = Object.freeze({
3144
3157
  'claude.notConfigured': 'Not configured',
3145
3158
  'claude.action.edit': 'Edit',
3146
3159
  'claude.action.delete': 'Delete',
3147
- 'claude.action.shareDisabled': 'Share import command (disabled)',
3160
+ 'claude.action.shareDisabled': 'Share import command',
3148
3161
  'claude.action.editAria': 'Edit Claude config: {name}',
3149
3162
  'claude.action.deleteAria': 'Delete Claude config: {name}',
3150
3163
  'claude.action.clone': 'Clone',
3151
3164
  'claude.action.cloneAria': 'Clone Claude config: {name}',
3165
+ 'claude.localBridge.poolTitle': 'Round-robin pool',
3166
+ 'claude.localBridge.poolHint': 'Select providers for load balancing',
3167
+ 'claude.localBridge.noProviders': 'No providers available. Add a provider first.',
3168
+ 'claude.localBridge.disabled': 'Disabled',
3169
+ 'claude.localBridge.enabled': 'Enabled',
3152
3170
 
3153
3171
  // OpenClaw config panel
3154
3172
  'openclaw.applyHint': 'Writes to ~/.openclaw/openclaw.json (JSON5 supported).',
@@ -122,8 +122,7 @@
122
122
  <div class="brand-head">
123
123
  <img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
124
124
  <div class="brand-copy">
125
- <div class="brand-kicker">{{ t('brand.kicker.workspace') }}</div>
126
- <div class="brand-title">Codex Mate</div>
125
+ <div class="brand-kicker">Codex Mate <span v-if="appVersion" class="brand-version">v{{ appVersion }}</span></div>
127
126
  </div>
128
127
  </div>
129
128
  <div class="brand-subtitle">{{ t('brand.subtitle.localConfigSessionsWorkspace') }}</div>
@@ -1,6 +1,11 @@
1
1
  <div v-if="showConfigTemplateModal" class="modal-overlay" @click.self="!configTemplateApplying && closeConfigTemplateModal()">
2
2
  <div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="config-template-modal-title">
3
- <div class="modal-title" id="config-template-modal-title">{{ t('modal.configTemplate.title') }}</div>
3
+ <div class="modal-header modal-editor-header">
4
+ <div class="modal-title" id="config-template-modal-title">{{ t('modal.configTemplate.title') }}</div>
5
+ <div class="modal-header-actions">
6
+ <button class="btn-mini btn-modal-copy" @click="pasteConfigTemplateContent" :disabled="configTemplateApplying || configTemplateDiffLoading || configTemplateDiffVisible">{{ t('common.paste') }}</button>
7
+ </div>
8
+ </div>
4
9
 
5
10
  <div class="form-group">
6
11
  <label class="form-label">{{ t('modal.configTemplate.label') }}</label>
@@ -91,6 +96,12 @@
91
96
  :disabled="agentsLoading">
92
97
  {{ t('modal.agents.copy') }}
93
98
  </button>
99
+ <button
100
+ class="btn-mini btn-modal-copy"
101
+ @click="pasteAgentsContent"
102
+ :disabled="agentsLoading || agentsSaving || agentsDiffVisible">
103
+ {{ t('common.paste') }}
104
+ </button>
94
105
  </div>
95
106
  </div>
96
107
 
@@ -65,8 +65,13 @@
65
65
  </div>
66
66
  <div class="form-group">
67
67
  <label class="form-label">{{ t('field.apiKey') }}</label>
68
- <input v-model="editingProvider.key" class="form-input" type="password" :placeholder="t('placeholder.keepUnchanged')">
69
- <div class="form-hint">{{ t('hint.keepKeyUnchanged') }}</div>
68
+ <div class="input-with-toggle">
69
+ <input v-model="editingProvider.key" class="form-input" :type="showEditProviderKey ? 'text' : 'password'" placeholder="sk-..." autocomplete="off" spellcheck="false">
70
+ <button type="button" class="input-toggle-btn" @click="toggleEditProviderKey" :title="showEditProviderKey ? t('common.hide') : t('common.show')">
71
+ <svg v-if="!showEditProviderKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
72
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
73
+ </button>
74
+ </div>
70
75
  </div>
71
76
 
72
77
  <div class="btn-group">
@@ -147,7 +152,13 @@
147
152
  </div>
148
153
  <div class="form-group">
149
154
  <label class="form-label">API Key</label>
150
- <input v-model="editingConfig.apiKey" class="form-input" type="password" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
155
+ <div class="input-with-toggle">
156
+ <input v-model="editingConfig.apiKey" class="form-input" :type="showEditClaudeConfigKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
157
+ <button type="button" class="input-toggle-btn" @click="toggleEditClaudeConfigKey" :title="showEditClaudeConfigKey ? t('common.hide') : t('common.show')">
158
+ <svg v-if="!showEditClaudeConfigKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
159
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
160
+ </button>
161
+ </div>
151
162
  </div>
152
163
  <div class="form-group">
153
164
  <label class="form-label">{{ t('field.baseUrl') }}</label>
@@ -1,4 +1,4 @@
1
- <!-- Claude Code 配置模式 -->
1
+ <!-- Claude Code 配置 -->
2
2
  <div
3
3
  v-show="mainTab === 'config' && configMode === 'claude'"
4
4
  class="mode-content mode-cards"
@@ -18,11 +18,7 @@
18
18
  <div class="docs-command-row">
19
19
  <div class="docs-command-box" role="group" :aria-label="t('cli.missing.commandAria', { name: 'Claude' })">
20
20
  <code class="install-command">{{ getInstallCommand('claude', 'install') }}</code>
21
- <button
22
- type="button"
23
- class="btn-mini docs-copy-btn"
24
- :disabled="!getInstallCommand('claude', 'install')"
25
- @click="copyInstallCommand(getInstallCommand('claude', 'install'))">{{ t('common.copy') }}</button>
21
+ <button type="button" class="btn-mini docs-copy-btn" :disabled="!getInstallCommand('claude', 'install')" @click="copyInstallCommand(getInstallCommand('claude', 'install'))">{{ t('common.copy') }}</button>
26
22
  </div>
27
23
  </div>
28
24
  <button type="button" class="btn-tool btn-tool-compact" @click="mainTab = 'docs'; setInstallCommandAction('install')">{{ t('cli.missing.openDocs') }}</button>
@@ -30,21 +26,14 @@
30
26
  </div>
31
27
  </template>
32
28
  <template v-else>
33
- <!-- 添加提供商按钮 -->
34
29
  <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
35
- <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
36
- <path d="M10 4v12M4 10h12"/>
37
- </svg>
30
+ <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 4v12M4 10h12"/></svg>
38
31
  {{ t('claude.addProvider') }}
39
32
  </button>
40
- <div class="config-template-hint">
41
- {{ t('claude.applyDefault') }}
42
- </div>
33
+ <div class="config-template-hint">{{ t('claude.applyDefault') }}</div>
43
34
 
44
35
  <div class="selector-section">
45
- <div class="selector-header">
46
- <span class="selector-title">{{ t('claude.presetProviders') }}</span>
47
- </div>
36
+ <div class="selector-header"><span class="selector-title">{{ t('claude.presetProviders') }}</span></div>
48
37
  <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
49
38
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showClaudeConfigModal = true">Claude Official</button>
50
39
  <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showClaudeConfigModal = true">DeepSeek</button>
@@ -70,9 +59,7 @@
70
59
  </div>
71
60
 
72
61
  <div class="selector-section">
73
- <div class="selector-header">
74
- <span class="selector-title">{{ t('claude.model') }}</span>
75
- </div>
62
+ <div class="selector-header"><span class="selector-title">{{ t('claude.model') }}</span></div>
76
63
  <input
77
64
  v-if="claudeModelHasList"
78
65
  class="model-input"
@@ -94,57 +81,37 @@
94
81
  @keyup.enter="onClaudeModelChange"
95
82
  :placeholder="t('claude.model.placeholder')"
96
83
  >
97
- <div class="config-template-hint">
98
- {{ t('claude.model.hint') }}
99
- </div>
84
+ <div class="config-template-hint">{{ t('claude.model.hint') }}</div>
100
85
  </div>
101
86
 
102
87
  <div class="selector-section">
103
- <div class="selector-header">
104
- <span class="selector-title">CLAUDE.md</span>
105
- </div>
106
- <button class="btn-tool" @click="openClaudeMdEditor" :disabled="loading || !!initError || agentsLoading">
107
- {{ agentsLoading ? t('config.modelLoading') : t('claude.md.open') }}
108
- </button>
109
- <div class="config-template-hint">
110
- {{ t('claude.md.hint') }}
111
- </div>
88
+ <div class="selector-header"><span class="selector-title">CLAUDE.md</span></div>
89
+ <button class="btn-tool" @click="openClaudeMdEditor" :disabled="loading || !!initError || agentsLoading">{{ agentsLoading ? t('config.modelLoading') : t('claude.md.open') }}</button>
90
+ <div class="config-template-hint">{{ t('claude.md.hint') }}</div>
112
91
  </div>
113
92
 
114
93
  <div class="selector-section">
115
- <div class="selector-header">
116
- <span class="selector-title">{{ t('claude.health.title') }}</span>
117
- </div>
118
- <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
119
- {{ healthCheckLoading ? t('claude.health.running') : t('claude.health.run') }}
120
- </button>
121
- <div class="config-template-hint">{{ t('claude.health.hint') }}</div>
122
- <div v-if="healthCheckLoading && healthCheckBatchTotal" class="config-template-hint">
123
- {{ t('claude.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }}
124
- </div>
125
- <div v-if="healthCheckResult && !healthCheckLoading" class="config-template-hint">
126
- {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }}
127
- </div>
128
- <button v-if="healthCheckResult && !healthCheckLoading" type="button" class="btn-mini" @click="showHealthCheckModal = true">
129
- {{ t('common.detail') }}
130
- </button>
131
- <div v-if="healthCheckResult && !healthCheckLoading && (healthCheckResult.issues || []).length">
132
- <div v-for="(issue, index) in healthCheckResult.issues" :key="issue.code || ('issue-' + index)" class="config-template-hint">
133
- {{ issue.message || issue.code || '' }}<span v-if="issue.suggestion"> · {{ issue.suggestion }}</span>
134
- </div>
135
- </div>
94
+ <div class="selector-header"><span class="selector-title">{{ t('config.health.title') }}</span></div>
95
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">{{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}</button>
96
+ <div class="config-template-hint">{{ t('config.health.hint') }}</div>
136
97
  </div>
137
98
 
138
-
139
99
  <div class="card-list">
140
- <div v-for="(config, name) in claudeConfigs" :key="name"
141
- :class="['card', { active: currentClaudeConfig === name }]"
142
- @click="applyClaudeConfig(name)"
143
- @keydown.enter.self.prevent="applyClaudeConfig(name)"
144
- @keydown.space.self.prevent="applyClaudeConfig(name)"
145
- tabindex="0"
146
- role="button"
147
- :aria-current="currentClaudeConfig === name ? 'true' : null">
100
+ <div :class="['card', { active: currentClaudeConfig === 'claude-local' }]" @click="currentClaudeConfig = 'claude-local'" @keydown.enter.self.prevent="currentClaudeConfig = 'claude-local'" @keydown.space.self.prevent="currentClaudeConfig = 'claude-local'" tabindex="0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
101
+ <div class="card-leading">
102
+ <div class="card-icon">L</div>
103
+ <div class="card-content">
104
+ <div class="card-title">
105
+ <span>local</span>
106
+ <span class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <div class="card-trailing">
111
+ <span :class="['pill', claudeLocalBridgeConfigured() ? 'configured' : 'empty']">{{ claudeLocalBridgeConfigured() ? t('claude.configured') : t('claude.notConfigured') }}</span>
112
+ </div>
113
+ </div>
114
+ <div v-for="(config, name) in claudeConfigs" :key="name" :class="['card', { active: currentClaudeConfig === name }]" @click="applyClaudeConfig(name)" @keydown.enter.self.prevent="applyClaudeConfig(name)" @keydown.space.self.prevent="applyClaudeConfig(name)" tabindex="0" role="button" :aria-current="currentClaudeConfig === name ? 'true' : null">
148
115
  <div class="card-leading">
149
116
  <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
150
117
  <div class="card-content">
@@ -154,41 +121,46 @@
154
121
  </div>
155
122
  </div>
156
123
  <div class="card-trailing">
157
- <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">
158
- {{ formatLatency(claudeSpeedResults[name]) }}
159
- </span>
160
- <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
161
- {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}
162
- </span>
124
+ <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">{{ formatLatency(claudeSpeedResults[name]) }}</span>
125
+ <span :class="['pill', config.hasKey ? 'configured' : 'empty']">{{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}</span>
163
126
  <div class="card-actions" @click.stop>
164
127
  <button class="card-action-btn" @click="openEditConfigModal(name)" :aria-label="t('claude.action.editAria', { name })" :title="t('claude.action.edit')">
165
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
166
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
167
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
168
- </svg>
128
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
169
129
  </button>
170
130
  <button class="card-action-btn" @click="openCloneClaudeConfigModal(name, config)" :aria-label="t('claude.action.cloneAria', { name })" :title="t('claude.action.clone')">
171
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
172
- <rect x="9" y="9" width="13" height="13" rx="2"/>
173
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
174
- </svg>
131
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
175
132
  </button>
176
- <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" :aria-label="t('config.shareCommand.aria')">
177
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
178
- <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
179
- <path d="M16 6l-4-4-4 4"/>
180
- <path d="M12 2v14"/>
181
- </svg>
133
+ <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" :title="t('config.shareCommand')" :aria-label="t('config.shareCommand.aria')">
134
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/><path d="M16 6l-4-4-4 4"/><path d="M12 2v14"/></svg>
182
135
  </button>
183
136
  <button class="card-action-btn delete" @click="deleteClaudeConfig(name)" :aria-label="t('claude.action.deleteAria', { name })" :title="t('claude.action.delete')">
184
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
185
- <path d="M3 6h18"/>
186
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
187
- </svg>
137
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
188
138
  </button>
189
139
  </div>
190
140
  </div>
191
141
  </div>
192
142
  </div>
143
+
144
+ <div v-if="currentClaudeConfig === 'claude-local'" class="bridge-pool-panel">
145
+ <div class="bridge-pool-header">
146
+ <span class="bridge-pool-icon">
147
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
148
+ </span>
149
+ <span class="bridge-pool-title">{{ t('claude.localBridge.poolTitle') }}</span>
150
+ <span class="bridge-pool-hint">{{ t('claude.localBridge.poolHint') }}</span>
151
+ </div>
152
+ <div v-if="Object.keys(claudeConfigs || {}).length === 0" class="bridge-pool-empty">
153
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
154
+ <span>{{ t('claude.localBridge.noProviders') }}</span>
155
+ </div>
156
+ <div v-else class="bridge-pool-list">
157
+ <label v-for="(config, name) in claudeConfigs" :key="name" class="bridge-pool-item">
158
+ <span class="bridge-pool-item-name">{{ name }}</span>
159
+ <span class="bridge-pool-item-status" :class="{ active: !isClaudeLocalBridgeExcluded(name) }">{{ isClaudeLocalBridgeExcluded(name) ? t('claude.localBridge.disabled') : t('claude.localBridge.enabled') }}</span>
160
+ <input type="checkbox" :checked="!isClaudeLocalBridgeExcluded(name)" @change="toggleClaudeLocalBridgeExcluded(name)" />
161
+ </label>
162
+ </div>
163
+ </div>
164
+
193
165
  </template>
194
166
  </div>