codexmate 0.0.23 → 0.0.25

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 (73) hide show
  1. package/README.md +32 -9
  2. package/README.zh.md +33 -9
  3. package/cli/auth-profiles.js +23 -7
  4. package/cli/builtin-proxy.js +35 -0
  5. package/cli/claude-proxy.js +24 -0
  6. package/cli/doctor-core.js +903 -0
  7. package/cli/import-skills-url.js +356 -0
  8. package/cli/openai-bridge.js +51 -4
  9. package/cli/session-usage.js +8 -2
  10. package/cli.js +1921 -399
  11. package/lib/automation.js +404 -0
  12. package/lib/cli-models-utils.js +0 -40
  13. package/lib/cli-network-utils.js +28 -2
  14. package/lib/cli-path-utils.js +21 -5
  15. package/lib/cli-sessions.js +32 -1
  16. package/lib/download-artifacts.js +17 -2
  17. package/lib/mcp-stdio.js +13 -0
  18. package/package.json +3 -3
  19. package/plugins/README.md +20 -0
  20. package/plugins/README.zh-CN.md +20 -0
  21. package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
  22. package/plugins/prompt-templates/computed.mjs +253 -0
  23. package/plugins/prompt-templates/index.mjs +8 -0
  24. package/plugins/prompt-templates/manifest.mjs +15 -0
  25. package/plugins/prompt-templates/methods.mjs +619 -0
  26. package/plugins/prompt-templates/overview.mjs +90 -0
  27. package/plugins/prompt-templates/ownership.mjs +19 -0
  28. package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
  29. package/plugins/prompt-templates/storage.mjs +64 -0
  30. package/plugins/registry.mjs +16 -0
  31. package/web-ui/app.js +21 -35
  32. package/web-ui/index.html +4 -3
  33. package/web-ui/logic.sessions.mjs +2 -2
  34. package/web-ui/modules/app.computed.dashboard.mjs +24 -22
  35. package/web-ui/modules/app.computed.main-tabs.mjs +3 -0
  36. package/web-ui/modules/app.computed.session.mjs +17 -0
  37. package/web-ui/modules/app.methods.agents.mjs +91 -3
  38. package/web-ui/modules/app.methods.codex-config.mjs +153 -164
  39. package/web-ui/modules/app.methods.install.mjs +28 -0
  40. package/web-ui/modules/app.methods.navigation.mjs +34 -1
  41. package/web-ui/modules/app.methods.runtime.mjs +24 -2
  42. package/web-ui/modules/app.methods.session-actions.mjs +8 -1
  43. package/web-ui/modules/app.methods.session-browser.mjs +37 -6
  44. package/web-ui/modules/app.methods.session-trash.mjs +4 -2
  45. package/web-ui/modules/config-mode.computed.mjs +1 -3
  46. package/web-ui/modules/i18n.dict.mjs +2055 -0
  47. package/web-ui/modules/i18n.mjs +2 -1769
  48. package/web-ui/partials/index/layout-header.html +48 -34
  49. package/web-ui/partials/index/modal-config-template-agents.html +3 -4
  50. package/web-ui/partials/index/modal-health-check.html +33 -60
  51. package/web-ui/partials/index/panel-config-claude.html +35 -15
  52. package/web-ui/partials/index/panel-config-codex.html +47 -19
  53. package/web-ui/partials/index/panel-config-openclaw.html +8 -3
  54. package/web-ui/partials/index/panel-dashboard.html +186 -0
  55. package/web-ui/partials/index/panel-docs.html +1 -1
  56. package/web-ui/partials/index/panel-market.html +3 -0
  57. package/web-ui/partials/index/panel-orchestration.html +3 -0
  58. package/web-ui/partials/index/panel-plugins.html +16 -10
  59. package/web-ui/partials/index/panel-sessions.html +8 -3
  60. package/web-ui/partials/index/panel-settings.html +1 -1
  61. package/web-ui/partials/index/panel-usage.html +9 -1
  62. package/web-ui/res/logo-pack.webp +0 -0
  63. package/web-ui/styles/controls-forms.css +58 -4
  64. package/web-ui/styles/dashboard.css +274 -0
  65. package/web-ui/styles/layout-shell.css +3 -2
  66. package/web-ui/styles/responsive.css +0 -2
  67. package/web-ui/styles/sessions-list.css +5 -7
  68. package/web-ui/styles/sessions-toolbar-trash.css +4 -4
  69. package/web-ui/styles/sessions-usage.css +33 -0
  70. package/web-ui/styles.css +1 -0
  71. package/res/logo.png +0 -0
  72. /package/{res → web-ui/res}/json5.min.js +0 -0
  73. /package/{res → web-ui/res}/vue.global.prod.js +0 -0
