codexmate 0.0.38 → 0.0.40

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 (43) hide show
  1. package/cli/builtin-proxy.js +626 -207
  2. package/cli/config-bootstrap.js +6 -1
  3. package/cli/openai-bridge.js +541 -210
  4. package/cli.js +189 -4
  5. package/package.json +1 -1
  6. package/plugins/prompt-templates/computed.mjs +61 -3
  7. package/plugins/prompt-templates/manifest.mjs +3 -0
  8. package/web-ui/app.js +14 -3
  9. package/web-ui/modules/app.computed.main-tabs.mjs +39 -30
  10. package/web-ui/modules/app.methods.claude-config.mjs +111 -9
  11. package/web-ui/modules/app.methods.index.mjs +2 -0
  12. package/web-ui/modules/app.methods.openclaw-editing.mjs +48 -0
  13. package/web-ui/modules/app.methods.openclaw-persist.mjs +13 -7
  14. package/web-ui/modules/app.methods.providers.mjs +36 -10
  15. package/web-ui/modules/app.methods.runtime.mjs +76 -1
  16. package/web-ui/modules/app.methods.startup-claude.mjs +7 -0
  17. package/web-ui/modules/app.methods.tool-config-permissions.mjs +87 -0
  18. package/web-ui/modules/config-mode.computed.mjs +3 -3
  19. package/web-ui/modules/i18n/locales/en.mjs +1140 -0
  20. package/web-ui/modules/i18n/locales/ja.mjs +1130 -0
  21. package/web-ui/modules/i18n/locales/vi.mjs +239 -0
  22. package/web-ui/modules/i18n/locales/zh.mjs +1143 -0
  23. package/web-ui/modules/i18n.dict.mjs +9 -3195
  24. package/web-ui/modules/i18n.mjs +65 -16
  25. package/web-ui/partials/index/layout-header.html +16 -46
  26. package/web-ui/partials/index/modal-openclaw-config.html +135 -71
  27. package/web-ui/partials/index/modal-webhook.html +8 -8
  28. package/web-ui/partials/index/modals-basic.html +56 -16
  29. package/web-ui/partials/index/panel-config-claude.html +51 -21
  30. package/web-ui/partials/index/panel-config-codex.html +34 -5
  31. package/web-ui/partials/index/panel-config-openclaw.html +70 -64
  32. package/web-ui/partials/index/panel-dashboard.html +62 -77
  33. package/web-ui/partials/index/panel-settings.html +28 -7
  34. package/web-ui/partials/index/panel-trash.html +14 -14
  35. package/web-ui/res/web-ui-render.precompiled.js +1783 -1386
  36. package/web-ui/styles/controls-forms.css +99 -0
  37. package/web-ui/styles/dashboard.css +46 -14
  38. package/web-ui/styles/layout-shell.css +45 -0
  39. package/web-ui/styles/navigation-panels.css +3 -3
  40. package/web-ui/styles/openclaw-structured.css +383 -33
  41. package/web-ui/styles/responsive.css +68 -0
  42. package/web-ui/styles/sessions-usage.css +105 -9
  43. package/web-ui/styles/settings-panel.css +4 -0
@@ -25,9 +25,27 @@
25
25
  @blur="normalizeProviderDraft('add')">
26
26
  <div v-if="providerFieldError('add', 'url')" class="form-hint form-error">{{ providerFieldError('add', 'url') }}</div>
27
27
  </div>
28
+ <div class="form-group">
29
+ <label class="form-label">{{ t('field.modelName') }}</label>
30
+ <input
31
+ v-model="newProvider.model"
32
+ :class="['form-input', { invalid: !!providerFieldError('add', 'model') }]"
33
+ :placeholder="t('placeholder.modelExample')"
34
+ autocomplete="off"
35
+ spellcheck="false"
36
+ @blur="normalizeProviderDraft('add')">
37
+ <div v-if="providerFieldError('add', 'model')" class="form-hint form-error">{{ providerFieldError('add', 'model') }}</div>
38
+ </div>
28
39
  <div class="form-group">
29
40
  <label class="form-label">{{ t('field.apiKey') }}</label>
