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
@@ -37,10 +37,15 @@
37
37
  </aside>
38
38
 
39
39
  <section class="plugins-main" :aria-label="t('plugins.main.ariaWorkspace')">
40
- <div v-if="pluginsActiveId === 'prompt-templates'" class="plugins-panel">
40
+ <div v-if="pluginsLoading" class="skills-empty-state">{{ t('common.loading') }}</div>
41
+ <div v-else-if="pluginsError" class="skills-empty-state">
42
+ <div class="plugins-panel-note">{{ pluginsError }}</div>
43
+ <button type="button" class="btn-mini" @click="loadPluginsOverview({ forceRefresh: true, silent: false })" :disabled="loading || !!initError || pluginsLoading">{{ t('common.refresh') }}</button>
44
+ </div>
45
+ <div v-else-if="pluginsActiveId === 'prompt-templates'" class="plugins-panel">
41
46
  <div class="plugins-panel-head">
42
47
  <div class="plugins-panel-title">{{ t('plugins.promptTemplates.title') }}</div>
43
- <div class="plugins-panel-subtitle">{{ t('plugins.promptTemplates.subtitle') }}</div>
48
+ <div v-if="pluginsActiveAttribution" class="plugins-panel-note">{{ pluginsActiveAttribution }}</div>
44
49
  </div>
45
50
 
46
51
  <div class="prompt-templates-modebar" role="tablist" :aria-label="t('plugins.promptTemplates.mode.aria')">
@@ -63,6 +68,9 @@
63
68
  <div class="prompt-compose-selected">
64
69
  <div class="prompt-compose-selected-title">{{ (promptComposerActiveTemplate && promptComposerActiveTemplate.name) ? promptComposerActiveTemplate.name : t('plugins.promptTemplates.compose.chooseTemplate') }}</div>
65
70
  <div class="prompt-compose-selected-meta">{{ (promptComposerActiveTemplate && promptComposerActiveTemplate.description) ? promptComposerActiveTemplate.description : t('plugins.promptTemplates.compose.chooseTemplateHint') }}</div>
71
+ <div v-if="promptComposerActiveTemplate && promptComposerActiveTemplate.isBuiltin && (promptComposerActiveTemplate.createdBy || (promptComposerActiveTemplate.maintainers && promptComposerActiveTemplate.maintainers.length))" class="plugins-panel-note">
72
+ {{ t('plugins.meta.attribution', { createdBy: promptComposerActiveTemplate.createdBy || '', maintainers: (promptComposerActiveTemplate.maintainers || []).join(', ') }) }}
73
+ </div>
66
74
  </div>
67
75
 
68
76
  <div class="prompt-compose-form">
@@ -73,13 +81,13 @@
73
81
  @change="selectPromptComposerTemplate($event.target.value)"
74
82
  :disabled="pluginsLoading || !promptTemplatesList.length">
75
83
  <option v-for="tpl in promptTemplatesList" :key="'compose-tpl-' + tpl.id" :value="tpl.id">
76
- {{ tpl.name }}{{ tpl.isBuiltin ? '(内置)' : '' }}
84
+ {{ tpl.name }}{{ tpl.isBuiltin ? t('plugins.promptTemplates.compose.builtinSuffix') : '' }}
77
85
  </option>
78
86
  </select>
79
87
 
80
88
  <div v-if="!promptComposerActiveTemplate" class="plugins-panel-note">{{ t('plugins.promptTemplates.compose.empty') }}</div>
81
- <div v-else class="plugins-panel-note">{{ t('plugins.promptTemplates.compose.varsHint') }}</div>
82
- <div class="prompt-compose-actions">
89
+ <div v-else-if="!promptComposerActiveTemplate.isBuiltin" class="plugins-panel-note">{{ t('plugins.promptTemplates.compose.varsHint') }}</div>
90
+ <div v-if="promptComposerActiveTemplate && !promptComposerActiveTemplate.isBuiltin" class="prompt-compose-actions">
83
91
  <button