@@ -1,5 +1,15 @@
1
1
  <div id="app" class="container" v-cloak>
2
- <div v-if="!sessionStandalone" class="top-tabs" role="tablist" aria-label="Navigation">
2
+ <div v-if="!sessionStandalone" class="top-tabs" role="tablist" :aria-label="t('nav.topTabs.aria')">
3
+ <button class="top-tab"
4
+ id="tab-dashboard"
5
+ role="tab"
6
+ data-main-tab="dashboard"
7
+ :tabindex="mainTab === 'dashboard' ? 0 : -1"
8
+ :aria-selected="mainTab === 'dashboard'"
9
+ aria-controls="panel-dashboard"
10
+ :class="{ active: isMainTabNavActive('dashboard') }"
11
+ @pointerdown="onMainTabPointerDown('dashboard', $event)"
12
+ @click="onMainTabClick('dashboard', $event)">{{ t('tab.dashboard') }}</button>
3
13
  <button class="top-tab"
4
14
  id="tab-docs"
5
15
  role="tab"
@@ -11,38 +21,16 @@
11
21
  @pointerdown="onMainTabPointerDown('docs', $event)"
12
22
  @click="onMainTabClick('docs', $event)">{{ t('tab.docs') }}</button>
13
23
  <button class="top-tab"
14
- id="tab-config-codex"
24
+ id="tab-config"
15
25
  role="tab"
16
26
  data-main-tab="config"
17
- data-config-mode="codex"
18
- :tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
19
- :aria-selected="mainTab === 'config' && configMode === 'codex'"
20
- aria-controls="panel-config-provider"
21
- :class="{ active: isConfigModeNavActive('codex') }"
22
- @pointerdown="onConfigTabPointerDown('codex', $event)"
23
- @click="onConfigTabClick('codex', $event)">{{ t('tab.config.codex') }}</button>
24
- <button class="top-tab"
25
- id="tab-config-claude"
26
- role="tab"
27
- data-main-tab="config"
28
- data-config-mode="claude"
29
- :tabindex="mainTab === 'config' && configMode === 'claude' ? 0 : -1"
30
- :aria-selected="mainTab === 'config' && configMode === 'claude'"
31
- aria-controls="panel-config-claude"
32
- :class="{ active: isConfigModeNavActive('claude') }"
33
- @pointerdown="onConfigTabPointerDown('claude', $event)"
34
- @click="onConfigTabClick('claude', $event)">{{ t('tab.config.claude') }}</button>
35
- <button class="top-tab"
36
- id="tab-config-openclaw"
37
- role="tab"
38
- data-main-tab="config"
39
- data-config-mode="openclaw"
40
- :tabindex="mainTab === 'config' && configMode === 'openclaw' ? 0 : -1"
41
- :aria-selected="mainTab === 'config' && configMode === 'openclaw'"
42
- aria-controls="panel-config-openclaw"
43
- :class="{ active: isConfigModeNavActive('openclaw') }"
44
- @pointerdown="onConfigTabPointerDown('openclaw', $event)"
45
- @click="onConfigTabClick('openclaw', $event)">{{ t('tab.config.openclaw') }}</button>
27
+ :data-config-mode="configMode"
28
+ :tabindex="mainTab === 'config' ? 0 : -1"
29
+ :aria-selected="mainTab === 'config'"
30
+ :aria-controls="configMode === 'claude' ? 'panel-config-claude' : (configMode === 'openclaw' ? 'panel-config-openclaw' : 'panel-config-provider')"
31
+ :class="{ active: isMainTabNavActive('config') }"
32
+ @pointerdown="onMainTabPointerDown('config', $event)"
33
+ @click="onMainTabClick('config', $event)">{{ t('tab.config') }}</button>
46
34
  <button class="top-tab"