30
- <input v-model="newProvider.key" class="form-input" type="password" placeholder="sk-...">
41
+ <div class="input-with-toggle">
42
+ <input v-model="newProvider.key" :class="['form-input', { invalid: !!providerFieldError('add', 'key') }]" :type="showAddProviderKey ? 'text' : 'password'" placeholder="sk-..." autocomplete="off" spellcheck="false" @blur="normalizeProviderDraft('add')">
43
+ <button type="button" class="input-toggle-btn" @click="toggleAddProviderKey" :title="showAddProviderKey ? t('common.hide') : t('common.show')" :aria-label="showAddProviderKey ? t('common.hide') : t('common.show')">
44
+ <svg v-if="!showAddProviderKey" 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>
45
+ <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>
46
+ </button>
47
+ </div>
48
+ <div v-if="providerFieldError('add', 'key')" class="form-hint form-error">{{ providerFieldError('add', 'key') }}</div>
31
49
  </div>
32
50
  <div class="form-group">
33
51
  <label class="form-label">
@@ -67,7 +85,7 @@
67
85
  <label class="form-label">{{ t('field.apiKey') }}</label>
68
86
  <div class="input-with-toggle">
69
87
  <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')">
88
+ <button type="button" class="input-toggle-btn" @click="toggleEditProviderKey" :title="showEditProviderKey ? t('common.hide') : t('common.show')" :aria-label="showEditProviderKey ? t('common.hide') : t('common.show')">
71
89
  <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
90
  <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
91
  </button>
@@ -123,20 +141,34 @@
123
141
 
124
142
  <div class="form-group">
125
143
  <label class="form-label">{{ t('field.configName') }}</label>
126
- <input v-model="newClaudeConfig.name" class="form-input" :placeholder="t('placeholder.configNameExample')">
144
+ <input v-model="newClaudeConfig.name" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'name') }]" :placeholder="t('placeholder.configNameExample')">
145
+ <div v-if="claudeConfigFieldError('add', 'name')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'name') }}</div>
127
146
  </div>
128
147
  <div class="form-group">
129
148
  <label class="form-label">API Key</label>
130
- <input v-model="newClaudeConfig.apiKey" class="form-input" type="password" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
149
+ <div class="input-with-toggle">
150
+ <input v-model="newClaudeConfig.apiKey" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'apiKey') }]" :type="showAddClaudeConfigKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
151
+ <button type="button" class="input-toggle-btn" @click="toggleAddClaudeConfigKey" :title="showAddClaudeConfigKey ? t('common.hide') : t('common.show')" :aria-label="showAddClaudeConfigKey ? t('common.hide') : t('common.show')">
152
+ <svg v-if="!showAddClaudeConfigKey" 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>
153
+ <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>
154
+ </button>
155
+ </div>
156
+ <div v-if="claudeConfigFieldError('add', 'apiKey')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'apiKey') }}</div>
131
157
  </div>
132
158
  <div class="form-group">
133
159
  <label class="form-label">{{ t('field.baseUrl') }}</label>
134
- <input v-model="newClaudeConfig.baseUrl" class="form-input" :placeholder="t('placeholder.baseUrlExampleClaude')">
160
+ <input v-model="newClaudeConfig.baseUrl" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'baseUrl') }]" :placeholder="t('placeholder.baseUrlExampleClaude')">
161
+ <div v-if="claudeConfigFieldError('add', 'baseUrl')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'baseUrl') }}</div>
162
+ </div>
163
+ <div class="form-group">
164
+ <label class="form-label">{{ t('field.modelName') }}</label>
165
+ <input v-model="newClaudeConfig.model" :class="['form-input', { invalid: !!claudeConfigFieldError('add', 'model') }]" :placeholder="t('placeholder.modelExample')" autocomplete="off" spellcheck="false">
166
+ <div v-if="claudeConfigFieldError('add', 'model')" class="form-hint form-error">{{ claudeConfigFieldError('add', 'model') }}</div>
135
167
  </div>
136
168
 
137
169
  <div class="btn-group">
138
170
  <button class="btn btn-cancel" @click="closeClaudeConfigModal">{{ t('common.cancel') }}</button>
