codexmate 0.1.10 → 0.1.13

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 (40) hide show
  1. package/README.md +5 -7
  2. package/README.vi.md +4 -7
  3. package/README.zh.md +5 -7
  4. package/cli/agents-files.js +27 -3
  5. package/cli/prompt-history.js +171 -0
  6. package/cli/system-prompt-files.js +20 -2
  7. package/cli.js +162 -5
  8. package/package.json +1 -1
  9. package/web-ui/app.js +95 -4
  10. package/web-ui/index.html +2 -1
  11. package/web-ui/modules/app.methods.agents.mjs +90 -0
  12. package/web-ui/modules/app.methods.index.mjs +2 -0
  13. package/web-ui/modules/app.methods.navigation.mjs +7 -1
  14. package/web-ui/modules/app.methods.pi-config.mjs +371 -0
  15. package/web-ui/modules/app.methods.session-actions.mjs +57 -0
  16. package/web-ui/modules/app.methods.startup-claude.mjs +2 -1
  17. package/web-ui/modules/app.methods.system-prompt.mjs +81 -0
  18. package/web-ui/modules/app.methods.tool-config-permissions.mjs +3 -2
  19. package/web-ui/modules/app.methods.web-ui-preferences.mjs +8 -1
  20. package/web-ui/modules/config-mode.computed.mjs +2 -1
  21. package/web-ui/modules/i18n/locales/en.mjs +52 -0
  22. package/web-ui/modules/i18n/locales/ja.mjs +52 -0
  23. package/web-ui/modules/i18n/locales/vi.mjs +52 -0
  24. package/web-ui/modules/i18n/locales/zh-tw.mjs +52 -0
  25. package/web-ui/modules/i18n/locales/zh.mjs +52 -0
  26. package/web-ui/partials/index/layout-header.html +20 -0
  27. package/web-ui/partials/index/panel-config-claude.html +3 -3
  28. package/web-ui/partials/index/panel-config-codex.html +3 -3
  29. package/web-ui/partials/index/panel-config-kilocode.html +5 -5
  30. package/web-ui/partials/index/panel-config-openclaw.html +3 -3
  31. package/web-ui/partials/index/panel-config-opencode.html +5 -5
  32. package/web-ui/partials/index/panel-config-pi.html +171 -0
  33. package/web-ui/partials/index/panel-prompts.html +52 -0
  34. package/web-ui/partials/index/panel-sessions.html +12 -0
  35. package/web-ui/partials/index/panel-settings.html +42 -0
  36. package/web-ui/res/web-ui-render.precompiled.js +1101 -252
  37. package/web-ui/styles/modals-core.css +86 -0
  38. package/web-ui/styles/sessions-preview.css +38 -0
  39. package/web-ui/styles/settings-panel.css +7 -0
  40. package/web-ui/styles/titles-cards.css +2 -2
@@ -6,11 +6,11 @@
6
6
  role="tabpanel"
7
7
  :aria-labelledby="forceCompactLayout ? 'tab-config' : 'side-tab-config-opencode'">
8
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
- <button type="button" :class="['segment', { active: configMode === 'opencode' }]" @click="switchConfigMode('opencode')">{{ t('tab.config.opencode') }}</button>
13
- <button type="button" :class="['segment', { active: configMode === 'kilocode' }]" @click="switchConfigMode('kilocode')">{{ t('tab.config.kilocode') }}</button>
9
+ <button v-if="isConfigModeVisible('codex')" type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
10
+ <button v-if="isConfigModeVisible('claude')" type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
+ <button v-if="isConfigModeVisible('openclaw')" type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
+ <button v-if="isConfigModeVisible('opencode')" type="button" :class="['segment', { active: configMode === 'opencode' }]" @click="switchConfigMode('opencode')">{{ t('tab.config.opencode') }}</button>
13
+ <button v-if="isConfigModeVisible('kilocode')" type="button" :class="['segment', { active: configMode === 'kilocode' }]" @click="switchConfigMode('kilocode')">{{ t('tab.config.kilocode') }}</button>
14
14
  </div>
