codexmate 0.0.22 → 0.0.24

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 (66) hide show
  1. package/README.md +5 -3
  2. package/README.zh.md +8 -5
  3. package/cli/auth-profiles.js +23 -7
  4. package/cli/doctor-core.js +903 -0
  5. package/cli/import-skills-url.js +334 -0
  6. package/cli.js +304 -208
  7. package/lib/cli-models-utils.js +0 -40
  8. package/lib/cli-network-utils.js +28 -2
  9. package/package.json +5 -2
  10. package/plugins/README.md +20 -0
  11. package/plugins/README.zh-CN.md +20 -0
  12. package/plugins/prompt-templates/comment-polish/index.mjs +25 -0
  13. package/plugins/prompt-templates/computed.mjs +253 -0
  14. package/plugins/prompt-templates/index.mjs +8 -0
  15. package/plugins/prompt-templates/manifest.mjs +15 -0
  16. package/plugins/prompt-templates/methods.mjs +619 -0
  17. package/plugins/prompt-templates/overview.mjs +90 -0
  18. package/plugins/prompt-templates/ownership.mjs +19 -0
  19. package/plugins/prompt-templates/rule-ack/index.mjs +21 -0
  20. package/plugins/prompt-templates/storage.mjs +64 -0
  21. package/plugins/registry.mjs +16 -0
  22. package/res/logo-pack.webp +0 -0
  23. package/web-ui/app.js +68 -34
  24. package/web-ui/index.html +4 -3
  25. package/web-ui/modules/app.computed.dashboard.mjs +22 -22
  26. package/web-ui/modules/app.computed.main-tabs.mjs +9 -2
  27. package/web-ui/modules/app.methods.agents.mjs +91 -3
  28. package/web-ui/modules/app.methods.codex-config.mjs +153 -164
  29. package/web-ui/modules/app.methods.install.mjs +16 -0
  30. package/web-ui/modules/app.methods.navigation.mjs +76 -0
  31. package/web-ui/modules/app.methods.runtime.mjs +24 -2
  32. package/web-ui/modules/app.methods.session-browser.mjs +73 -1
  33. package/web-ui/modules/app.methods.startup-claude.mjs +12 -0
  34. package/web-ui/modules/app.methods.task-orchestration.mjs +96 -11
  35. package/web-ui/modules/config-mode.computed.mjs +1 -3
  36. package/web-ui/modules/i18n.dict.mjs +2039 -0
  37. package/web-ui/modules/i18n.mjs +2 -1555
  38. package/web-ui/modules/plugins.computed.mjs +2 -219
  39. package/web-ui/modules/plugins.methods.mjs +2 -619
  40. package/web-ui/modules/plugins.storage.mjs +11 -37
  41. package/web-ui/modules/sessions-filters-url.mjs +85 -0
  42. package/web-ui/partials/index/layout-header.html +38 -34
  43. package/web-ui/partials/index/modal-config-template-agents.html +3 -4
  44. package/web-ui/partials/index/modal-health-check.html +33 -60
  45. package/web-ui/partials/index/panel-config-claude.html +56 -15
  46. package/web-ui/partials/index/panel-config-codex.html +68 -19
  47. package/web-ui/partials/index/panel-config-openclaw.html +8 -3
  48. package/web-ui/partials/index/panel-dashboard.html +186 -0
  49. package/web-ui/partials/index/panel-docs.html +1 -1
  50. package/web-ui/partials/index/panel-market.html +3 -0
  51. package/web-ui/partials/index/panel-orchestration.html +105 -111
  52. package/web-ui/partials/index/panel-plugins.html +48 -12
  53. package/web-ui/partials/index/panel-sessions.html +12 -3
  54. package/web-ui/partials/index/panel-settings.html +1 -1
  55. package/web-ui/partials/index/panel-usage.html +7 -6
  56. package/web-ui/styles/controls-forms.css +16 -2
  57. package/web-ui/styles/dashboard.css +274 -0
  58. package/web-ui/styles/layout-shell.css +11 -5
  59. package/web-ui/styles/navigation-panels.css +8 -0
  60. package/web-ui/styles/plugins-panel.css +5 -0
  61. package/web-ui/styles/sessions-list.css +3 -3
  62. package/web-ui/styles/sessions-usage.css +37 -0
  63. package/web-ui/styles/skills-market.css +12 -2
  64. package/web-ui/styles/task-orchestration.css +57 -11
  65. package/web-ui/styles.css +1 -0
  66. package/res/logo.png +0 -0