139
- <button class="btn btn-confirm" @click="addClaudeConfig">{{ t('common.add') }}</button>
171
+ <button class="btn btn-confirm" @click="addClaudeConfig" :disabled="!canSubmitClaudeConfig('add')">{{ t('common.add') }}</button>
140
172
  </div>
141
173
  </div>
142
174
  </div>
@@ -148,46 +180,54 @@
148
180
 
149
181
  <div class="form-group">
150
182
  <label class="form-label">{{ t('field.configName') }}</label>
151
- <input v-model="editingConfig.name" class="form-input" :placeholder="t('field.configName')" readonly>
183
+ <input v-model="editingConfig.name" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'name') }]" :placeholder="t('field.configName')" readonly>
184
+ <div v-if="claudeConfigFieldError('edit', 'name')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'name') }}</div>
152
185
  </div>
153
186
  <div class="form-group">
154
187
  <label class="form-label">API Key</label>
155
188
  <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')">
189
+ <input v-model="editingConfig.apiKey" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'apiKey') }]" :type="showEditClaudeConfigKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" :placeholder="t('placeholder.apiKeyExampleClaude')">
190
+ <button type="button" class="input-toggle-btn" @click="toggleEditClaudeConfigKey" :title="showEditClaudeConfigKey ? t('common.hide') : t('common.show')" :aria-label="showEditClaudeConfigKey ? t('common.hide') : t('common.show')">
158
191
  <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
192
  <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
193
  </button>
161
194
  </div>
195
+ <div v-if="claudeConfigFieldError('edit', 'apiKey')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'apiKey') }}</div>
162
196
  </div>
163
197
  <div class="form-group">
164
198
  <label class="form-label">{{ t('field.baseUrl') }}</label>
165
- <input v-model="editingConfig.baseUrl" class="form-input" :placeholder="t('placeholder.baseUrlExampleClaude')">
199
+ <input v-model="editingConfig.baseUrl" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'baseUrl') }]" :placeholder="t('placeholder.baseUrlExampleClaude')">
200
+ <div v-if="claudeConfigFieldError('edit', 'baseUrl')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'baseUrl') }}</div>
201
+ </div>
202
+ <div class="form-group">
203
+ <label class="form-label">{{ t('field.modelName') }}</label>
204
+ <input v-model="editingConfig.model" :class="['form-input', { invalid: !!claudeConfigFieldError('edit', 'model') }]" :placeholder="t('placeholder.modelExample')" autocomplete="off" spellcheck="false">
205
+ <div v-if="claudeConfigFieldError('edit', 'model')" class="form-hint form-error">{{ claudeConfigFieldError('edit', 'model') }}</div>
166
206
  </div>
167
207
 
168
208
  <div class="btn-group">
169
209
  <button class="btn btn-cancel" @click="closeEditConfigModal">{{ t('common.cancel') }}</button>
170
- <button class="btn btn-confirm" @click="saveAndApplyConfig">{{ t('common.saveApply') }}</button>
210
+ <button class="btn btn-confirm" @click="saveAndApplyConfig" :disabled="!canSubmitClaudeConfig('edit')">{{ t('common.saveApply') }}</button>
171
211
  </div>
172
212
  </div>
173
213
  </div>
174
214
 
175
- <!-- Codex 轮询池控制模态框 -->
215
+ <!-- Codex bridge pool modal -->
176
216
  <div v-if="showCodexBridgePoolModal" class="modal-overlay" @click.self="showCodexBridgePoolModal = false">
177
217
  <div class="modal modal-bridge-pool" role="dialog" aria-modal="true" aria-labelledby="codex-bridge-pool-modal-title">
178
218
  <div class="modal-title" id="codex-bridge-pool-modal-title">
179
219
  <svg class="modal-title-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><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>
180
- 轮询池设置
220
+ {{ t('config.localBridge.poolSettings') }}
181
221
  </div>
182
- <div class="bridge-pool-modal-hint">勾选参与负载均衡的提供商</div>
222
+ <div class="bridge-pool-modal-hint">{{ t('config.localBridge.poolHint') }}</div>
183
223
  <div v-if="localBridgeCandidateProviders().length === 0" class="bridge-pool-empty">