15
15
 
16
16
  <section class="tool-config-write-card" :aria-label="t('opencode.writeAria')">
@@ -0,0 +1,171 @@
1
+ <!-- Pi 配置 -->
2
+ <div
3
+ v-show="mainTab === 'config' && configMode === 'pi'"
4
+ class="mode-content mode-cards"
5
+ id="panel-config-pi"
6
+ role="tabpanel"
7
+ :aria-labelledby="forceCompactLayout ? 'tab-config' : 'side-tab-config-pi'">
8
+ <div v-if="forceCompactLayout && !sessionStandalone" class="segmented-control">
9
+ <button v-if="isConfigModeVisible('codex')" type="button" :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">{{ t('tab.config.codex') }}</button>
10
+ <button v-if="isConfigModeVisible('claude')" type="button" :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">{{ t('tab.config.claude') }}</button>
11
+ <button v-if="isConfigModeVisible('openclaw')" type="button" :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">{{ t('tab.config.openclaw') }}</button>
12
+ <button v-if="isConfigModeVisible('opencode')" type="button" :class="['segment', { active: configMode === 'opencode' }]" @click="switchConfigMode('opencode')">{{ t('tab.config.opencode') }}</button>
13
+ <button v-if="isConfigModeVisible('kilocode')" type="button" :class="['segment', { active: configMode === 'kilocode' }]" @click="switchConfigMode('kilocode')">{{ t('tab.config.kilocode') }}</button>
14
+ <button v-if="isConfigModeVisible('pi')" type="button" :class="['segment', { active: configMode === 'pi' }]" @click="switchConfigMode('pi')">{{ t('tab.config.pi') }}</button>
15
+ </div>
16
+ <section class="tool-config-write-card" :aria-label="t('toolConfig.pi.title')">
17
+ <div class="tool-config-write-copy">
18
+ <div class="tool-config-write-title">{{ t('toolConfig.pi.title') }}</div>
19
+ <p class="tool-config-write-desc">{{ t('toolConfig.pi.desc') }}</p>
20
+ </div>
21
+ <label class="settings-toggle-row tool-config-write-toggle">
22
+ <input
23
+ type="checkbox"
24
+ autocomplete="off"
25
+ :checked="isToolConfigWriteAllowed('pi')"
26
+ :disabled="toolConfigPermissionSaving.pi"
27
+ @change="setToolConfigPermission('pi', $event.target.checked)">
28
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
29
+ <span>{{ toolConfigPermissionStatusLabel('pi') }}</span>
30
+ </label>
31
+ </section>
32
+
33
+ <div class="tool-config-write-scope" :class="{ locked: !isToolConfigWriteAllowed('pi') }">
34
+ <div class="tool-config-write-body">
35
+ <section class="selector-section">
36
+ <div class="selector-header">
37
+ <span class="selector-title">{{ t('pi.providers.title') }}</span>
38
+ <button type="button" class="btn-tool" @click="startAddPiProvider" :disabled="!isToolConfigWriteAllowed('pi') || piSaving">{{ t('pi.providers.add') }}</button>
39
+ </div>
40
+ <div class="config-template-hint">{{ t('pi.providers.targetFile', { path: '~/.pi/agent/models.json' }) }}</div>
41
+ <div class="card-list" v-if="piProviderIds.length">
42
+ <div v-for="providerId in piProviderIds" :key="providerId" class="card" role="button" tabindex="0" @click="openEditPiProvider(providerId)">
43
+ <div class="card-leading">
44
+ <div class="card-icon">{{ piProviderName(providerId).charAt(0).toUpperCase() }}</div>
45
+ <div class="card-content">
46
+ <div class="card-title">{{ piProviderName(providerId) }}</div>
47
+ <div class="card-subtitle">{{ piProviderSummary(providerId) }}</div>
48
+ </div>
49
+ </div>
50
+ <div class="card-trailing">
51
+ <span v-if="piActiveProvider === providerId" class="pill configured">{{ t('pi.providers.active') }}</span>
52
+ <button v-else type="button" class="btn-tool" @click.stop="switchPiActiveProvider(providerId)" :disabled="!isToolConfigWriteAllowed('pi') || piSaving">{{ t('pi.providers.setActive') }}</button>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <div v-if="!piProviderIds.length" class="config-template-hint">{{ t('pi.providers.empty') }}</div>
57
+ </section>
58
+
59
+ <section class="selector-section" v-if="editingPiProvider">
60
+ <div class="selector-header">
61
+ <span class="selector-title">{{ t('pi.providers.editorTitle') }}</span>
62
+ <div class="settings-tab-actions">
63
+ <button type="button" class="btn-tool" @click="savePiProvider" :disabled="!isToolConfigWriteAllowed('pi') || piSaving">{{ t('common.save') }}</button>
64
+ </div>
65
+ </div>
66
+ <div class="codex-config-grid">
67
+ <div class="form-group codex-config-field">
68
+ <label class="form-label">{{ t('pi.providers.fields.name') }}</label>
69
+ <input class="form-input" type="text" :value="piProviderName(editingPiProvider.id)" readonly>
70
+ </div>
71
+ <div class="form-group codex-config-field">
72
+ <label class="form-label">{{ t('pi.providers.fields.baseUrl') }}</label>
73
+ <input class="form-input" type="url" v-model="editingPiProvider.form.baseUrl" autocomplete="off" spellcheck="false" placeholder="https://">
74
+ </div>
75
+ <div class="form-group codex-config-field">
76
+ <label class="form-label">{{ t('pi.providers.fields.api') }}</label>
77
+ <select class="form-input" v-model="editingPiProvider.form.api" :disabled="!isToolConfigWriteAllowed('pi')">
78
+ <option value="openai-completions">openai-completions — {{ t('pi.providers.addModal.api.openaiCompletions') }}</option>
79
+ <option value="openai-responses">openai-responses — {{ t('pi.providers.addModal.api.openaiResponses') }}</option>
80
+ <option value="anthropic-messages">anthropic-messages — {{ t('pi.providers.addModal.api.anthropicMessages') }}</option>
81
+ <option value="google-generative-ai">google-generative-ai — {{ t('pi.providers.addModal.api.googleGenerativeAi') }}</option>
82
+ <option v-if="!['openai-completions', 'openai-responses', 'anthropic-messages', 'google-generative-ai'].includes(editingPiProvider.form.api)" :value="editingPiProvider.form.api">{{ editingPiProvider.form.api || '—' }}</option>
83
+ </select>
84
+ </div>
85
+ <div class="form-group codex-config-field">
86
+ <label class="form-label">{{ t('pi.providers.fields.apiKey') }}</label>
87
+ <div class="input-with-toggle">
88
+ <input class="form-input" v-model="editingPiProvider.form.apiKey" :type="piShowKey ? 'text' : 'password'" autocomplete="off" spellcheck="false" placeholder="sk-...">
89
+ <button type="button" class="input-toggle-btn" @mousedown.prevent @click="piShowKey = !piShowKey" :title="piShowKey ? t('common.hide') : t('common.show')" :aria-label="piShowKey ? t('common.hide') : t('common.show')">
90
+ <svg v-if="!piShowKey" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M10 4C5 4 1.73 8.11 1 10c.73 1.89 4 6 9 6s8.27-4.11 9-6c-.73-1.89-4-6-9-6z"/><circle cx="10" cy="10" r="3"/></svg>
91
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16"><path d="M2 2l16 16M8.2 4.2A9.9 9.9 0 0 1 10 4c5 0 8.27 4.11 9 6-.44.94-1.5 2.7-3.2 4.2M14.5 14.5A5.9 5.9 0 0 1 10 16c-5 0-8.27-4.11-9-6 .76-1.66 2.2-3.6 4.3-5"/></svg>
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ <div class="form-group">
97
+ <label class="form-label">{{ t('pi.providers.fields.models') }}</label>
98
+ <p v-if="piRemoteModelsLoading" class="config-template-hint">{{ t('pi.providers.models.remoteLoading') }}</p>
99
+ <p v-else-if="piRemoteModelError" class="config-template-hint">{{ piRemoteModelError }} · {{ t('pi.providers.models.remoteFailed') }}</p>
100
+ <div class="card-list">
101
+ <div class="card" v-for="(model, index) in editingPiProvider.form.models" :key="index">
102
+ <div class="input-with-toggle">
103
+ <input class="form-input" type="text" v-model="model.id" autocomplete="off" spellcheck="false" placeholder="claude-sonnet-4" :disabled="!isToolConfigWriteAllowed('pi')" @input="piModelSearch = model.id">
104
+ <button type="button" class="btn-tool input-toggle-btn" @click="editingPiProvider.form.models.splice(index, 1)" :disabled="!isToolConfigWriteAllowed('pi')">{{ t('pi.providers.models.remove') }}</button>
105
+ </div>
106
+ </div>
107
+ <div class="card" v-if="editingPiProvider.form.models.length === 0">
108
+ <input class="form-input" type="text" v-model="piModelSearch" autocomplete="off" spellcheck="false" :placeholder="t('pi.providers.models.search')" @keydown.enter.prevent="addPiRemoteModel(piModelSearch.trim())">
109
+ </div>
110
+ <div class="card" v-if="!piRemoteModelsLoading && !piRemoteModelError && piModelSearch.trim() && piFilteredRemoteModels().length > 0">
111
+ <button type="button" class="btn-tool" style="width: 100%; text-align: left; justify-content: flex-start;" v-for="id in piFilteredRemoteModels()" :key="id" @click="addPiRemoteModel(id)">{{ id }}</button>
112
+ <p class="config-template-hint">{{ t('pi.providers.models.remoteCount', { count: piRemoteModels.length }) }}</p>
113
+ </div>
114
+ <p v-if="!piRemoteModelsLoading && !piRemoteModelError && piModelSearch.trim() && piFilteredRemoteModels().length === 0" class="config-template-hint">{{ t('pi.providers.models.noMatch') }}</p>
115
+ </div>
116
+ </div>
117
+ <div class="settings-tab-actions">
118
+ <button type="button" class="btn-tool" @click="confirmDeletePiProvider" :disabled="!isToolConfigWriteAllowed('pi') || piSaving">{{ t('pi.providers.delete') }}</button>
119
+ </div>
120
+ </section>
121
+
122
+ <section class="selector-section" v-if="showAddPiProviderModal">
123
+ <div class="selector-header">
124
+ <span class="selector-title">{{ t('pi.providers.addModal.title') }}</span>
125
+ <div class="settings-tab-actions">
126
+ <button type="button" class="btn-tool" @click="addPiProviderFromModal" :disabled="!isToolConfigWriteAllowed('pi') || piSaving">{{ t('common.save') }}</button>
127
+ <button type="button" class="btn-tool" @click="cancelAddPiProviderModal">{{ t('common.cancel') }}</button>
128
+ </div>
129
+ </div>
130
+ <div class="config-template-hint">{{ t('pi.providers.addModal.subtitle') }}</div>
131
+ <div class="codex-config-grid">
132
+ <div class="form-group codex-config-field">
133
+ <label class="form-label">{{ t('pi.providers.addModal.fields.baseUrl') }}</label>
134
+ <input class="form-input" type="url" v-model="addingPiProviderBaseUrl" autocomplete="off" spellcheck="false" placeholder="https://" @input="onAddingPiEndpointChange">
135
+ </div>
136
+ <div class="form-group codex-config-field">
137
+ <label class="form-label">{{ t('pi.providers.addModal.fields.api') }}</label>
138
+ <select class="form-input" v-model="addingPiProviderApi">
139
+ <option value="openai-completions">openai-completions — {{ t('pi.providers.addModal.api.openaiCompletions') }}</option>
140
+ <option value="openai-responses">openai-responses — {{ t('pi.providers.addModal.api.openaiResponses') }}</option>
141
+ <option value="anthropic-messages">anthropic-messages — {{ t('pi.providers.addModal.api.anthropicMessages') }}</option>
142
+ <option value="google-generative-ai">google-generative-ai — {{ t('pi.providers.addModal.api.googleGenerativeAi') }}</option>
143
+ </select>
144
+ </div>
145
+ <div class="form-group codex-config-field">
146
+ <label class="form-label">{{ t('pi.providers.addModal.fields.apiKey') }}</label>
147
+ <input class="form-input" type="password" v-model="addingPiProviderApiKey" autocomplete="off" spellcheck="false" :placeholder="t('common.optional')" @input="onAddingPiEndpointChange">
148
+ </div>
149
+ <div class="form-group codex-config-field">
150
+ <label class="form-label">{{ t('pi.providers.fields.models') }}</label>
151
+ <p v-if="addingPiRemoteLoading" class="config-template-hint">{{ t('pi.providers.models.remoteLoading') }}</p>
152
+ <p v-else-if="addingPiRemoteError" class="config-template-hint">{{ addingPiRemoteError }} · {{ t('pi.providers.models.remoteFailed') }}</p>
153
+ <input class="form-input" type="text" v-model="addingPiProviderModel" autocomplete="off" spellcheck="false" :placeholder="addingPiRemoteModels.length ? t('pi.providers.models.search') : 'claude-sonnet-4'">
154
+ <template v-if="!addingPiRemoteLoading && addingPiProviderModel.trim() && piFilteredAddingRemoteModels().length > 0">
155
+ <button type="button" class="btn-tool" style="width: 100%; text-align: left; justify-content: flex-start; margin-top: 8px;" v-for="id in piFilteredAddingRemoteModels()" :key="id" @click="addingPiProviderModel = id">{{ id }}</button>
156
+ <p class="config-template-hint">{{ t('pi.providers.models.remoteCount', { count: addingPiRemoteModels.length }) }}</p>
157
+ </template>
158
+ <p v-else-if="!addingPiRemoteLoading && !addingPiRemoteError && addingPiProviderModel.trim() && addingPiRemoteModels.length > 0" class="config-template-hint">{{ t('pi.providers.models.noMatch') }}</p>
159
+ </div>
160
+ </div>
161
+ </section>
162
+ <div v-if="!isToolConfigWriteAllowed('pi')" class="tool-config-write-overlay">
163
+ <div class="tool-config-write-overlay-card">
164
+ <div class="tool-config-write-overlay-title">{{ t('toolConfig.pi.lockedTitle') }}</div>
165
+ <p>{{ t('toolConfig.pi.lockedDesc') }}</p>
166
+ <button type="button" class="btn-tool" @click="setToolConfigPermission('pi', true)" :disabled="toolConfigPermissionSaving.pi">{{ t('toolConfig.enableWrite') }}</button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
@@ -65,6 +65,13 @@
65
65
  :title="t('common.paste')">