47
35
  id="tab-sessions"
48
36
  role="tab"
@@ -126,7 +114,7 @@
126
114
  <aside class="side-rail" v-if="!sessionStandalone">
127
115
  <div class="brand-block">
128
116
  <div class="brand-head">
129
- <img class="brand-logo" src="/res/logo.png" alt="Codex Mate logo">
117
+ <img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
130
118
  <div class="brand-copy">
131
119
  <div class="brand-kicker">{{ t('brand.kicker.workspace') }}</div>
132
120
  <div class="brand-title">Codex Mate</div>
@@ -136,6 +124,22 @@
136
124
  </div>
137
125
 
138
126
  <div class="side-rail-nav">
127
+ <div class="side-section" role="navigation" :aria-label="t('side.overview')">
128
+ <div class="side-section-title">{{ t('side.overview') }}</div>
129
+ <button
130
+ id="side-tab-dashboard"
131
+ data-main-tab="dashboard"
132
+ :aria-current="mainTab === 'dashboard' ? 'page' : null"
133
+ :class="['side-item', { active: isMainTabNavActive('dashboard') }]"
134
+ @pointerdown="onMainTabPointerDown('dashboard', $event)"
135
+ @click="onMainTabClick('dashboard', $event)">
136
+ <div class="side-item-title">{{ t('side.overview.doctor') }}</div>
137
+ <div class="side-item-meta">
138
+ <span>{{ t('side.overview.doctor.meta') }}</span>
139
+ <span>{{ inspectorHealthStatus }}</span>
140
+ </div>
141
+ </button>
142
+ </div>
139
143
  <div class="side-section" role="navigation" :aria-label="t('side.docs')">
140
144
  <div class="side-section-title">{{ t('side.docs') }}</div>
141
145
  <button
@@ -210,7 +214,7 @@
210
214
  <div class="side-item-title">{{ t('side.sessions.browser') }}</div>
211
215
  <div class="side-item-meta">
212
216
  <span>{{ t('side.sessions.browser.meta') }}</span>
213
- <span>{{ t('sessions.sourceLabel', { value: (sessionFilterSource === 'all' ? t('sessions.source.all') : (sessionFilterSource === 'codex' ? 'Codex' : 'Claude')) }) }}</span>
217
+ <span>{{ t('sessions.sourceLabel', { value: (sessionFilterSource === 'all' ? t('sessions.source.all') : (sessionFilterSource === 'claude' ? 'Claude Code' : (sessionFilterSource === 'gemini' ? 'Gemini CLI' : (sessionFilterSource === 'codebuddy' ? 'CodeBuddy Code' : 'Codex')))) }) }}</span>
214
218
  </div>
215
219
  </button>
216
220
  <button
@@ -365,7 +369,17 @@
365
369
  <div class="status-chip">
366
370
  <span class="label">{{ t('status.currentSource') }}</span>
367
371
  <span class="value">
368
- {{ sessionFilterSource === 'all' ? t('sessions.source.all') : (sessionFilterSource === 'claude' ? 'Claude Code' : 'Codex') }}
372
+ {{ sessionFilterSource === 'all'
373
+ ? t('sessions.source.all')
374
+ : (sessionFilterSource === 'codex'
375
+ ? t('sessions.source.codex')
376
+ : (sessionFilterSource === 'claude'
377
+ ? t('sessions.source.claudeCode')
378
+ : (sessionFilterSource === 'gemini'
379
+ ? t('sessions.source.gemini')
380
+ : (sessionFilterSource === 'codebuddy'
381
+ ? t('sessions.source.codebuddy')
382
+ : t('sessions.source.codex'))))) }}
369
383
  </span>
370
384
  </div>
371
385
  <div class="status-chip">
@@ -107,7 +107,7 @@
107
107
 
108
108
 
109
109
  <div class="form-group">
110
- <label class="form-label">{{ t('modal.agents.contentLabel') }}</label>
110
+ <label class="form-label">{{ t(agentsContext === 'claude-md' ? 'modal.agents.contentLabel.claudeMd' : 'modal.agents.contentLabel') }}</label>
111
111
  <div
112
112
  v-if="!agentsLoading && (hasAgentsContentChanged() || agentsDiffVisible)"
113
113
  class="agents-diff-save-alert">