84
92
  type="button"
85
93
  class="btn-mini"
@@ -88,6 +96,31 @@
88
96
  </div>
89
97
  </div>
90
98
 
99
+ <div v-if="promptComposerActiveTemplate && promptComposerActiveTemplate.vars && promptComposerActiveTemplate.vars.length" class="prompt-vars-block">
100
+ <div class="prompt-vars-head">
101
+ <div>
102
+ <div class="prompt-vars-title">{{ t('plugins.promptTemplates.vars.title') }}</div>
103
+ <div v-if="!promptComposerActiveTemplate.isBuiltin" class="plugins-panel-note">{{ t('plugins.promptTemplates.compose.varsHint') }}</div>
104
+ <div v-if="promptComposerMissingVars.length" class="plugins-panel-note">{{ t('plugins.promptTemplates.compose.missingCount', { count: promptComposerMissingVars.length }) }}</div>
105
+ </div>
106
+ <div class="prompt-editor-actions">
107
+ <button v-if="promptComposerMissingVars.length" type="button" class="btn-mini" @click="focusPromptComposerFirstMissingVar" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.compose.jumpToMissing') }}</button>
108
+ <button type="button" class="btn-mini" @click="resetPromptComposerVarValues" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.reset') }}</button>
109
+ </div>
110
+ </div>
111
+ <div class="prompt-vars-grid">
112
+ <label v-for="(name, idx) in promptComposerActiveTemplate.vars" :key="'prompt-compose-var-' + name" class="prompt-var-row">
113
+ <span class="prompt-var-label mono">{{ name }}</span>
114
+ <template v-if="idx === 0">
115
+ <input ref="promptComposerFirstField" :class="['form-input', 'prompt-var-input', { 'is-missing': promptComposerMissingVars.includes(name) }]" type="text" :value="promptComposerVarValues[name] || ''" @input="setPromptComposerVarValue(name, $event.target.value)" :placeholder="t('plugins.promptTemplates.vars.valuePlaceholder', { name })">
116
+ </template>
117
+ <template v-else>
118
+ <input :class="['form-input', 'prompt-var-input', { 'is-missing': promptComposerMissingVars.includes(name) }]" type="text" :value="promptComposerVarValues[name] || ''" @input="setPromptComposerVarValue(name, $event.target.value)" :placeholder="t('plugins.promptTemplates.vars.valuePlaceholder', { name })">
119
+ </template>
120
+ </label>
121
+ </div>
122
+ </div>
123
+
91
124
  <div class="prompt-preview-block prompt-compose-preview">
92
125
  <div class="prompt-vars-head">
93
126
  <div>
@@ -96,7 +129,7 @@
96
129
  </div>
97
130
  <button type="button" class="btn-mini" @click="copyPromptComposerRendered" :disabled="pluginsLoading || !promptComposerRendered">{{ t('plugins.promptTemplates.compose.copy') }}</button>
98
131
  </div>
99
- <textarea class="form-input prompt-preview-textarea" :value="promptComposerRendered" rows="10" readonly spellcheck="false" aria-label="Rendered prompt (composer)"></textarea>
132
+ <textarea class="form-input prompt-preview-textarea" :value="promptComposerRendered" rows="10" readonly spellcheck="false" :aria-label="t('plugins.promptTemplates.compose.outputAria')"></textarea>
100
133
  </div>
101
134
  </div>
102
135
  </div>
@@ -108,7 +141,7 @@
108
141
  class="form-input"
109
142
  type="text"
110
143
  v-model.trim="promptTemplatesKeyword"
111
- aria-label="Search templates"
144
+ :aria-label="t('plugins.promptTemplates.manage.searchAria')"
112
145
  :placeholder="t('plugins.promptTemplates.manage.searchPlaceholder')">
113
146
  <button type="button" class="btn-mini" @click="createPromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.manage.create') }}</button>