184
224
  <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>
185
- <span>暂无可用上游 provider,请先添加直连 provider</span>
225
+ <span>{{ t('config.localBridge.noProviders') }}</span>
186
226
  </div>
187
227
  <div v-else class="bridge-pool-list">
188
228
  <label v-for="cp in localBridgeCandidateProviders()" :key="cp.name" class="bridge-pool-item">
189
229
  <span class="bridge-pool-item-name">{{ cp.name }}</span>
190
- <span class="bridge-pool-item-status" :class="{ active: !isLocalBridgeExcluded(cp.name) }">{{ isLocalBridgeExcluded(cp.name) ? '未启用' : '已启用' }}</span>
230
+ <span class="bridge-pool-item-status" :class="{ active: !isLocalBridgeExcluded(cp.name) }">{{ isLocalBridgeExcluded(cp.name) ? t('common.disabled') : t('common.enabled') }}</span>
191
231
  <input type="checkbox" :checked="!isLocalBridgeExcluded(cp.name)" @change="toggleLocalBridgeExcluded(cp.name)" />
192
232
  </label>
193
233
  </div>
@@ -26,7 +26,27 @@
26
26
  </div>
27
27
  </template>
28
28
  <template v-else>
29
- <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
29
+ <section class="tool-config-write-card" :aria-label="t('toolConfig.claude.title')">
30
+ <div class="tool-config-write-copy">
31
+ <div class="tool-config-write-title">{{ t('toolConfig.claude.title') }}</div>
32
+ <p class="tool-config-write-desc">{{ t('toolConfig.claude.desc') }}</p>
33
+ </div>
34
+ <label class="settings-toggle-row tool-config-write-toggle">
35
+ <input
36
+ type="checkbox"
37
+ :checked="isToolConfigWriteAllowed('claude')"
38
+ :disabled="toolConfigPermissionSaving.claude"
39
+ @change="setToolConfigPermission('claude', $event.target.checked)">
40
+ <span class="toggle-track">
41
+ <span class="toggle-thumb"></span>
42
+ </span>
43
+ <span>{{ toolConfigPermissionStatusLabel('claude') }}</span>
44
+ </label>
45
+ </section>
46
+
47
+ <div class="tool-config-write-scope" :class="{ locked: !isToolConfigWriteAllowed('claude') }">
48
+ <div class="tool-config-write-body">
49
+ <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError" :disabled="!isToolConfigWriteAllowed('claude')">
30
50
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 4v12M4 10h12"/></svg>
31
51
  {{ t('claude.addProvider') }}
32
52
  </button>
@@ -35,26 +55,26 @@
35
55
  <div class="selector-section">
36
56
  <div class="selector-header"><span class="selector-title">{{ t('claude.presetProviders') }}</span></div>