66
66
  <svg class="btn-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/></svg>
67
67
  </button>
68
+ <button
69
+ class="btn-mini"
70
+ @click="openPromptHistory"
71
+ :disabled="agentsSaving || agentsDiffVisible"
72
+ :title="t('common.history')">
73
+ <svg class="btn-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 8v4l3 2"/><path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-7.5 4M3 4v4h4"/></svg>
74
+ </button>
68
75
  </div>
69
76
  <div class="prompts-editor-group prompts-editor-group--workflow">
70
77
  <button class="btn-mini" @click="loadPromptsContent" :disabled="agentsSaving || agentsDiffLoading" :title="t('common.cancel')">
@@ -85,6 +92,27 @@
85
92
  </div>
86
93
  </div>
87
94
  </div>
95
+ <details v-if="promptHistoryVisible" class="prompt-history-panel" open>
96
+ <summary class="prompt-history-summary">
97
+ <span>{{ t('common.history') }}</span>
98
+ <button type="button" class="btn-mini prompt-history-close" @click="closePromptHistory" :title="t('common.close')">✕</button>
99
+ </summary>
100
+ <div class="prompt-history-body">
101
+ <ul v-if="!promptHistoryLoading && !promptHistoryError && promptHistoryItems.length" class="prompt-history-list">
102
+ <li v-for="item in promptHistoryItems" :key="item.id" :class="['prompt-history-item', { active: promptHistoryPreviewId === item.id }]" @click="viewPromptHistoryItem(item)">
103
+ <span class="prompt-history-time">{{ item.id }}</span>
104
+ <span class="prompt-history-size">{{ Math.round(item.size / 1024 * 10) / 10 }} KB</span>
105
+ </li>
106
+ </ul>
107
+ <div v-if="promptHistoryLoading" class="state-message">{{ t('diff.generating') }}</div>
108
+ <div v-else-if="promptHistoryError" class="state-message error">{{ promptHistoryError }}</div>
109
+ <div v-else-if="!promptHistoryItems.length" class="state-message">{{ t('toast.history.empty') }}</div>
110
+ <div v-if="promptHistoryPreviewContent" class="prompt-history-preview-wrap">
111
+ <pre class="prompt-history-preview">{{ promptHistoryPreviewContent }}</pre>
112
+ <button type="button" class="btn-mini btn-confirm-mini" @click="applyPromptHistoryToEditor" :disabled="agentsSaving || agentsDiffVisible">{{ t('common.restoreToEditor') }}</button>
113
+ </div>
114
+ </div>
115
+ </details>
88
116
  <div class="prompt-presets-inline-row">