114
147
  <button type="button" class="btn-mini" @click="exportPromptTemplates" :disabled="pluginsLoading || !promptTemplatesList.length">{{ t('plugins.promptTemplates.manage.export') }}</button>
@@ -143,7 +176,7 @@
143
176
  <template v-else>
144
177
  <div class="prompt-editor-head">
145
178
  <div class="prompt-editor-title-row">
146
- <input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraft.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" aria-label="Template name">
179
+ <input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraft.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" :aria-label="t('plugins.promptTemplates.editor.nameAria')">
147
180
  <div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
148
181
  <button type="button" class="btn-mini" @click="duplicatePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.duplicate') }}</button>
149
182
  <button type="button" class="btn-mini delete" @click="deletePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.delete') }}</button>
@@ -153,7 +186,10 @@
153
186
  <div v-if="promptTemplateDraft.isBuiltin" class="plugins-panel-note">
154
187
  {{ t('plugins.promptTemplates.editor.builtinReadOnly') }}
155
188
  </div>
156
- <input class="form-input" type="text" v-model.trim="promptTemplateDraft.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" aria-label="Template description">
189
+ <div v-if="promptTemplateDraft.isBuiltin && (promptTemplateDraft.createdBy || (promptTemplateDraft.maintainers && promptTemplateDraft.maintainers.length))" class="plugins-panel-note">
190
+ {{ t('plugins.meta.attribution', { createdBy: promptTemplateDraft.createdBy || '', maintainers: (promptTemplateDraft.maintainers || []).join(', ') }) }}
191
+ </div>
192
+ <input class="form-input" type="text" v-model.trim="promptTemplateDraft.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" :aria-label="t('plugins.promptTemplates.editor.descAria')">
157
193
  </div>
158
194
 
159
195
  <div class="prompt-editor-body">
@@ -164,7 +200,7 @@
164
200
  :disabled="promptTemplateDraft.isBuiltin"
165
201
  rows="10"
166
202
  spellcheck="false"
167
- aria-label="Template body"
203
+ :aria-label="t('plugins.promptTemplates.editor.templateAria')"
168
204
  :placeholder="t('plugins.promptTemplates.editor.templatePlaceholder')"></textarea>
169
205
 
170
206
  <div class="prompt-vars-block">
@@ -183,7 +219,7 @@
183
219
  <div v-else class="prompt-vars-grid">
184
220
  <label v-for="name in promptTemplateVars" :key="'prompt-var-' + name" class="prompt-var-row">
185
221
  <span class="prompt-var-label mono">{{ name }}</span>
186
- <input class="form-input prompt-var-input" type="text" :disabled="promptTemplateDraft.isBuiltin" :value="promptTemplateVarValues[name] || ''" @input="setPromptVariableValue(name, $event.target.value)" :placeholder="t('plugins.promptTemplates.vars.valuePlaceholder', { name })">
222
+ <input class="form-input prompt-var-input" type="text" :readonly="promptTemplateDraft.isBuiltin" :value="promptTemplateVarValues[name] || ''" @input="setPromptVariableValue(name, $event.target.value)" :placeholder="t('plugins.promptTemplates.vars.valuePlaceholder', { name })">
187
223
  </label>
188
224
  </div>
189
225
  </div>
@@ -196,7 +232,7 @@
196
232
  </div>
197
233
  <button type="button" class="btn-mini" @click="copyRenderedPrompt" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.preview.copy') }}</button>
198
234
  </div>
199
- <textarea class="form-input prompt-preview-textarea" :value="renderedPrompt" rows="10" readonly spellcheck="false" aria-label="Rendered prompt"></textarea>
235
+ <textarea class="form-input prompt-preview-textarea" :value="renderedPrompt" rows="10" readonly spellcheck="false" :aria-label="t('plugins.promptTemplates.preview.outputAria')"></textarea>
200
236
  </div>
201
237
  </div>
202
238
  </template>