37
57
  <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
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>
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>
40
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Zhipu GLM</button>
41
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Z.ai GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.z.ai/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Z.ai GLM</button>
42
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Qwen Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://coding.dashscope.aliyuncs.com/apps/anthropic'; newClaudeConfig.model = 'qwen3-coder'; showClaudeConfigModal = true">Qwen Coder</button>
43
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi k2'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.moonshot.cn/anthropic'; newClaudeConfig.model = 'kimi-k2.5'; showClaudeConfigModal = true">Kimi k2</button>
44
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi For Coding'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.kimi.com/coding/'; newClaudeConfig.model = 'kimi-k2.5'; showClaudeConfigModal = true">Kimi For Coding</button>
45
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showClaudeConfigModal = true">KAT-Coder</button>
46
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Longcat'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.longcat.chat/anthropic'; newClaudeConfig.model = 'LongCat-Flash-Chat'; showClaudeConfigModal = true">Longcat</button>
47
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimaxi.com/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showClaudeConfigModal = true">MiniMax</button>
48
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax en'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimax.io/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showClaudeConfigModal = true">MiniMax en</button>
49
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DouBaoSeed'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://ark.cn-beijing.volces.com/api/coding'; newClaudeConfig.model = 'doubao-seed-2-0-code-preview-latest'; showClaudeConfigModal = true">DouBaoSeed</button>
50
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'BaiLing'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.tbox.cn/api/anthropic'; newClaudeConfig.model = 'Ling-2.5-1T'; showClaudeConfigModal = true">BaiLing</button>
51
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'ModelScope'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api-inference.modelscope.cn'; newClaudeConfig.model = 'ZhipuAI/GLM-5'; showClaudeConfigModal = true">ModelScope</button>
52
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AiHubMix'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://aihubmix.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">AiHubMix</button>
53
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DMXAPI'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.dmxapi.cn'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">DMXAPI</button>
54
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'PackyCode'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.packyapi.com'; newClaudeConfig.model = 'glm-4.7'; showClaudeConfigModal = true">PackyCode</button>
55
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AnyRouter'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://anyrouter.top'; newClaudeConfig.model = 'claude-opus-4-7[1m]'; showClaudeConfigModal = true">AnyRouter</button>
56
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi MiMo'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showClaudeConfigModal = true">Xiaomi MiMo</button>
57
- <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi Token Plan'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://token-plan-cn.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showClaudeConfigModal = true">Xiaomi Token Plan</button>
58
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Claude Official</button>
59
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">DeepSeek</button>
60
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Zhipu GLM</button>
61
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Z.ai GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.z.ai/api/anthropic'; newClaudeConfig.model = 'glm-5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Z.ai GLM</button>
62
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Qwen Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://coding.dashscope.aliyuncs.com/apps/anthropic'; newClaudeConfig.model = 'qwen3-coder'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Qwen Coder</button>
63
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi k2'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.moonshot.cn/anthropic'; newClaudeConfig.model = 'kimi-k2.5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Kimi k2</button>
64
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Kimi For Coding'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.kimi.com/coding/'; newClaudeConfig.model = 'kimi-k2.5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Kimi For Coding</button>
65
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'KAT-Coder'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://vanchin.streamlake.ai/api/gateway/v1/endpoints/${ENDPOINT_ID}/claude-code-proxy'; newClaudeConfig.model = 'KAT-Coder-Pro V1'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">KAT-Coder</button>
66
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Longcat'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.longcat.chat/anthropic'; newClaudeConfig.model = 'LongCat-Flash-Chat'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Longcat</button>
67
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimaxi.com/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">MiniMax</button>
68
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'MiniMax en'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.minimax.io/anthropic'; newClaudeConfig.model = 'MiniMax-M2.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">MiniMax en</button>
69
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DouBaoSeed'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://ark.cn-beijing.volces.com/api/coding'; newClaudeConfig.model = 'doubao-seed-2-0-code-preview-latest'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">DouBaoSeed</button>
70
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'BaiLing'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.tbox.cn/api/anthropic'; newClaudeConfig.model = 'Ling-2.5-1T'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">BaiLing</button>
71
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'ModelScope'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api-inference.modelscope.cn'; newClaudeConfig.model = 'ZhipuAI/GLM-5'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">ModelScope</button>
72
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AiHubMix'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://aihubmix.com'; newClaudeConfig.model = 'glm-4.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">AiHubMix</button>
73
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DMXAPI'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.dmxapi.cn'; newClaudeConfig.model = 'glm-4.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">DMXAPI</button>
74
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'PackyCode'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://www.packyapi.com'; newClaudeConfig.model = 'glm-4.7'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">PackyCode</button>
75
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'AnyRouter'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://anyrouter.top'; newClaudeConfig.model = 'claude-opus-4-7[1m]'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">AnyRouter</button>
76
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi MiMo'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Xiaomi MiMo</button>
77
+ <button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Xiaomi Token Plan'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://token-plan-cn.xiaomimimo.com/anthropic'; newClaudeConfig.model = 'mimo-v2.5-pro'; showAddClaudeConfigKey = false; showClaudeConfigModal = true">Xiaomi Token Plan</button>
58
78
  </div>
59
79
  </div>
60
80
 
@@ -153,5 +173,15 @@
153
173
  </div>
154
174
  </div>
155
175
 