@@ -137,13 +137,12 @@
137
137
  </div>
138
138
  </div>
139
139
  <textarea
140
- v-else
141
140
  v-model="agentsContent"
142
141
  class="form-input template-editor"
143
142
  spellcheck="false"
144
- :readonly="agentsLoading || agentsSaving"
143
+ :readonly="agentsLoading || agentsSaving || agentsDiffVisible"
145
144
  @input="onAgentsContentInput"
146
- :placeholder="t('modal.agents.placeholder')"></textarea>
145
+ :placeholder="t(agentsContext === 'claude-md' ? 'modal.agents.placeholder.claudeMd' : 'modal.agents.placeholder')"></textarea>
147
146
  <div class="template-editor-warning">
148
147
  {{ agentsModalHint }}
149
148
  <div class="agents-diff-hint">{{ t('modal.agents.hint.shortcuts') }}</div>
@@ -1,72 +1,45 @@
1
- <div v-if="showHealthCheckDialog" class="modal-overlay" @click.self="closeHealthCheckDialog()">
2
- <div class="modal modal-wide health-check-dialog" role="dialog" aria-modal="true" aria-labelledby="health-check-dialog-title">
3
- <div class="modal-header">
4
- <div>
5
- <div class="modal-title" id="health-check-dialog-title">{{ t('modal.healthCheck.title') }}</div>
6
- <div class="health-check-dialog-subtitle">{{ t('modal.healthCheck.subtitle') }}</div>
7
- </div>
8
- <div v-if="healthCheckDialogLockedProvider" class="health-check-dialog-lock">
9
- {{ t('modal.healthCheck.lockedPrefix', { value: healthCheckDialogLockedProvider }) }}
10
- </div>
11
- </div>
1
+ <div v-if="showHealthCheckModal" class="modal-overlay" @click.self="showHealthCheckModal = false">
2
+ <div class="modal" role="dialog" aria-modal="true" aria-labelledby="health-check-modal-title">
3
+ <div class="modal-title" id="health-check-modal-title">{{ t('config.health.title') }}</div>
12
4
 
13
- <div class="health-check-dialog-controls">
14
- <div class="form-group health-check-dialog-provider">
15
- <label class="form-label">{{ t('field.provider') }}</label>
16
- <select
17
- v-model="healthCheckDialogSelectedProvider"
18
- class="form-select"
19
- :disabled="!!healthCheckDialogLockedProvider || healthCheckDialogSending">
20
- <option value="" disabled>{{ t('placeholder.selectProvider') }}</option>
21
- <option v-for="provider in displayProvidersList" :key="'health-check-provider-' + provider.name" :value="provider.name">
22
- {{ provider.name }}
23
- </option>
24
- </select>
5
+ <div v-if="!healthCheckResult" class="state-message">{{ t('common.notLoaded') }}</div>
6
+ <template v-else>
7
+ <div class="form-hint">
8
+ {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }}
9
+ <span v-if="healthCheckResult.issues">({{ t('config.health.issues', { count: healthCheckResult.issues.length }) }})</span>
25
10
  </div>
26
- <div v-if="healthCheckDialogLastResult" class="health-check-dialog-result" :class="healthCheckDialogLastResult.ok ? 'ok' : 'error'">
27
- <span>{{ healthCheckDialogLastResult.ok ? t('modal.healthCheck.result.ok') : t('modal.healthCheck.result.fail') }}</span>
28
- <span v-if="healthCheckDialogLastResult.model">{{ t('label.model') }}{{ healthCheckDialogLastResult.model }}</span>
29
- <span v-if="healthCheckDialogLastResult.status">HTTP {{ healthCheckDialogLastResult.status }}</span>
30
- <span v-if="healthCheckDialogLastResult.durationMs">{{ healthCheckDialogLastResult.durationMs }} ms</span>
31
- </div>
32
- </div>
33
11
 
34
- <div class="health-check-dialog-thread">
35
- <div v-if="healthCheckDialogMessages.length === 0" class="health-check-dialog-empty">
36
- {{ t('modal.healthCheck.emptyHint') }}
12
+ <div v-if="healthCheckResult.remote && healthCheckResult.remote.type === 'remote-health-check'" class="form-hint">
13
+ {{ healthCheckResult.remote.endpoint || '' }}
14
+ <span v-if="healthCheckResult.remote.statusCode"> · {{ healthCheckResult.remote.statusCode }}</span>
15
+ <span v-if="healthCheckResult.remote.message"> · {{ healthCheckResult.remote.message }}</span>
37
16
  </div>