@@ -28,6 +28,9 @@
28
28
  <button class="btn-tool btn-tool-compact" @click="loadSessions({ forceRefresh: true })" :disabled="sessionsLoading">
29
29
  {{ sessionsLoading ? t('sessions.refreshing') : t('sessions.refresh') }}
30
30
  </button>
31
+ <button class="btn-tool btn-tool-compact" type="button" @click="switchMainTab('dashboard')">
32
+ {{ t('dashboard.doctor.title') }}
33
+ </button>
31
34
  </div>
32
35
  </div>
33
36
  <div class="session-toolbar">
@@ -51,7 +54,7 @@
51
54
  <input
52
55
  class="session-query-input"
53
56
  v-model="sessionQuery"
54
- @keyup.enter="loadSessions"
57
+ @keyup.enter="onSessionFilterChange"
55
58
  :disabled="sessionsLoading || !isSessionQueryEnabled"
56
59
  :placeholder="sessionQueryPlaceholder">
57
60
  </div>
@@ -60,7 +63,7 @@
60
63
  class="session-role-select"
61
64
  v-model="sessionRoleFilter"
62
65
  @change="onSessionFilterChange"
63
- disabled>
66
+ :disabled="sessionsLoading">
64
67
  <option value="all">{{ t('sessions.role.all') }}</option>
65
68
  <option value="user">{{ t('sessions.role.user') }}</option>
66
69
  <option value="assistant">{{ t('sessions.role.assistant') }}</option>
@@ -70,12 +73,18 @@
70
73
  class="session-time-select"
71
74
  v-model="sessionTimePreset"
72
75
  @change="onSessionFilterChange"
73
- disabled>
76
+ :disabled="sessionsLoading">
74
77
  <option value="all">{{ t('sessions.time.all') }}</option>
75
78
  <option value="7d">{{ t('sessions.time.7d') }}</option>
76
79
  <option value="30d">{{ t('sessions.time.30d') }}</option>
77
80
  <option value="90d">{{ t('sessions.time.90d') }}</option>
78
81
  </select>
82
+ <button class="btn-tool btn-tool-compact" type="button" @click="copySessionsFilterShareUrl" :disabled="sessionsLoading">
83
+ {{ t('sessions.filters.copyLink') }}
84
+ </button>
85
+ <button class="btn-tool btn-tool-compact" type="button" @click="clearSessionFilters" :disabled="sessionsLoading">
86
+ {{ t('common.resetFilters') }}
87
+ </button>
79
88
  </div>
80
89
  </div>
81
90
  <div class="session-toolbar-footer">
@@ -5,7 +5,7 @@
5
5
  id="panel-settings"
6
6
  role="tabpanel"
7
7
  :aria-labelledby="'tab-settings'">
8
- <div class="config-subtabs settings-subtabs" role="tablist" aria-label="Settings tabs">
8
+ <div class="config-subtabs settings-subtabs" role="tablist" :aria-label="t('settings.tabs.aria')">
9
9
  <button
10
10
  id="settings-tab-backup"
11
11
  role="tab"
@@ -10,10 +10,11 @@
10
10
  <span class="selector-title">{{ t('usage.overview') }}</span>
11
11
  </div>
12
12
  <div class="usage-range-group" role="group" :aria-label="t('usage.range.aria')">