176
+ </div>
177
+ <div v-if="!isToolConfigWriteAllowed('claude')" class="tool-config-write-overlay">
178
+ <div class="tool-config-write-overlay-card">
179
+ <div class="tool-config-write-overlay-title">{{ t('toolConfig.claude.lockedTitle') }}</div>
180
+ <p>{{ t('toolConfig.claude.lockedDesc') }}</p>
181
+ <button type="button" class="btn-tool" @click="setToolConfigPermission('claude', true)" :disabled="toolConfigPermissionSaving.claude">{{ t('toolConfig.enableWrite') }}</button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+
156
186
  </template>
157
187
  </div>
@@ -26,7 +26,27 @@
26
26
  </div>
27
27
  </template>
28
28
  <template v-else>
29
- <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
29
+ <section class="tool-config-write-card" :aria-label="t('toolConfig.codex.title')">
30
+ <div class="tool-config-write-copy">
31
+ <div class="tool-config-write-title">{{ t('toolConfig.codex.title') }}</div>
32
+ <p class="tool-config-write-desc">{{ t('toolConfig.codex.desc') }}</p>
33
+ </div>
34
+ <label class="settings-toggle-row tool-config-write-toggle">
35
+ <input
36
+ type="checkbox"
37
+ :checked="isToolConfigWriteAllowed('codex')"
38
+ :disabled="toolConfigPermissionSaving.codex"
39
+ @change="setToolConfigPermission('codex', $event.target.checked)">
40
+ <span class="toggle-track">
41
+ <span class="toggle-thumb"></span>
42
+ </span>
43
+ <span>{{ toolConfigPermissionStatusLabel('codex') }}</span>
44
+ </label>
45
+ </section>
46
+
47
+ <div class="tool-config-write-scope" :class="{ locked: !isToolConfigWriteAllowed('codex') }">
48
+ <div class="tool-config-write-body">
49
+ <button class="btn-add" @click="showAddProviderKey = false; showAddModal = true" v-if="!loading && !initError" :disabled="!isToolConfigWriteAllowed('codex')">
30
50
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 4v12M4 10h12"/></svg>
31
51
  {{ t('config.addProvider') }}
32
52
  </button>
@@ -36,7 +56,7 @@
36
56
  <span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
37
57
  </div>
38
58
  <div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
39
- <button v-for="tpl in codexProviderTemplates" :key="tpl.name" type="button" class="btn-mini" @click="newProvider.name = tpl.name; newProvider.url = tpl.url; newProvider._suggestedModel = tpl.model || ''; newProvider.useTransform = !!tpl.useTransform; showAddModal = true">{{ tpl.label }}</button>
59
+ <button v-for="tpl in codexProviderTemplates" :key="tpl.name" type="button" class="btn-mini" @click="newProvider.name = tpl.name; newProvider.url = tpl.url; newProvider.model = tpl.model || ''; newProvider.useTransform = !!tpl.useTransform; showAddProviderKey = false; showAddModal = true">{{ tpl.label }}</button>
40
60
  </div>
41
61
  </div>
42
62
 
@@ -122,11 +142,11 @@
122
142
  <div v-if="!loading && !initError" class="card-list">
123
143
  <div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" @click="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.enter.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.space.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
124
144
  <div class="card-leading">
125
- <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
145
+ <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" :title="t('config.transformProvider.title')"></span></div>
126
146
  <div class="card-content">
127
147
  <div v-if="provider.name === 'local'" class="bridge-pool-summary">
128
148
  <svg class="bridge-pool-summary-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><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>
129
- <span class="bridge-pool-summary-text">已启用 {{ localBridgeCandidateProviders().filter(cp => !isLocalBridgeExcluded(cp.name)).length }} / {{ localBridgeCandidateProviders().length }}</span>
149
+ <span class="bridge-pool-summary-text">{{ t('config.localBridge.enabledCount', { enabled: localBridgeCandidateProviders().filter(cp => !isLocalBridgeExcluded(cp.name)).length, total: localBridgeCandidateProviders().length }) }}</span>
130
150
  </div>
131
151
  <div class="card-title">
132
152
  <span>{{ provider.name }}</span>
@@ -140,7 +160,7 @@
140
160
  <span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">{{ formatLatency(speedResults[provider.name]) }}</span>