89
117
  <div class="prompt-presets-inline-group">
90
118
  <label class="prompt-presets-inline-label">{{ t('prompts.presets.title') }}</label>
@@ -228,6 +256,9 @@
228
256
  <button class="btn-mini" @click="pasteSysPromptContent" :disabled="sysPromptLoading || sysPromptSaving || sysPromptDiffVisible" :title="t('common.paste')">
229
257
  <svg class="btn-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="8" y="2" width="8" height="4" rx="1"/><path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"/></svg>
230
258
  </button>
259
+ <button class="btn-mini" @click="openSysHistory" :disabled="sysPromptSaving || sysPromptDiffVisible" :title="t('common.history')">
260
+ <svg class="btn-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 8v4l3 2"/><path d="M3 12a9 9 0 1 0 9-9 9 9 0 0 0-7.5 4M3 4v4h4"/></svg>
261
+ </button>
231
262
  </div>
232
263
  <div class="prompts-editor-group prompts-editor-group--workflow">
233
264
  <button class="btn-mini" @click="cancelSysPromptEdit" :disabled="sysPromptSaving || sysPromptDiffLoading" :title="t('common.cancel')">
@@ -243,6 +274,27 @@
243
274
  </div>
244
275
  </div>
245
276
  </div>
277
+ <details v-if="sysHistoryVisible" class="prompt-history-panel" open>
278
+ <summary class="prompt-history-summary">
279
+ <span>{{ t('common.history') }}</span>
280
+ <button type="button" class="btn-mini prompt-history-close" @click="closeSysHistory" :title="t('common.close')">✕</button>
281
+ </summary>
282
+ <div class="prompt-history-body">
283
+ <ul v-if="!sysHistoryLoading && !sysHistoryError && sysHistoryItems.length" class="prompt-history-list">
284
+ <li v-for="item in sysHistoryItems" :key="item.id" :class="['prompt-history-item', { active: sysHistoryPreviewId === item.id }]" @click="viewSysHistoryItem(item)">
285
+ <span class="prompt-history-time">{{ item.id }}</span>
286
+ <span class="prompt-history-size">{{ Math.round(item.size / 1024 * 10) / 10 }} KB</span>
287
+ </li>
288
+ </ul>
289
+ <div v-if="sysHistoryLoading" class="state-message">{{ t('diff.generating') }}</div>
290
+ <div v-else-if="sysHistoryError" class="state-message error">{{ sysHistoryError }}</div>
291
+ <div v-else-if="!sysHistoryItems.length" class="state-message">{{ t('toast.history.empty') }}</div>
292
+ <div v-if="sysHistoryPreviewContent" class="prompt-history-preview-wrap">
293
+ <pre class="prompt-history-preview">{{ sysHistoryPreviewContent }}</pre>
294
+ <button type="button" class="btn-mini btn-confirm-mini" @click="applySysHistoryToEditor" :disabled="sysPromptSaving || sysPromptDiffVisible">{{ t('common.restoreToEditor') }}</button>
295
+ </div>
296
+ </div>
297
+ </details>
246
298
  <div class="form-group">