38
- <div
39
- v-for="item in healthCheckDialogMessages"
40
- :key="item.id"
41
- :class="['health-check-message', item.role, item.ok === false ? 'error' : '']">
42
- <div class="health-check-message-meta">
43
- <span>{{ item.role === 'user' ? t('role.you') : t('role.provider') }}</span>
44
- <span v-if="item.model">{{ item.model }}</span>
45
- <span v-if="item.status">HTTP {{ item.status }}</span>
46
- <span v-if="item.durationMs">{{ item.durationMs }} ms</span>
17
+
18
+ <div v-if="healthCheckResult.remote && healthCheckResult.remote.speedTests" class="model-list">
19
+ <div
20
+ v-for="(result, name) in healthCheckResult.remote.speedTests"
21
+ :key="'health-speed-' + name"
22
+ class="model-item"
23
+ >
24
+ <span>{{ name }}</span>
25
+ <span v-if="result && result.ok" class="latency ok">{{ formatLatency(result) }}</span>
26
+ <span v-else class="latency error">{{ (result && result.error) ? result.error : t('config.health.fail') }}</span>
47
27
  </div>
48
- <div class="health-check-message-text">{{ item.text }}</div>
49
- <pre v-if="item.rawPreview" class="health-check-message-raw">{{ item.rawPreview }}</pre>
50
28
  </div>
51
- </div>
52
29
 
53
- <div class="form-group">
54
- <label class="form-label">{{ t('field.message') }}</label>
55
- <textarea
56
- v-model="healthCheckDialogPrompt"
57
- class="form-input health-check-dialog-textarea"
58
- rows="4"
59
- :disabled="healthCheckDialogSending"
60
- :placeholder="t('placeholder.healthCheckPromptExample')"
61
- @keydown.ctrl.enter.prevent="sendHealthCheckDialogMessage"></textarea>
62
- <div class="form-hint">{{ t('modal.healthCheck.realApiHint') }}</div>
63
- </div>
30
+ <div v-if="healthCheckResult.issues && healthCheckResult.issues.length" class="model-list">
31
+ <div
32
+ v-for="(issue, index) in healthCheckResult.issues"
33
+ :key="issue.code || ('health-issue-' + index)"
34
+ class="model-item"
35
+ >
36
+ <span>{{ issue.message || issue.code || '' }}</span>
37
+ </div>
38
+ </div>
39
+ </template>
64
40
 
65
41
  <div class="btn-group">
66
- <button class="btn btn-cancel" @click="closeHealthCheckDialog()" :disabled="healthCheckDialogSending">{{ t('common.close') }}</button>
67
- <button class="btn btn-confirm" @click="sendHealthCheckDialogMessage" :disabled="healthCheckDialogSending">
68
- {{ healthCheckDialogSending ? t('common.sending') : t('modal.healthCheck.send') }}
69
- </button>
42
+ <button class="btn btn-confirm" @click="showHealthCheckModal = false">{{ t('common.close') }}</button>
70
43
  </div>
71
44
  </div>
72
45
  </div>
@@ -4,7 +4,12 @@
4
4
  class="mode-content mode-cards"
5
5
  id="panel-config-claude"
6
6
  role="tabpanel"
7
- :aria-labelledby="'tab-config-claude'">
7
+ :aria-labelledby="forceCompactLayout ? 'tab-config' : 'side-tab-config-claude'">
8
+ <div v-if="forceCompactLayout && !sessionStandalone" class="segmented-control">
9
+ <button type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
10
+ <button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
+ <button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
+ </div>
8
13
  <template v-if="shouldShowCliInstallPlaceholder('claude')">
9
14
  <div class="selector-section">
10
15
  <div class="empty-state">
@@ -89,22 +94,37 @@
89
94
 
90
95
  <div class="selector-section">
91
96
  <div class="selector-header">
92
- <span class="selector-title">{{ t('claude.health.title') }}</span>
97
+ <span class="selector-title">CLAUDE.md</span>
93
98
  </div>