141
161
  <span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">{{ providerPillText(provider) }}</span>
142
162
  <div class="card-actions" @click.stop>
143
- <button v-if="provider.name === 'local'" class="card-action-btn bridge-pool-trigger" @click="showCodexBridgePoolModal = true" :aria-label="'轮询池设置'" :title="'轮询池设置'">
163
+ <button v-if="provider.name === 'local'" class="card-action-btn bridge-pool-trigger" @click="showCodexBridgePoolModal = true" :aria-label="t('config.localBridge.poolSettings')" :title="t('config.localBridge.poolSettings')">
144
164
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>
145
165
  </button>
146
166
  <button class="card-action-btn" :class="{ loading: speedLoading[provider.name] }" :disabled="!!speedLoading[provider.name]" @click="runSpeedTest(provider.name, { silent: true })" :aria-label="t('config.availabilityTestAria', { name: provider.name })" :title="t('config.availabilityTest')">
@@ -172,6 +192,15 @@
172
192
  </div>
173
193
  </div>
174
194
  </div>
195
+ </div>
196
+ </div>
197
+ <div v-if="!isToolConfigWriteAllowed('codex')" class="tool-config-write-overlay">
198
+ <div class="tool-config-write-overlay-card">
199
+ <div class="tool-config-write-overlay-title">{{ t('toolConfig.codex.lockedTitle') }}</div>
200
+ <p>{{ t('toolConfig.codex.lockedDesc') }}</p>
201
+ <button type="button" class="btn-tool" @click="setToolConfigPermission('codex', true)" :disabled="toolConfigPermissionSaving.codex">{{ t('toolConfig.enableWrite') }}</button>
202
+ </div>
203
+ </div>
175
204
  </div>
176
205
  </template>
177
206
  </div>
@@ -10,74 +10,80 @@
10
10
  <button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
11
  <button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
12
  </div>
13
- <div class="config-template-hint">
14
- {{ t('openclaw.applyHint') }}
15
- </div>
16
-
17
- <div class="selector-section">
18
- <div class="selector-header">
19
- <span class="selector-title">AGENTS.md</span>
20
- </div>
21
- <div class="config-template-hint">
22
- {{ t('openclaw.agents.hint') }}
23
- </div>
24
- <button class="btn-tool" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
25
- {{ agentsLoading ? t('config.modelLoading') : t('openclaw.agents.open') }}
26
- </button>
27
- </div>
28
-
29
- <div class="selector-section">
30
- <div class="selector-header">
31
- <label class="selector-title" for="openclaw-workspace-file">{{ t('openclaw.workspaceFile') }}</label>
32
- </div>
33
- <input
34
- id="openclaw-workspace-file"
35
- class="form-input"
36
- v-model="openclawWorkspaceFileName"
37
- :placeholder="t('openclaw.workspace.placeholder')">
38
- <div class="config-template-hint">
39
- {{ t('openclaw.workspace.hint') }}
40
- </div>
41
- <button class="btn-tool" @click="openOpenclawWorkspaceEditor" :disabled="loading || !!initError || agentsLoading">
42
- {{ agentsLoading ? t('config.modelLoading') : t('openclaw.workspace.open') }}
43
- </button>
44
- </div>
13
+ <div class="openclaw-layout">
14
+ <section class="settings-card settings-card--wide openclaw-workspace-card">
15
+ <div class="settings-card-body">
16
+ <div class="openclaw-tools-grid">
17
+ <button class="openclaw-tool-btn" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
18
+ <div class="tool-icon">📄</div>
19
+ <div class="tool-content">
20
+ <div class="tool-title">AGENTS.md</div>
21
+ <div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : t('openclaw.agents.hint') }}</div>
22
+ </div>
23
+ <svg class="tool-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
24
+ <path d="M9 18l6-6-6-6"/>
25
+ </svg>
26
+ </button>
45
27
 