13
- <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '7d' }" @click="sessionsUsageTimeRange = '7d'">{{ t('usage.range.7d') }}</button>
14
- <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '30d' }" @click="sessionsUsageTimeRange = '30d'">{{ t('usage.range.30d') }}</button>
15
- <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === 'all' }" @click="sessionsUsageTimeRange = 'all'">{{ t('usage.range.all') }}</button>
16
- <button type="button" class="usage-range-btn" @click="loadSessionsUsage({ forceRefresh: true })" :disabled="sessionsUsageLoading">{{ sessionsUsageLoading ? t('usage.refreshing') : t('usage.refresh') }}</button>
13
+ <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '7d' }" @click="setSessionsUsageTimeRange('7d')">{{ t('usage.range.7d') }}</button>
14
+ <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '30d' }" @click="setSessionsUsageTimeRange('30d')">{{ t('usage.range.30d') }}</button>
15
+ <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === 'all' }" @click="setSessionsUsageTimeRange('all')">{{ t('usage.range.all') }}</button>
16
+ <button type="button" class="usage-range-btn" @click="loadSessionsUsage({ forceRefresh: true, range: sessionsUsageTimeRange })" :disabled="sessionsUsageLoading">{{ sessionsUsageLoading ? t('usage.refreshing') : t('usage.refresh') }}</button>
17
+ <button type="button" class="usage-range-btn" @click="switchMainTab('dashboard')">{{ t('dashboard.doctor.title') }}</button>
17
18
  </div>
18
19
  </div>
19
20
 
@@ -299,10 +300,10 @@
299
300
  </div>
300
301
  </section>
301
302
 
302
- <section class="usage-card">
303
+ <section class="usage-card usage-card-top-paths">
303
304
  <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
304
305
  <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>
305
- <div v-else class="usage-list">
306
+ <div v-else class="usage-list usage-list-scroll usage-list-top-paths">
306
307
  <div v-for="item in sessionUsageCharts.topPaths" :key="item.path" class="usage-list-row">
307
308
  <div class="usage-list-label">{{ t('usage.paths.count', { count: item.count }) }}</div>
308
309
  <div class="usage-progress"><div class="usage-progress-fill" :style="{ width: ((item.count / Math.max((sessionUsageCharts.topPaths.length ? sessionUsageCharts.topPaths[0].count : 1), 1)) * 100) + '%' }"></div></div>
@@ -86,6 +86,7 @@
86
86
  .selector-actions {
87
87
  display: flex;
88
88
  gap: var(--spacing-xs);
89
+ align-items: center;
89
90
  }
90
91
 
91
92
  .health-report {
@@ -316,6 +317,13 @@
316
317
  margin-top: var(--spacing-xs);
317
318
  }
318
319
 
320
+ .selector-header .settings-tab-actions > .btn-tool + .btn-tool,
321
+ .selector-header .settings-tab-actions > .btn-tool-compact + .btn-tool,
322
+ .selector-header .settings-tab-actions > .btn-tool + .btn-tool-compact,
323
+ .selector-header .settings-tab-actions > .btn-tool-compact + .btn-tool-compact {
324
+ margin-top: 0;
325
+ }
326
+
319
327
  .selector-header .trash-header-actions > .btn-tool + .btn-tool {
320
328
  margin-top: 0;
321
329
  }
@@ -353,10 +361,16 @@
353
361
  }
354
362
 
355
363
  .selector-header .btn-tool-compact {
356
- padding: 3px 7px;
364
+ display: inline-flex;
365
+ align-items: center;
366
+ justify-content: center;
367
+ width: auto;
368
+ min-width: 0;
369
+ padding: 2px 6px;
357
370
  font-size: 10px;
358
371
  line-height: 1;
359
372
  box-shadow: none;
360
- min-height: 24px;
373
+ height: 22px;
374
+ min-height: 22px;
361
375
  white-space: nowrap;
362
376
  }