@@ -0,0 +1,85 @@
1
+ import {
2
+ buildSessionFilterCacheState,
3
+ isSessionQueryEnabled
4
+ } from '../logic.mjs';
5
+
6
+ export function normalizeSessionRoleFilter(value) {
7
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
8
+ if (normalized === 'user' || normalized === 'assistant' || normalized === 'system') {
9
+ return normalized;
10
+ }
11
+ return 'all';
12
+ }
13
+
14
+ export function normalizeSessionTimePreset(value) {
15
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
16
+ if (normalized === '7d' || normalized === '30d' || normalized === '90d') {
17
+ return normalized;
18
+ }
19
+ return 'all';
20
+ }
21
+
22
+ export function readSessionsFilterUrlState() {
23
+ try {
24
+ const url = new URL(window.location.href);
25
+ if (url.pathname === '/session') return null;
26
+ const source = (url.searchParams.get('s_source') || '').trim().toLowerCase();
27
+ const pathFilter = url.searchParams.get('s_path') || '';
28
+ const query = url.searchParams.get('s_query') || '';
29
+ const roleFilter = url.searchParams.get('s_role') || 'all';
30
+ const timeRangePreset = url.searchParams.get('s_time') || 'all';
31
+ const hasAny = !!(source || pathFilter || query || roleFilter !== 'all' || timeRangePreset !== 'all');
32
+ if (!hasAny) return null;
33
+ return {
34
+ source: source || 'all',
35
+ pathFilter,
36
+ query,
37
+ roleFilter,
38
+ timeRangePreset
39
+ };
40
+ } catch (_) {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ export function applySessionsFilterUrlState(vm, state) {
46
+ if (!vm || !state) return;
47
+ const cached = buildSessionFilterCacheState(state.source, state.pathFilter);
48
+ vm.sessionFilterSource = cached.source;
49
+ vm.sessionPathFilter = cached.pathFilter;
50
+ vm.sessionQuery = typeof state.query === 'string' ? state.query : '';
51
+ vm.sessionRoleFilter = normalizeSessionRoleFilter(state.roleFilter);
52
+ vm.sessionTimePreset = normalizeSessionTimePreset(state.timeRangePreset);
53
+ if (typeof vm.refreshSessionPathOptions === 'function') {
54
+ vm.refreshSessionPathOptions(vm.sessionFilterSource);
55
+ }
56
+ }
57
+
58
+ export function buildSessionsFilterShareUrl(vm) {
59
+ try {
60
+ const url = new URL(window.location.href);
61
+ if (url.pathname === '/session') return '';
62
+ url.searchParams.set('s_source', String(vm.sessionFilterSource || 'all'));
63
+ if (vm.sessionPathFilter) url.searchParams.set('s_path', String(vm.sessionPathFilter || ''));
64
+ else url.searchParams.delete('s_path');
65
+ if (vm.sessionQuery && isSessionQueryEnabled(vm.sessionFilterSource)) url.searchParams.set('s_query', String(vm.sessionQuery || ''));
66
+ else url.searchParams.delete('s_query');
67
+ if (vm.sessionRoleFilter && vm.sessionRoleFilter !== 'all') url.searchParams.set('s_role', String(vm.sessionRoleFilter || 'all'));
68
+ else url.searchParams.delete('s_role');
69
+ if (vm.sessionTimePreset && vm.sessionTimePreset !== 'all') url.searchParams.set('s_time', String(vm.sessionTimePreset || 'all'));
70
+ else url.searchParams.delete('s_time');
71
+ url.searchParams.set('tab', 'sessions');
72
+ return url.toString();
73
+ } catch (_) {
74
+ return '';
75
+ }
76
+ }
77
+
78
+ export function syncSessionsFilterUrl(vm) {
79
+ const url = buildSessionsFilterShareUrl(vm);
80
+ if (!url) return;
81
+ try {
82
+ window.history.replaceState(null, '', url);
83
+ } catch (_) {}
84
+ }
85
+
@@ -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,16 +114,32 @@
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
- <div class="brand-kicker">Workspace</div>
119
+ <div class="brand-kicker">{{ t('brand.kicker.workspace') }}</div>
132
120
  <div class="brand-title">Codex Mate</div>
133
121
  </div>
134
122
  </div>
135
- <div class="brand-subtitle">Local config & sessions workspace</div>
123
+ <div class="brand-subtitle">{{ t('brand.subtitle.localConfigSessionsWorkspace') }}</div>
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
@@ -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,32 @@
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>
13
+ <template v-if="shouldShowCliInstallPlaceholder('claude')">
14
+ <div class="selector-section">
15
+ <div class="empty-state">
16
+ <div class="empty-state-title">{{ t('cli.missing.title', { name: 'Claude' }) }}</div>
17
+ <div class="empty-state-subtitle">{{ t('cli.missing.subtitle', { name: 'Claude' }) }}</div>
18
+ <div class="docs-command-row">
19
+ <div class="docs-command-box" role="group" :aria-label="t('cli.missing.commandAria', { name: 'Claude' })">
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>
26
+ </div>
27
+ </div>
28
+ <button type="button" class="btn-tool btn-tool-compact" @click="mainTab = 'docs'; setInstallCommandAction('install')">{{ t('cli.missing.openDocs') }}</button>
29
+ </div>
30
+ </div>
31
+ </template>
32
+ <template v-else>
8
33
  <!-- 添加提供商按钮 -->
9
34
  <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
10
35
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -69,22 +94,37 @@
69
94
 
70
95
  <div class="selector-section">
71
96
  <div class="selector-header">
72
- <span class="selector-title">{{ t('claude.health.title') }}</span>
97
+ <span class="selector-title">CLAUDE.md</span>
73
98
  </div>
74
- <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
75
- {{ 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') }}
76
101
  </button>
102
+ <div class="config-template-hint">
103
+ {{ t('claude.md.hint') }}
104
+ </div>
77
105
  </div>
78
106
 
79
107
  <div class="selector-section">
80
108
  <div class="selector-header">
81
- <span class="selector-title">CLAUDE.md</span>
109
+ <span class="selector-title">{{ t('claude.health.title') }}</span>
82
110
  </div>
83
- <button class="btn-tool" @click="openClaudeMdEditor" :disabled="loading || !!initError || agentsLoading">
84
- {{ 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') }}
85
113
  </button>
86
- <div class="config-template-hint">
87
- {{ 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>
88
128
  </div>
89
129
  </div>
90
130
 
@@ -105,27 +145,27 @@
105
145
  </div>
106
146
  </div>
107
147
  <div class="card-trailing">
108
- <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
109
- {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}
110
- </span>
111
148
  <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">
112
149
  {{ formatLatency(claudeSpeedResults[name]) }}
113
150
  </span>
151
+ <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
152
+ {{ config.hasKey ? t('claude.configured') : t('claude.notConfigured') }}
153
+ </span>
114
154
  <div class="card-actions" @click.stop>
115
- <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')">
116
156
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
117
157
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
118
158
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
119
159
  </svg>
120
160
  </button>
121
- <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')">
122
162
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
123
163
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
124
164
  <path d="M16 6l-4-4-4 4"/>
125
165
  <path d="M12 2v14"/>
126
166
  </svg>
127
167
  </button>
128
- <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')">
129
169
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
130
170
  <path d="M3 6h18"/>
131
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"/>
@@ -135,4 +175,5 @@
135
175
  </div>
136
176
  </div>
137
177
  </div>
178
+ </template>
138
179
  </div>
@@ -4,7 +4,32 @@
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>
13
+ <template v-if="isCodexConfigMode && shouldShowCliInstallPlaceholder('codex')">
14
+ <div class="selector-section">
15
+ <div class="empty-state">
16
+ <div class="empty-state-title">{{ t('cli.missing.title', { name: 'Codex' }) }}</div>
17
+ <div class="empty-state-subtitle">{{ t('cli.missing.subtitle', { name: 'Codex' }) }}</div>
18
+ <div class="docs-command-row">
19
+ <div class="docs-command-box" role="group" :aria-label="t('cli.missing.commandAria', { name: 'Codex' })">
20
+ <code class="install-command">{{ getInstallCommand('codex', 'install') }}</code>
21
+ <button
22
+ type="button"
23
+ class="btn-mini docs-copy-btn"
24
+ :disabled="!getInstallCommand('codex', 'install')"
25
+ @click="copyInstallCommand(getInstallCommand('codex', 'install'))">{{ t('common.copy') }}</button>
26
+ </div>
27
+ </div>
28
+ <button type="button" class="btn-tool btn-tool-compact" @click="mainTab = 'docs'; setInstallCommandAction('install')">{{ t('cli.missing.openDocs') }}</button>
29
+ </div>
30
+ </div>
31
+ </template>
32
+ <template v-else>
8
33
  <!-- 添加提供商按钮 -->
9
34
  <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
10
35
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -18,8 +43,8 @@
18
43
  <div class="selector-header">
19
44
  <span class="selector-title">{{ t('config.models') }}</span>
20
45
  <div class="selector-actions">
21
- <button class="btn-icon" @click="showModelModal = true" aria-label="Add model" title="添加模型" v-if="modelsSource === 'legacy'">+</button>
22
- <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>
23
48
  </div>
24
49
  </div>
25
50
  <select
@@ -66,7 +91,7 @@
66
91
  </div>
67
92
  <select class="model-select" v-model="serviceTier" @change="onServiceTierChange">
68
93
  <option value="fast">{{ t('config.serviceTier.fast') }}</option>
69
- <option value="standard">standard</option>
94
+ <option value="standard">{{ t('config.serviceTier.standard') }}</option>
70
95
  </select>
71
96
  <div class="config-template-hint">
72
97
  {{ t('config.serviceTier.hint', { field: 'service_tier' }) }}
@@ -143,6 +168,30 @@
143
168
  </button>
144
169
  </div>
145
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
+
146
195
  </template>
147
196
 
148
197
  <div v-if="!loading && !initError" class="card-list">
@@ -167,24 +216,23 @@
167
216
  </div>
168
217
  </div>
169
218
  <div class="card-trailing">
170
- <span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">
171
- {{ providerPillText(provider) }}
172
- </span>
173
219
  <span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">
174
220
  {{ formatLatency(speedResults[provider.name]) }}
175
221
  </span>
222
+ <span :class="['pill', providerPillConfigured(provider) ? 'configured' : 'empty']">
223
+ {{ providerPillText(provider) }}
224
+ </span>
176
225
  <div class="card-actions" @click.stop>
177
226
  <button
178
227
  class="card-action-btn"
179
- @click="openHealthCheckDialog({ providerName: provider.name, locked: true })"
180
- :disabled="displayCurrentProvider !== provider.name"
181
- :aria-label="`Open health dialog for ${provider.name}`"
182
- :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')"
183
233
  >
184
234
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
185
- <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
186
- <path d="M8 9h8"/>
187
- <path d="M8 13h5"/>
235
+ <path d="M13 2L3 14h7l-1 8 12-14h-7l1-6z"/>
188
236
  </svg>
189
237
  </button>
190
238
  <button
@@ -194,7 +242,7 @@
194
242
  :disabled="providerShareLoading[provider.name] || !shouldAllowProviderShare(provider)"
195
243
  @click="copyProviderShareCommand(provider)"
196
244
  :title="shouldAllowProviderShare(provider) ? t('config.shareCommand') : t('config.shareDisabled')"
197
- aria-label="Share import command">
245
+ :aria-label="t('config.shareCommand.aria')">
198
246
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
199
247
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
200
248
  <path d="M16 6l-4-4-4 4"/>
@@ -207,8 +255,8 @@
207
255
  :class="{ disabled: !shouldShowProviderEdit(provider) }"
208
256
  :disabled="!shouldShowProviderEdit(provider)"
209
257
  @click="openEditModal(provider)"
210
- :aria-label="`Edit provider ${provider.name}`"
211
- :title="shouldShowProviderEdit(provider) ? '编辑' : '不可编辑'">
258
+ :aria-label="t('config.provider.edit.aria', { name: provider.name })"
259
+ :title="shouldShowProviderEdit(provider) ? t('common.edit') : t('common.notEditable')">
212
260
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
213
261
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
214
262
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
@@ -220,8 +268,8 @@
220
268
  :class="{ disabled: !shouldShowProviderDelete(provider) }"
221
269
  :disabled="!shouldShowProviderDelete(provider)"
222
270
  @click="deleteProvider(provider.name)"
223
- :aria-label="`Delete provider ${provider.name}`"
224
- :title="shouldShowProviderDelete(provider) ? '删除' : '不可删除'">
271
+ :aria-label="t('config.provider.delete.aria', { name: provider.name })"
272
+ :title="shouldShowProviderDelete(provider) ? t('common.delete') : t('common.notDeletable')">
225
273
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
226
274
  <path d="M3 6h18"/>
227
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"/>
@@ -231,4 +279,5 @@
231
279
  </div>
232
280
  </div>
233
281
  </div>
282
+ </template>
234
283
  </div>