46
- <div class="card-list">
47
- <div v-for="(config, name) in openclawConfigs" :key="name"
48
- :class="['card', { active: currentOpenclawConfig === name }]"
49
- @click="applyOpenclawConfig(name)"
50
- @keydown.enter.self.prevent="applyOpenclawConfig(name)"
51
- @keydown.space.self.prevent="applyOpenclawConfig(name)"
52
- tabindex="0"
53
- role="button"
54
- :aria-current="currentOpenclawConfig === name ? 'true' : null">
55
- <div class="card-leading">
56
- <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
57
- <div class="card-content">
58
- <div class="card-title">{{ name }}</div>
59
- <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
28
+ <div class="openclaw-workspace-card">
29
+ <label class="workspace-label" for="openclaw-workspace-file">{{ t('openclaw.workspaceFile') }}</label>
30
+ <div class="workspace-input-group">
31
+ <input
32
+ id="openclaw-workspace-file"
33
+ class="form-input"
34
+ v-model="openclawWorkspaceFileName"
35
+ :placeholder="t('openclaw.workspace.placeholder')">
36
+ <button class="btn-tool" @click="openOpenclawWorkspaceEditor" :disabled="loading || !!initError || agentsLoading">
37
+ {{ agentsLoading ? t('config.modelLoading') : t('openclaw.workspace.open') }}
38
+ </button>
39
+ </div>
40
+ <div class="workspace-meta">{{ t('openclaw.workspace.hint') }}</div>
41
+ </div>
60
42
  </div>
61
43
  </div>
62
- <div class="card-trailing">
63
- <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
64
- {{ openclawHasContent(config) ? t('openclaw.configured') : t('openclaw.notConfigured') }}
65
- </span>
66
- <div class="card-actions" @click.stop>
67
- <button class="card-action-btn" @click="openOpenclawEditModal(name)" :aria-label="t('openclaw.action.editAria', { name })" :title="t('openclaw.action.edit')">
68
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
69
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
70
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
71
- </svg>
72
- </button>
73
- <button v-if="name !== '默认配置'" class="card-action-btn delete" @click="deleteOpenclawConfig(name)" :aria-label="t('openclaw.action.deleteAria', { name })" :title="t('openclaw.action.delete')">
74
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
75
- <path d="M3 6h18"/>
76
- <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"/>
77
- </svg>
78
- </button>
44
+ </section>
45
+
46
+ <section class="settings-card settings-card--wide openclaw-configs-card" aria-labelledby="openclaw-configs-title">
47
+ <div class="settings-card-body">
48
+ <div class="card-list openclaw-card-list">
49
+ <div v-for="(config, name) in openclawConfigs" :key="name"
50
+ :class="['card', { active: currentOpenclawConfig === name }]"
51
+ @click="applyOpenclawConfig(name)"
52
+ @keydown.enter.self.prevent="applyOpenclawConfig(name)"
53
+ @keydown.space.self.prevent="applyOpenclawConfig(name)"
54
+ tabindex="0"
55
+ role="button"
56
+ :aria-label="t('openclaw.action.applyAria', { name })"
57
+ :aria-current="currentOpenclawConfig === name ? 'true' : null">
58
+ <div class="card-leading">
59
+ <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
60
+ <div class="card-content">
61
+ <div class="card-title">{{ name }}</div>
62
+ <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
63
+ </div>
64
+ </div>
65
+ <div class="card-trailing">
66
+ <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
67
+ {{ openclawHasContent(config) ? t('openclaw.configured') : t('openclaw.notConfigured') }}
68
+ </span>
69
+ <div class="card-actions" @click.stop>
70
+ <button class="card-action-btn" @click="openOpenclawEditModal(name)" :aria-label="t('openclaw.action.editAria', { name })" :title="t('openclaw.action.edit')">
71
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
72
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
73
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
74
+ </svg>
75
+ </button>
76
+ <button v-if="!isDefaultOpenclawConfig(name, config)" class="card-action-btn delete" @click="deleteOpenclawConfig(name)" :aria-label="t('openclaw.action.deleteAria', { name })" :title="t('openclaw.action.delete')">
77
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
78
+ <path d="M3 6h18"/>
79
+ <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"/>
80
+ </svg>
81
+ </button>
82
+ </div>
83
+ </div>
84
+ </div>
79
85
  </div>
80
86
  </div>
81
- </div>
87
+ </section>
82
88
  </div>
83
89
  </div>