@@ -0,0 +1,274 @@
1
+ .doctor-hero {
2
+ margin-top: 0;
3
+ }
4
+
5
+ .doctor-grid {
6
+ display: grid;
7
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
8
+ gap: 12px;
9
+ }
10
+
11
+ .doctor-card {
12
+ text-align: left;
13
+ padding: 14px 14px 12px;
14
+ border-radius: var(--radius-lg);
15
+ border: 1px solid var(--color-border-soft);
16
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.92) 0%, rgba(246, 240, 232, 0.9) 100%);
17
+ box-shadow: var(--shadow-subtle);
18
+ cursor: pointer;
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 8px;
22
+ min-width: 0;
23
+ max-width: 100%;
24
+ overflow: hidden;
25
+ }
26
+
27
+ .doctor-card:disabled {
28
+ opacity: 0.6;
29
+ cursor: not-allowed;
30
+ }
31
+
32
+ .doctor-card-title {
33
+ font-size: var(--font-size-body);
34
+ font-weight: var(--font-weight-title);
35
+ color: var(--color-text-primary);
36
+ }
37
+
38
+ .doctor-card-meta {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 6px;
42
+ font-size: var(--font-size-caption);
43
+ color: var(--color-text-tertiary);
44
+ white-space: nowrap;
45
+ overflow: hidden;
46
+ text-overflow: ellipsis;
47
+ }
48
+
49
+ .doctor-card-meta span {
50
+ min-width: 0;
51
+ }
52
+
53
+ .doctor-card-meta span:not(:nth-child(2)) {
54
+ flex: 1 1 auto;
55
+ overflow: hidden;
56
+ text-overflow: ellipsis;
57
+ }
58
+
59
+ .doctor-card-kv {
60
+ display: grid;
61
+ grid-template-columns: 92px minmax(0, 1fr);
62
+ gap: 8px;
63
+ align-items: baseline;
64
+ font-size: var(--font-size-caption);
65
+ color: var(--color-text-secondary);
66
+ min-width: 0;
67
+ }
68
+
69
+ .doctor-card-kv > span:first-child {
70
+ color: var(--color-text-tertiary);
71
+ }
72
+
73
+ .doctor-card-kv > span:nth-child(2) {
74
+ min-width: 0;
75
+ overflow: hidden;
76
+ display: -webkit-box;
77
+ -webkit-box-orient: vertical;
78
+ -webkit-line-clamp: 2;
79
+ overflow-wrap: anywhere;
80
+ word-break: break-word;
81
+ justify-self: center;
82
+ text-align: center;
83
+ }
84
+
85
+ .doctor-kv-error {
86
+ color: #8a2f36;
87
+ overflow: hidden;
88
+ text-overflow: ellipsis;
89
+ white-space: nowrap;
90
+ }
91
+
92
+ .doctor-status-row {
93
+ margin-top: 12px;
94
+ display: flex;
95
+ flex-wrap: wrap;
96
+ gap: 10px;
97
+ justify-content: center;
98
+ }
99
+
100
+ .doctor-status-chip {
101
+ display: inline-flex;
102
+ align-items: center;
103
+ gap: 8px;
104
+ padding: 8px 10px;
105
+ border-radius: 999px;
106
+ border: 1px solid var(--color-border-soft);
107
+ background: rgba(255, 255, 255, 0.78);
108
+ font-size: var(--font-size-caption);
109
+ color: var(--color-text-secondary);
110
+ justify-content: center;
111
+ text-align: center;
112
+ }
113
+
114
+ .doctor-status-chip strong {
115
+ color: var(--color-text-primary);
116
+ font-weight: var(--font-weight-secondary);
117
+ }
118
+
119
+ .doctor-status-chip.ok {
120
+ border-color: rgba(57, 181, 97, 0.25);
121
+ background: rgba(57, 181, 97, 0.08);
122
+ color: #2b6a3b;
123
+ }
124
+
125
+ .doctor-status-chip.warn {
126
+ border-color: rgba(246, 211, 106, 0.45);
127
+ background: rgba(246, 211, 106, 0.16);
128
+ color: #6f4b00;
129
+ }
130
+
131
+ .doctor-status-chip.error {
132
+ border-color: rgba(220, 95, 108, 0.28);
133
+ background: rgba(220, 95, 108, 0.08);
134
+ color: #8a2f36;
135
+ }
136
+
137
+ .doctor-health-result {
138
+ margin-top: 14px;
139
+ border-radius: var(--radius-lg);
140
+ border: 1px solid var(--color-border-soft);
141
+ padding: 12px 12px 10px;
142
+ background: rgba(255, 255, 255, 0.78);
143
+ text-align: center;
144
+ }
145
+
146
+ .doctor-health-result.ok {
147
+ border-color: rgba(57, 181, 97, 0.25);
148
+ background: rgba(57, 181, 97, 0.08);
149
+ color: #2b6a3b;
150
+ }
151
+
152
+ .doctor-health-result.error {
153
+ border-color: rgba(220, 95, 108, 0.28);
154
+ background: rgba(220, 95, 108, 0.08);
155
+ color: #8a2f36;
156
+ }
157
+
158
+ .doctor-health-title {
159
+ font-weight: var(--font-weight-secondary);
160
+ color: var(--color-text-primary);
161
+ margin-bottom: 8px;
162
+ text-align: center;
163
+ }
164
+
165
+ .doctor-health-issues {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 6px;
169
+ font-size: var(--font-size-caption);
170
+ align-items: center;
171
+ }
172
+
173
+ .doctor-health-issue {
174
+ padding: 8px 10px;
175
+ border-radius: 12px;
176
+ border: 1px solid rgba(220, 95, 108, 0.2);
177
+ background: rgba(255, 255, 255, 0.75);
178
+ color: var(--color-text-secondary);
179
+ overflow-wrap: anywhere;
180
+ word-break: break-word;
181
+ text-align: center;
182
+ width: 100%;
183
+ max-width: 720px;
184
+ }
185
+
186
+ .doctor-action-list {
187
+ margin-top: 14px;
188
+ display: grid;
189
+ gap: 10px;
190
+ }
191
+
192
+ .doctor-action-card {
193
+ border-radius: var(--radius-lg);
194
+ border: 1px solid var(--color-border-soft);
195
+ background: rgba(255, 255, 255, 0.78);
196
+ padding: 12px;
197
+ overflow: hidden;
198
+ text-align: center;
199
+ }
200
+
201
+ .doctor-action-head {
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: center;
205
+ gap: 10px;
206
+ flex-wrap: wrap;
207
+ margin-bottom: 8px;
208
+ }
209
+
210
+ .doctor-action-title {
211
+ font-weight: var(--font-weight-secondary);
212
+ color: var(--color-text-primary);
213
+ min-width: 0;
214
+ overflow: hidden;
215
+ text-overflow: ellipsis;
216
+ white-space: nowrap;
217
+ max-width: 100%;
218
+ }
219
+
220
+ .doctor-action-severity {
221
+ font-size: var(--font-size-caption);
222
+ padding: 3px 10px;
223
+ border-radius: 999px;
224
+ border: 1px solid var(--color-border-soft);
225
+ background: rgba(255, 255, 255, 0.85);
226
+ color: var(--color-text-secondary);
227
+ text-transform: uppercase;
228
+ }
229
+
230
+ .doctor-action-severity.error {
231
+ border-color: rgba(220, 95, 108, 0.28);
232
+ background: rgba(220, 95, 108, 0.08);
233
+ color: #8a2f36;
234
+ }
235
+
236
+ .doctor-action-severity.warn {
237
+ border-color: rgba(246, 211, 106, 0.45);
238
+ background: rgba(246, 211, 106, 0.16);
239
+ color: #6f4b00;
240
+ }
241
+
242
+ .doctor-action-severity.info {
243
+ border-color: rgba(126, 163, 215, 0.35);
244
+ background: rgba(126, 163, 215, 0.14);
245
+ color: #1f3d62;
246
+ }
247
+
248
+ .doctor-action-impact {
249
+ font-size: var(--font-size-caption);
250
+ color: var(--color-text-secondary);
251
+ overflow-wrap: anywhere;
252
+ word-break: break-word;
253
+ max-width: 920px;
254
+ margin: 0 auto 10px;
255
+ }
256
+
257
+ .doctor-action-buttons {
258
+ display: flex;
259
+ flex-wrap: wrap;
260
+ gap: 8px;
261
+ justify-content: center;
262
+ }
263
+
264
+ .doctor-action-footer {
265
+ display: flex;
266
+ justify-content: center;
267
+ margin-top: 2px;
268
+ }
269
+
270
+ @media (max-width: 520px) {
271
+ .doctor-card-kv {
272
+ grid-template-columns: 76px minmax(0, 1fr);
273
+ }
274
+ }
@@ -73,7 +73,7 @@ body::after {
73
73
  display: flex;
74
74
  flex-direction: column;
75
75
  gap: 4px;
76
- padding: 14px 10px;
76
+ padding: 6px;
77
77
  }