247
299
  <div v-if="sysPromptDiffVisible">
248
300
  <div
@@ -410,6 +410,18 @@
410
410
  </div>
411
411
  </div>
412
412
  <div class="session-msg-content" v-html="highlightQueryText(msg.text) || ''"></div>
413
+ <button
414
+ v-if="msg.normalizedRole === 'user'"
415
+ type="button"
416
+ class="session-msg-copy"
417
+ @click="copySessionMessage(msg)"
418
+ :aria-label="t('sessions.preview.copyMessage')"
419
+ :title="t('sessions.preview.copyMessage')">
420
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
421
+ <rect x="8" y="8" width="12" height="12" rx="2"></rect>
422
+ <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
423
+ </svg>
424
+ </button>
413
425
  </div>
414
426
  <div v-if="canLoadMoreSessionMessages" class="session-preview-load-more">
415
427
  <span>{{ t('sessions.preview.shownCount', { shown: activeSessionVisibleMessages.length, total: activeSessionMessages.length }) }}</span>
@@ -138,6 +138,48 @@
138
138
  </div>
139
139
  </div>
140
140
  </section>
141
+
142
+ <section class="settings-card" :aria-label="t('settings.configTabs.title')">
143
+ <div class="settings-card-main">
144
+ <div class="settings-card-content">
145
+ <div class="settings-card-title">{{ t('settings.configTabs.title') }}</div>
146
+ <p class="settings-card-desc">{{ t('settings.configTabs.meta') }}</p>
147
+ <div class="settings-toggle-grid">
148
+ <label class="settings-toggle-row">
149
+ <input type="checkbox" :checked="configModeVisibility.codex" @change="setConfigModeVisibility('codex', $event.target.checked)">
150
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
151
+ <span>{{ t('tab.config.codex') }}</span>
152
+ </label>
153
+ <label class="settings-toggle-row">
154
+ <input type="checkbox" :checked="configModeVisibility.claude" @change="setConfigModeVisibility('claude', $event.target.checked)">
155
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
156
+ <span>{{ t('tab.config.claude') }}</span>
157
+ </label>
158
+ <label class="settings-toggle-row">
159
+ <input type="checkbox" :checked="configModeVisibility.openclaw" @change="setConfigModeVisibility('openclaw', $event.target.checked)">
160
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
161
+ <span>{{ t('tab.config.openclaw') }}</span>
162
+ </label>
163
+ <label class="settings-toggle-row">
164
+ <input type="checkbox" :checked="configModeVisibility.opencode" @change="setConfigModeVisibility('opencode', $event.target.checked)">
165
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
166
+ <span>{{ t('tab.config.opencode') }}</span>
167
+ </label>
168
+ <label class="settings-toggle-row">
169
+ <input type="checkbox" :checked="configModeVisibility.kilocode" @change="setConfigModeVisibility('kilocode', $event.target.checked)">
170
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
171
+ <span>{{ t('tab.config.kilocode') }}</span>
172
+ </label>
173
+ <label class="settings-toggle-row">
174
+ <input type="checkbox" :checked="configModeVisibility.pi" @change="setConfigModeVisibility('pi', $event.target.checked)">
175
+ <span class="toggle-track"><span class="toggle-thumb"></span></span>
176
+ <span>{{ t('tab.config.pi') }}</span>
177
+ </label>
178
+ </div>
179
+ <p class="settings-card-hint">{{ t('settings.configTabs.hint') }}</p>
180
+ </div>
181
+ </div>
182
+ </section>
141
183
  </div>
142
184
  </div>
143
185