94
- <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
95
- {{ healthCheckLoading ? t('claude.health.running') : t('claude.health.run') }}
99
+ <button class="btn-tool" @click="openClaudeMdEditor" :disabled="loading || !!initError || agentsLoading">
100
+ {{ agentsLoading ? t('config.modelLoading') : t('claude.md.open') }}
96
101
  </button>
102
+ <div class="config-template-hint">
103
+ {{ t('claude.md.hint') }}
104
+ </div>
97
105
  </div>
98
106
 
99
107
  <div class="selector-section">
100
108
  <div class="selector-header">
101
- <span class="selector-title">CLAUDE.md</span>
109
+ <span class="selector-title">{{ t('claude.health.title') }}</span>
102
110
  </div>
103
- <button class="btn-tool" @click="openClaudeMdEditor" :disabled="loading || !!initError || agentsLoading">
104
- {{ agentsLoading ? t('config.modelLoading') : t('claude.md.open') }}
111
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
112
+ {{ healthCheckLoading ? t('claude.health.running') : t('claude.health.run') }}
105
113
  </button>
106
- <div class="config-template-hint">
107
- {{ t('claude.md.hint') }}
114
+ <div class="config-template-hint">{{ t('claude.health.hint') }}</div>
115
+ <div v-if="healthCheckLoading && healthCheckBatchTotal" class="config-template-hint">
116
+ {{ t('claude.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }}
117
+ </div>
118
+ <div v-if="healthCheckResult && !healthCheckLoading" class="config-template-hint">
119
+ {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }}
120
+ </div>
121
+ <button v-if="healthCheckResult && !healthCheckLoading" type="button" class="btn-mini" @click="showHealthCheckModal = true">
122
+ {{ t('common.detail') }}
123
+ </button>
124
+ <div v-if="healthCheckResult && !healthCheckLoading && (healthCheckResult.issues || []).length">
125
+ <div v-for="(issue, index) in healthCheckResult.issues" :key="issue.code || ('issue-' + index)" class="config-template-hint">
126
+ {{ issue.message || issue.code || '' }}<span v-if="issue.suggestion"> · {{ issue.suggestion }}</span>
127
+ </div>
108
128
  </div>
109
129
  </div>
110
130
 
@@ -125,27 +145,27 @@
125
145
  </div>
126
146
  </div>
127
147
  <div class="card-trailing">
128
- <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
129
- {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}
130
- </span>
131
148
  <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">
132
149
  {{ formatLatency(claudeSpeedResults[name]) }}
133
150
  </span>
151
+ <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
152
+ {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}
153
+ </span>
134
154
  <div class="card-actions" @click.stop>
135
- <button class="card-action-btn" @click="openEditConfigModal(name)" :aria-label="`Edit Claude config ${name}`" :title="t('claude.action.edit')">
155
+ <button class="card-action-btn" @click="openEditConfigModal(name)" :aria-label="t('claude.action.editAria', { name })" :title="t('claude.action.edit')">
136
156
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
137
157
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
138
158
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
139
159
  </svg>
140
160
  </button>
141
- <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" aria-label="Share import command">
161
+ <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" :aria-label="t('config.shareCommand.aria')">
142
162
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
143
163
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
144
164
  <path d="M16 6l-4-4-4 4"/>
145
165
  <path d="M12 2v14"/>
146
166
  </svg>
147
167
  </button>
148
- <button class="card-action-btn delete" @click="deleteClaudeConfig(name)" :aria-label="`Delete Claude config ${name}`" :title="t('claude.action.delete')">
168
+ <button class="card-action-btn delete" @click="deleteClaudeConfig(name)" :aria-label="t('claude.action.deleteAria', { name })" :title="t('claude.action.delete')">
149
169
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
150
170
  <path d="M3 6h18"/>
151
171
  <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"/>
@@ -4,7 +4,12 @@
4
4
  class="mode-content mode-cards"
5
5
  id="panel-config-provider"
6
6
  role="tabpanel"
7
- :aria-labelledby="'tab-config-' + configMode">
7
+ :aria-labelledby="forceCompactLayout ? 'tab-config' : ('side-tab-config-' + configMode)">
8
+ <div v-if="forceCompactLayout && !sessionStandalone" class="segmented-control">
9
+ <button type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
10
+ <button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
+ <button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
+ </div>
8
13
  <template v-if="isCodexConfigMode && shouldShowCliInstallPlaceholder('codex')">
9
14
  <div class="selector-section">
10
15
  <div class="empty-state">
@@ -38,8 +43,8 @@
38
43
  <div class="selector-header">
39
44
  <span class="selector-title">{{ t('config.models') }}</span>
40
45
  <div class="selector-actions">
41
- <button class="btn-icon" @click="showModelModal = true" aria-label="Add model" title="添加模型" v-if="modelsSource === 'legacy'">+</button>
42
- <button class="btn-icon" @click="showModelListModal = true" aria-label="Manage models" title="管理模型" v-if="modelsSource === 'legacy'">≡</button>
46
+ <button class="btn-icon" @click="showModelModal = true" :aria-label="t('modal.modelAdd.title')" :title="t('modal.modelAdd.title')" v-if="modelsSource === 'legacy'">+</button>
47
+ <button class="btn-icon" @click="showModelListModal = true" :aria-label="t('modal.modelManage.title')" :title="t('modal.modelManage.title')" v-if="modelsSource === 'legacy'">≡</button>
43
48
  </div>
44
49
  </div>
45
50
  <select
@@ -86,7 +91,7 @@
86
91
  </div>
87
92
  <select class="model-select" v-model="serviceTier" @change="onServiceTierChange">
88
93
  <option value="fast">{{ t('config.serviceTier.fast') }}</option>
89
- <option value="standard">standard</option>
94
+ <option value="standard">{{ t('config.serviceTier.standard') }}</option>
90
95
  </select>
91
96
  <div class="config-template-hint">
92
97
  {{ t('config.serviceTier.hint', { field: 'service_tier' }) }}
@@ -163,6 +168,30 @@
163
168
  </button>
164
169
  </div>
165
170
 
171
+ <div class="selector-section">
172
+ <div class="selector-header">
173
+ <span class="selector-title">{{ t('config.health.title') }}</span>
174
+ </div>
175
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
176
+ {{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}
177
+ </button>
178
+ <div class="config-template-hint">{{ t('config.health.hint') }}</div>
179
+ <div v-if="healthCheckLoading && healthCheckBatchTotal" class="config-template-hint">
180
+ {{ t('config.health.progress', { done: healthCheckBatchDone, total: healthCheckBatchTotal, failed: healthCheckBatchFailed }) }}
181
+ </div>
182
+ <div v-if="healthCheckResult && !healthCheckLoading" class="config-template-hint">
183
+ {{ healthCheckResult.ok ? t('config.health.ok') : t('config.health.fail') }} · {{ t('config.health.issues', { count: (healthCheckResult.issues || []).length }) }}
184
+ </div>
185
+ <button v-if="healthCheckResult && !healthCheckLoading" type="button" class="btn-mini" @click="showHealthCheckModal = true">
186
+ {{ t('common.detail') }}
187
+ </button>
188
+ <div v-if="healthCheckResult && !healthCheckLoading && (healthCheckResult.issues || []).length">
189
+ <div v-for="(issue, index) in healthCheckResult.issues" :key="issue.code || ('issue-' + index)" class="config-template-hint">
190
+ {{ issue.message || issue.code || '' }}<span v-if="issue.suggestion"> · {{ issue.suggestion }}</span>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
166
195
  </template>
167
196
 
168
197
  <div v-if="!loading && !initError" class="card-list">
@@ -187,24 +216,23 @@
187
216
  </div>
188
217
  </div>
189
218
  <div class="card-trailing">
190
- <span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">
191
- {{ providerPillText(provider) }}
192
- </span>
193
219
  <span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">
194
220
  {{ formatLatency(speedResults[provider.name]) }}
195
221
  </span>
222
+ <span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">
223
+ {{ providerPillText(provider) }}
224
+ </span>
196
225
  <div class="card-actions" @click.stop>
197
226
  <button
198
227
  class="card-action-btn"
199
- @click="openHealthCheckDialog({ providerName: provider.name, locked: true })"
200
- :disabled="displayCurrentProvider !== provider.name"
201
- :aria-label="`Open health dialog for ${provider.name}`"
202
- :title="displayCurrentProvider === provider.name ? t('config.healthTest') : t('config.switchProviderFirst')"
228
+ :class="{ loading: speedLoading[provider.name] }"
229
+ :disabled="!!speedLoading[provider.name]"
230
+ @click="runSpeedTest(provider.name, { silent: true })"
231
+ :aria-label="t('config.availabilityTestAria', { name: provider.name })"
232
+ :title="t('config.availabilityTest')"
203
233
  >
204
234
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
205
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
206
- <path d="M8 9h8"/>
207
- <path d="M8 13h5"/>
235
+ <path d="M13 2L3 14h7l-1 8 12-14h-7l1-6z"/>
208
236
  </svg>
209
237
  </button>
210
238
  <button
@@ -214,7 +242,7 @@
214
242
  :disabled="providerShareLoading[provider.name] || !shouldAllowProviderShare(provider)"
215
243
  @click="copyProviderShareCommand(provider)"
216
244
  :title="shouldAllowProviderShare(provider) ? t('config.shareCommand') : t('config.shareDisabled')"
217
- aria-label="Share import command">
245
+ :aria-label="t('config.shareCommand.aria')">
218
246
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
219
247
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
220
248
  <path d="M16 6l-4-4-4 4"/>
@@ -227,8 +255,8 @@
227
255
  :class="{ disabled: !shouldShowProviderEdit(provider) }"
228
256
  :disabled="!shouldShowProviderEdit(provider)"
229
257
  @click="openEditModal(provider)"
230
- :aria-label="`Edit provider ${provider.name}`"
231
- :title="shouldShowProviderEdit(provider) ? '编辑' : '不可编辑'">
258
+ :aria-label="t('config.provider.edit.aria', { name: provider.name })"
259
+ :title="shouldShowProviderEdit(provider) ? t('common.edit') : t('common.notEditable')">
232
260
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
233
261
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
234
262
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
@@ -240,8 +268,8 @@
240
268
  :class="{ disabled: !shouldShowProviderDelete(provider) }"
241
269
  :disabled="!shouldShowProviderDelete(provider)"
242
270
  @click="deleteProvider(provider.name)"
243
- :aria-label="`Delete provider ${provider.name}`"
244
- :title="shouldShowProviderDelete(provider) ? '删除' : '不可删除'">
271
+ :aria-label="t('config.provider.delete.aria', { name: provider.name })"
272
+ :title="shouldShowProviderDelete(provider) ? t('common.delete') : t('common.notDeletable')">
245
273
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
246
274
  <path d="M3 6h18"/>
247
275
  <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"/>
@@ -4,7 +4,12 @@
4
4
  class="mode-content mode-cards"
5
5
  id="panel-config-openclaw"
6
6
  role="tabpanel"
7
- :aria-labelledby="'tab-config-openclaw'">
7
+ :aria-labelledby="forceCompactLayout ? 'tab-config' : 'side-tab-config-openclaw'">
8
+ <div v-if="forceCompactLayout && !sessionStandalone" class="segmented-control">
9
+ <button type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
10
+ <button type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
+ <button type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
+ </div>
8
13
  <div class="config-template-hint">
9
14
  {{ t('openclaw.applyHint') }}
10
15
  </div>
@@ -59,13 +64,13 @@
59
64
  {{ openclawHasContent(config) ? t('openclaw.configured') : t('openclaw.notConfigured') }}
60
65
  </span>
61
66
  <div class="card-actions" @click.stop>
62
- <button class="card-action-btn" @click="openOpenclawEditModal(name)" :aria-label="`Edit OpenClaw config ${name}`" :title="t('openclaw.action.edit')">
67
+ <button class="card-action-btn" @click="openOpenclawEditModal(name)" :aria-label="t('openclaw.action.editAria', { name })" :title="t('openclaw.action.edit')">
63
68
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
64
69
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
65
70
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
66
71
  </svg>
67
72
  </button>
68
- <button v-if="name !== '默认配置'" class="card-action-btn delete" @click="deleteOpenclawConfig(name)" :aria-label="`Delete OpenClaw config ${name}`" :title="t('openclaw.action.delete')">
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')">
69
74
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
70
75
  <path d="M3 6h18"/>
71
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"/>