78
78
 
79
79
  .side-section + .side-section {
@@ -210,13 +210,19 @@ body::after {
210
210
  z-index: 2;
211
211
  margin-top: auto;
212
212
  padding: 12px 10px;
213
- background: linear-gradient(180deg, rgba(255, 253, 252, 0.45) 0%, rgba(255, 253, 252, 0.95) 48%, rgba(255, 253, 252, 1) 100%);
213
+ background: transparent;
214
214
  border-top: 1px solid var(--color-border);
215
- backdrop-filter: blur(10px);
215
+ backdrop-filter: none;
216
216
  display: flex;
217
217
  justify-content: center;
218
218
  }
219
219
 
220
+ .side-rail-lang .lang-choice {
221
+ background: rgba(255, 253, 252, 0.9);
222
+ box-shadow: none;
223
+ backdrop-filter: none;
224
+ }
225
+
220
226
  .lang-switch {
221
227
  width: 100%;
222
228
  border: none;
@@ -309,14 +315,14 @@ body::after {
309
315
  color: var(--color-text-muted);
310
316
  letter-spacing: 0.06em;
311
317
  text-transform: uppercase;
312
- padding: 0 8px;
318
+ padding: 0 6px;
313
319
  margin-bottom: 8px;
314
320
  }
315
321
 
316
322
  .side-item {
317
323
  width: 100%;
318
324
  text-align: left;
319
- padding: 7px 10px;
325
+ padding: 5px;
320
326
  border-radius: 8px;
321
327
  border: 1px solid transparent;
322
328
  background: transparent;
@@ -148,6 +148,14 @@
148
148
  box-shadow: var(--shadow-input-focus);
149
149
  }
150
150
 
151
+ .provider-fast-switch-select:disabled {
152
+ cursor: not-allowed;
153
+ opacity: 0.72;
154
+ border-color: var(--color-border-soft);
155
+ background-color: var(--color-surface-alt);
156
+ box-shadow: none;
157
+ }
158
+
151
159
  .main-panel {
152
160
  min-width: 0;
153
161
  background: var(--color-bg);
@@ -502,6 +502,11 @@
502
502
  min-width: 0;
503
503
  }
504
504
 
505
+ .prompt-var-input.is-missing {
506
+ border-color: var(--color-error);
507
+ box-shadow: 0 0 0 1px rgba(196, 69, 54, 0.18);
508
+ }
509
+
505
510
  @media (max-width: 980px) {
506
511
  .plugins-layout {
507
512
  grid-template-columns: 1fr;
@@ -142,7 +142,7 @@
142
142
  gap: var(--spacing-sm);
143
143
  align-items: start;
144
144
  height: min(72vh, 760px);
145
- min-height: 520px;
145
+ min-height: 360px;
146
146
  }
147
147
 
148
148
  .session-layout.session-standalone {
@@ -205,9 +205,9 @@
205
205
  min-width: 0;
206
206
  position: relative;
207
207
  overflow: hidden;
208
- min-height: 102px;
208
+ min-height: 80px;
209
209
  content-visibility: auto;
210
- contain-intrinsic-size: 102px;
210
+ contain-intrinsic-size: 80px;
211
211
  }
212
212
 
213
213
  .session-item-header {