codexmate 0.1.28 → 0.1.29

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 (41) hide show
  1. package/README.md +53 -44
  2. package/README.vi.md +45 -36
  3. package/README.zh.md +51 -42
  4. package/cli/session-usage.js +2 -2
  5. package/cli.js +369 -15
  6. package/package.json +1 -1
  7. package/web-ui/app.js +3 -0
  8. package/web-ui/index.html +3 -2
  9. package/web-ui/logic.sessions.mjs +6 -4
  10. package/web-ui/modules/app.computed.session.mjs +52 -33
  11. package/web-ui/modules/app.methods.agents.mjs +36 -0
  12. package/web-ui/modules/app.methods.codex-config.mjs +3 -1
  13. package/web-ui/modules/app.methods.providers.mjs +12 -4
  14. package/web-ui/modules/app.methods.session-actions.mjs +10 -8
  15. package/web-ui/modules/app.methods.session-browser.mjs +3 -1
  16. package/web-ui/modules/app.methods.session-trash.mjs +16 -16
  17. package/web-ui/modules/app.methods.theme.mjs +5 -4
  18. package/web-ui/modules/i18n/locales/en.mjs +74 -1
  19. package/web-ui/modules/i18n/locales/ja.mjs +74 -1
  20. package/web-ui/modules/i18n/locales/vi.mjs +74 -1
  21. package/web-ui/modules/i18n/locales/zh-tw.mjs +74 -1
  22. package/web-ui/modules/i18n/locales/zh.mjs +74 -1
  23. package/web-ui/modules/i18n.mjs +7 -0
  24. package/web-ui/partials/index/modal-config-template-agents.html +1 -1
  25. package/web-ui/partials/index/modal-health-check.html +1 -1
  26. package/web-ui/partials/index/modal-openclaw-config.html +50 -50
  27. package/web-ui/partials/index/panel-dashboard.html +1 -1
  28. package/web-ui/partials/index/panel-prompts.html +2 -2
  29. package/web-ui/partials/index/panel-sessions.html +45 -28
  30. package/web-ui/partials/index/panel-settings.html +6 -0
  31. package/web-ui/res/web-ui-render.precompiled.js +219 -156
  32. package/web-ui/session-helpers.mjs +3 -0
  33. package/web-ui/styles/base-theme.css +2 -2
  34. package/web-ui/styles/bridge-pool.css +1 -1
  35. package/web-ui/styles/layout-shell.css +0 -1
  36. package/web-ui/styles/sessions-preview.css +77 -0
  37. package/web-ui/styles/theme-dark-tokens.css +2 -2
  38. package/web-ui/styles/theme-dark.css +2 -2
  39. package/web-ui/styles/theme-ocean-tokens.css +88 -0
  40. package/web-ui/styles/theme-ocean.css +920 -0
  41. package/web-ui/styles.css +1 -0
@@ -12,7 +12,7 @@
12
12
  <div class="form-hint">
13
13
  {{ openclawConfigPath || t('common.notLoaded') }}
14
14
  <span v-if="openclawConfigPath">
15
- ({{ openclawConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnApply') }})
15
+ {{ paren(openclawConfigExists ? t('common.exists') : t('common.notExistsWillCreateOnApply')) }}
16
16
  </span>
17
17
  </div>
18
18
  <div class="btn-group" style="justify-content:flex-start;">
@@ -52,7 +52,7 @@
52
52
  <div class="accordion-content" v-show="openclawAccordionStep === 1">
53
53
  <div class="form-group">
54
54
  <label class="form-label">{{ t('field.providerName') }}</label>
55
- <input v-model="openclawQuick.providerName" class="form-input" placeholder="例如: custom-myapi" @input="validateProviderName">
55
+ <input v-model="openclawQuick.providerName" class="form-input" :placeholder="t('modal.openclaw.quick.providerNamePlaceholder')" @input="validateProviderName">
56
56
  <div class="form-hint" :class="{ 'hint-error': !openclawValidation.providerName.valid }">
57
57
  {{ openclawValidation.providerName.message || t('modal.openclaw.quick.providerHint') }}
58
58
  </div>
@@ -87,7 +87,7 @@
87
87
  </div>
88
88
  <div class="accordion-actions">
89
89
  <button class="btn btn-confirm btn-sm" @click="nextAccordionStep" :disabled="!openclawQuick.providerName">
90
- 下一步 →
90
+ {{ t('modal.openclaw.next') }}
91
91
  </button>
92
92
  </div>
93
93
  </div>
@@ -111,13 +111,13 @@
111
111
  <label class="form-label">{{ t('field.modelId') }}</label>
112
112
  <input v-model="openclawQuick.modelId" class="form-input" :placeholder="t('placeholder.modelIdExample')" @input="validateModelId">
113
113
  <div class="form-hint" :class="{ 'hint-error': !openclawValidation.modelId.valid }">
114
- {{ openclawValidation.modelId.message || '必填,例如: gpt-4' }}
114
+ {{ openclawValidation.modelId.message || t('modal.openclaw.quick.modelIdHint') }}
115
115
  </div>
116
116
  </div>
117
117
  <div class="form-group">
118
118
  <label class="form-label">{{ t('field.displayName') }}</label>
119
119
  <input v-model="openclawQuick.modelName" class="form-input" :placeholder="t('placeholder.modelNameOptional')">
120
- <div class="form-hint">可选,用于显示</div>
120
+ <div class="form-hint">{{ t('modal.openclaw.quick.modelNameHint') }}</div>
121
121
  </div>
122
122
  <div class="form-group">
123
123
  <label class="form-label">{{ t('field.contextAndMaxOutput') }}</label>
@@ -129,10 +129,10 @@
129
129
  </div>
130
130
  <div class="accordion-actions">
131
131
  <button class="btn btn-cancel btn-sm" @click="prevAccordionStep">
132
- ← 上一步
132
+ {{ t('modal.openclaw.prev') }}
133
133
  </button>
134
134
  <button class="btn btn-confirm btn-sm" @click="nextAccordionStep" :disabled="!openclawQuick.modelId">
135
- 下一步 →
135
+ {{ t('modal.openclaw.next') }}
136
136
  </button>
137
137
  </div>
138
138
  </div>
@@ -146,7 +146,7 @@
146
146
  <template v-else>3</template>
147
147
  </span>
148
148
  <span class="accordion-title">{{ t('modal.openclaw.quick.optionsTitle') }}</span>
149
- <span class="accordion-status">高级选项</span>
149
+ <span class="accordion-status">{{ t('modal.openclaw.quick.advancedOptions') }}</span>
150
150
  <svg class="accordion-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
151
151
  <path d="M6 9l6 6 6-6"/>
152
152
  </svg>
@@ -167,10 +167,10 @@
167
167
  <div class="form-hint">{{ t('modal.openclaw.quick.optionsHint') }}</div>
168
168
  <div class="accordion-actions">
169
169
  <button class="btn btn-cancel btn-sm" @click="prevAccordionStep">
170
- ← 上一步
170
+ {{ t('modal.openclaw.prev') }}
171
171
  </button>
172
172
  <button class="btn btn-confirm btn-sm" @click="finishAccordionStep">
173
- 完成 ✓
173
+ {{ t('modal.openclaw.finish') }}
174
174
  </button>
175
175
  </div>
176
176
  </div>
@@ -184,55 +184,55 @@
184
184
 
185
185
  <div class="structured-section">
186
186
  <div class="structured-header">
187
- <span class="structured-title">结构化配置(高级)</span>
187
+ <span class="structured-title">{{ t('modal.openclaw.structured.title') }}</span>
188
188
  <span class="form-hint">{{ t('modal.openclaw.structured.writeHint') }}</span>
189
189
  </div>
190
190
  <div class="structured-grid">
191
191
  <div class="structured-card">
192
192
  <div class="structured-card-title">Agents Defaults</div>
193
193
  <div class="form-group">
194
- <label class="form-label">主模型</label>
195
- <input v-model="openclawStructured.agentPrimary" class="form-input" placeholder="例如: provider/model">
194
+ <label class="form-label">{{ t('modal.openclaw.structured.primaryModel') }}</label>
195
+ <input v-model="openclawStructured.agentPrimary" class="form-input" :placeholder="t('modal.openclaw.structured.providerModelPlaceholder')">
196
196
  </div>
197
197
  <div class="form-group">
198
- <label class="form-label">备选模型</label>
198
+ <label class="form-label">{{ t('modal.openclaw.structured.fallbackModels') }}</label>
199
199
  <div class="list-row" v-for="(item, index) in openclawStructured.agentFallbacks" :key="'fallback-' + index">
200
- <input v-model="openclawStructured.agentFallbacks[index]" class="form-input" placeholder="例如: provider/model">
201
- <button class="btn-mini delete" @click="removeOpenclawFallback(index)">删除</button>
200
+ <input v-model="openclawStructured.agentFallbacks[index]" class="form-input" :placeholder="t('modal.openclaw.structured.providerModelPlaceholder')">
201
+ <button class="btn-mini delete" @click="removeOpenclawFallback(index)">{{ t('common.delete') }}</button>
202
202
  </div>
203
- <button class="btn-mini" @click="addOpenclawFallback">添加备选</button>
203
+ <button class="btn-mini" @click="addOpenclawFallback">{{ t('modal.openclaw.structured.addFallback') }}</button>
204
204
  </div>
205
205
  <div class="form-group">
206
206
  <label class="form-label">Workspace</label>
207
- <input v-model="openclawStructured.workspace" class="form-input" placeholder="例如: ~/.openclaw/workspace">
207
+ <input v-model="openclawStructured.workspace" class="form-input" :placeholder="t('modal.openclaw.structured.workspacePlaceholder')">
208
208
  </div>
209
209
  <div class="form-group">
210
210
  <label class="form-label">Timeout(s)</label>
211
- <input v-model="openclawStructured.timeout" class="form-input" placeholder="例如: 600">
211
+ <input v-model="openclawStructured.timeout" class="form-input" :placeholder="t('modal.openclaw.structured.timeoutPlaceholder')">
212
212
  </div>
213
213
  <div class="form-group">
214
214
  <label class="form-label">Context Tokens</label>
215
- <input v-model="openclawStructured.contextTokens" class="form-input" placeholder="例如: 4096">
215
+ <input v-model="openclawStructured.contextTokens" class="form-input" :placeholder="t('modal.openclaw.structured.contextTokensPlaceholder')">
216
216
  </div>
217
217
  <div class="form-group">
218
218
  <label class="form-label">Max Concurrent</label>
219
- <input v-model="openclawStructured.maxConcurrent" class="form-input" placeholder="例如: 2">
219
+ <input v-model="openclawStructured.maxConcurrent" class="form-input" :placeholder="t('modal.openclaw.structured.maxConcurrentPlaceholder')">
220
220
  </div>
221
221
  </div>
222
222
 
223
223
  <div class="structured-card">
224
224
  <div class="structured-card-title">Env</div>
225
225
  <div class="form-group">
226
- <label class="form-label">环境变量</label>
226
+ <label class="form-label">{{ t('modal.openclaw.structured.envVars') }}</label>
227
227
  <div class="list-row" v-for="(item, index) in openclawStructured.envItems" :key="'env-' + index">
228
228
  <input v-model="item.key" class="form-input" placeholder="KEY">
229
229
  <input v-model="item.value" class="form-input" :type="item.show ? 'text' : 'password'" placeholder="VALUE">
230
230
  <button class="btn-mini" @click="toggleOpenclawEnvItem(index)">
231
- {{ item.show ? '隐藏' : '显示' }}
231
+ {{ item.show ? t('common.hide') : t('common.show') }}
232
232
  </button>
233
- <button class="btn-mini delete" @click="removeOpenclawEnvItem(index)">删除</button>
233
+ <button class="btn-mini delete" @click="removeOpenclawEnvItem(index)">{{ t('common.delete') }}</button>
234
234
  </div>
235
- <button class="btn-mini" @click="addOpenclawEnvItem">添加变量</button>
235
+ <button class="btn-mini" @click="addOpenclawEnvItem">{{ t('modal.openclaw.structured.addEnvVar') }}</button>
236
236
  </div>
237
237
  </div>
238
238
 
@@ -250,34 +250,34 @@
250
250
  <div class="form-group">
251
251
  <label class="form-label">Allow</label>
252
252
  <div class="list-row" v-for="(item, index) in openclawStructured.toolsAllow" :key="'allow-' + index">
253
- <input v-model="openclawStructured.toolsAllow[index]" class="form-input" placeholder="例如: fs.read*">
254
- <button class="btn-mini delete" @click="removeOpenclawToolsAllow(index)">删除</button>
253
+ <input v-model="openclawStructured.toolsAllow[index]" class="form-input" :placeholder="t('modal.openclaw.structured.allowPlaceholder')">
254
+ <button class="btn-mini delete" @click="removeOpenclawToolsAllow(index)">{{ t('common.delete') }}</button>
255
255
  </div>
256
- <button class="btn-mini" @click="addOpenclawToolsAllow">添加 allow</button>
256
+ <button class="btn-mini" @click="addOpenclawToolsAllow">{{ t('modal.openclaw.structured.addAllow') }}</button>
257
257
  </div>
258
258
  <div class="form-group">
259
259
  <label class="form-label">Deny</label>
260
260
  <div class="list-row" v-for="(item, index) in openclawStructured.toolsDeny" :key="'deny-' + index">
261
- <input v-model="openclawStructured.toolsDeny[index]" class="form-input" placeholder="例如: net.*">
262
- <button class="btn-mini delete" @click="removeOpenclawToolsDeny(index)">删除</button>
261
+ <input v-model="openclawStructured.toolsDeny[index]" class="form-input" :placeholder="t('modal.openclaw.structured.denyPlaceholder')">
262
+ <button class="btn-mini delete" @click="removeOpenclawToolsDeny(index)">{{ t('common.delete') }}</button>
263
263
  </div>
264
- <button class="btn-mini" @click="addOpenclawToolsDeny">添加 deny</button>
264
+ <button class="btn-mini" @click="addOpenclawToolsDeny">{{ t('modal.openclaw.structured.addDeny') }}</button>
265
265
  </div>
266
266
  </div>
267
267
 
268
268
  <div class="structured-card">
269
- <div class="structured-card-title">Providers(只读)</div>
269
+ <div class="structured-card-title">{{ t('modal.openclaw.structured.providersTitle') }}</div>
270
270
  <div v-if="openclawProviders.length === 0" class="form-hint">
271
- 未发现 providers 配置(可能使用环境变量或外部配置)。
271
+ {{ t('modal.openclaw.structured.noProviders') }}
272
272
  </div>
273
273
  <div v-else class="provider-list">
274
274
  <div class="provider-item" v-for="(provider, index) in openclawProviders" :key="provider.key + '-' + provider.source + '-' + index">
275
275
  <div class="provider-header">
276
276
  <span class="provider-name">{{ provider.key }}</span>
277
- <span class="provider-source">来源: {{ provider.source }}</span>
278
- <span v-if="provider.isActive" class="pill configured">使用中</span>
277
+ <span class="provider-source">{{ t('modal.openclaw.structured.sourceLabel') }}: {{ provider.source }}</span>
278
+ <span v-if="provider.isActive" class="pill configured">{{ t('modal.openclaw.structured.active') }}</span>
279
279
  </div>
280
- <div v-if="provider.fields.length === 0" class="form-hint">未配置字段</div>
280
+ <div v-if="provider.fields.length === 0" class="form-hint">{{ t('modal.openclaw.structured.noFields') }}</div>
281
281
  <div v-else class="provider-fields">
282
282
  <div class="provider-field" v-for="field in provider.fields" :key="provider.key + '-' + field.key">
283
283
  <span class="provider-field-key">{{ field.key }}</span>
@@ -287,14 +287,14 @@
287
287
  </div>
288
288
  </div>
289
289
  <div v-if="openclawMissingProviders.length" class="form-hint">
290
- 使用中的 provider 未在配置中显示:{{ openclawMissingProviders.join(', ') }}。
290
+ {{ t('modal.openclaw.structured.missingProviders', { list: openclawMissingProviders.join(', ') }) }}
291
291
  </div>
292
292
  </div>
293
293
 
294
294
  <div class="structured-card">
295
- <div class="structured-card-title">Agents(只读)</div>
295
+ <div class="structured-card-title">{{ t('modal.openclaw.structured.agentsTitle') }}</div>
296
296
  <div v-if="openclawAgentsList.length === 0" class="form-hint">
297
- 未发现 agents.list 配置。
297
+ {{ t('modal.openclaw.structured.noAgents') }}
298
298
  </div>
299
299
  <div v-else class="agent-list">
300
300
  <div class="agent-item" v-for="(agent, index) in openclawAgentsList" :key="agent.key + '-' + index">
@@ -303,41 +303,41 @@
303
303
  <span class="agent-id">ID: {{ agent.id }}</span>
304
304
  </div>
305
305
  <div class="agent-meta" v-if="agent.theme || agent.emoji || agent.avatar">
306
- <span v-if="agent.theme">主题: {{ agent.theme }}</span>
307
- <span v-if="agent.emoji">表情: {{ agent.emoji }}</span>
308
- <span v-if="agent.avatar">头像: {{ agent.avatar }}</span>
306
+ <span v-if="agent.theme">{{ t('modal.openclaw.structured.themeLabel') }}: {{ agent.theme }}</span>
307
+ <span v-if="agent.emoji">{{ t('modal.openclaw.structured.emojiLabel') }}: {{ agent.emoji }}</span>
308
+ <span v-if="agent.avatar">{{ t('modal.openclaw.structured.avatarLabel') }}: {{ agent.avatar }}</span>
309
309
  </div>
310
310
  </div>
311
311
  </div>
312
312
  </div>
313
313
  </div>
314
314
  <div class="btn-group">
315
- <button class="btn btn-confirm secondary" @click="syncOpenclawStructuredFromText">从文本刷新</button>
315
+ <button class="btn btn-confirm secondary" @click="syncOpenclawStructuredFromText">{{ t('common.refreshFromText') }}</button>
316
316
  <button class="btn btn-confirm" @click="applyOpenclawStructuredToText">{{ t('common.writeToEditor') }}</button>
317
317
  </div>
318
318
  </div>
319
319
 
320
320
  <div class="form-group">
321
- <label class="form-label">OpenClaw 配置(JSON5)</label>
321
+ <label class="form-label">{{ t('modal.openclaw.editorLabel') }}</label>
322
322
  <textarea
323
323
  v-model="openclawEditing.content"
324
324
  class="form-input template-editor"
325
325
  spellcheck="false"
326
326
  :readonly="openclawSaving || openclawApplying"
327
- placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
327
+ :placeholder="t('modal.openclaw.editorPlaceholder')"></textarea>
328
328
  <div class="template-editor-warning">
329
- <span v-if="openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name)">默认配置始终映射当前 openclaw.json,请直接使用“保存并应用”。</span>
330
- <span v-else>保存仅写入本地配置库。点击“保存并应用”后会写入 openclaw.json。</span>
329
+ <span v-if="openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name)">{{ t('modal.openclaw.defaultConfigHint') }}</span>
330
+ <span v-else>{{ t('modal.openclaw.saveOnlyHint') }}</span>
331
331
  </div>
332
332
  </div>
333
333
 
334
334
  <div class="btn-group">
335
- <button class="btn btn-cancel" @click="closeOpenclawConfigModal" :disabled="openclawSaving || openclawApplying">取消</button>
335
+ <button class="btn btn-cancel" @click="closeOpenclawConfigModal" :disabled="openclawSaving || openclawApplying">{{ t('common.cancel') }}</button>
336
336
  <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving || openclawApplying || (openclawEditing.lockName && isDefaultOpenclawConfig(openclawEditing.name))">
337
- {{ openclawSaving ? '保存中...' : '保存' }}
337
+ {{ openclawSaving ? t('common.saving') : t('common.save') }}
338
338
  </button>
339
339
  <button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawSaving || openclawApplying">
340
- {{ openclawApplying ? '应用中...' : '保存并应用' }}
340
+ {{ openclawApplying ? t('common.applying') : t('common.saveApply') }}
341
341
  </button>
342
342
  </div>
343
343
  </div>
@@ -35,7 +35,7 @@
35
35
  <div class="doctor-health-result" :class="healthCheckResult.ok ? 'ok' : 'error'">
36
36
  <div class="doctor-health-title">
37
37
  {{ healthCheckResult.ok ? t('dashboard.health.ok') : t('dashboard.health.fail') }}
38
- <span v-if="healthCheckResult.issues && healthCheckResult.issues.length">({{ t('dashboard.health.issues', { count: healthCheckResult.issues.length }) }})</span>
38
+ <span v-if="healthCheckResult.issues && healthCheckResult.issues.length">{{ paren(t('dashboard.health.issues', { count: healthCheckResult.issues.length })) }}</span>
39
39
  </div>
40
40
  </div>
41
41
  <div v-if="healthCheckResult.report" class="doctor-action-list">
@@ -40,7 +40,7 @@
40
40
  <div class="form-hint">
41
41
  {{ agentsPath || t('common.notLoaded') }}
42
42
  <span v-if="agentsPath">
43
- ({{ agentsExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }})
43
+ {{ paren(agentsExists ? t('common.exists') : t('common.notExistsWillCreateOnSave')) }}
44
44
  </span>
45
45
  <span v-if="agentsPath" class="prompts-token-hint">· {{ t('prompts.tokenCount.hint', { count: agentsTokenCount }) }}</span>
46
46
  </div>
@@ -279,7 +279,7 @@
279
279
  <div class="form-hint">
280
280
  {{ sysPromptPath || t('common.notLoaded') }}
281
281
  <span v-if="sysPromptPath">
282
- ({{ sysPromptExists ? t('common.exists') : t('common.notExistsWillCreateOnSave') }})
282
+ {{ paren(sysPromptExists ? t('common.exists') : t('common.notExistsWillCreateOnSave')) }}
283
283
  </span>
284
284
  <span v-if="sysPromptPath" class="prompts-token-hint">· {{ t('prompts.tokenCount.hint', { count: sysPromptTokenCount }) }}</span>
285
285
  </div>
@@ -1,7 +1,7 @@
1
1
  <!-- 会话浏览模式 -->
2
2
  <div
3
3
  v-show="mainTab === 'sessions'"
4
- class="mode-content"
4
+ class="mode-content session-browser-refined"
5
5
  id="panel-sessions"
6
6
  role="tabpanel"
7
7
  :aria-labelledby="'tab-sessions'">
@@ -133,12 +133,19 @@
133
133
  </div>
134
134
  </div>
135
135
 
136
+ <div v-if="sessionSearchInfo && !sessionsLoading" class="session-search-status" role="status">
137
+ <span class="session-search-count">{{ t('sessions.search.coverage', { scanned: sessionSearchInfo.scanned, candidates: sessionSearchInfo.candidates }) }}</span>
138
+ <span v-if="!sessionSearchInfo.complete" class="session-search-partial">{{ t('sessions.search.partial') }}</span>
139
+ <small class="session-search-scope">{{ t('sessions.search.scopeHint') }}</small>
140
+ </div>
141
+
136
142
  <div v-if="sessionsLoading && sessionsList.length === 0" class="state-message">
137
143
  {{ t('sessions.loadingList') }}
138
144
  </div>
139
145
 
140
- <div v-else-if="!sessionsLoading && sessionsList.length === 0" class="session-empty">
141
- {{ t('sessions.empty') }}
146
+ <div v-else-if="!sessionsLoading && sessionsList.length === 0" class="session-empty session-empty-refined">
147
+ <p>{{ t(hasActiveSessionFilters() ? 'sessions.empty.filtered' : 'sessions.empty.source') }}</p>
148
+ <button v-if="hasActiveSessionFilters()" class="btn-tool btn-tool-compact" type="button" @click="clearSessionFilters">{{ t('sessions.empty.clearFilters') }}</button>
142
149
  </div>
143
150
 
144
151
  <div v-else class="session-layout">
@@ -244,24 +251,13 @@
244
251
  <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
245
252
  </div>
246
253
  </div>
247
- <div class="session-actions">
248
- <button class="btn-session-refresh has-label" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession"
249
- :title="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')"
250
- :aria-label="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')">
251
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.8"/><path d="M13.5 8a5.5 5.5 0 0 1-9.4 3.8"/><polyline points="2.5 2 2.5 5 5.5 5"/><polyline points="13.5 14 13.5 11 10.5 11"/></svg>
252
- <span class="session-action-label">{{ t('sessions.preview.refresh') }}</span>
253
- </button>
254
- <button
255
- v-if="isDeleteAvailable(activeSession)"
256
- class="btn-session-delete has-label"
257
- @click="deleteSession(activeSession)"
258
- :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]"
259
- :title="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))"
260
- :aria-label="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))">
261
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 4 4 4 13 4"/><path d="M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4"/><path d="M12 4v9.5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 13.5V4"/></svg>
262
- <span class="session-action-label">{{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash')) }}</span>
263
- </button>
264
- <button
254
+ <div class="session-export-control">
255
+ <label class="session-export-label" for="session-export-range">{{ t('sessions.export.range') }}</label>
256
+ <select id="session-export-range" v-model="sessionExportRange" :disabled="sessionExporting[getSessionExportKey(activeSession)]" aria-describedby="session-export-scope">
257
+ <option value="default">{{ t('sessions.export.default') }}</option>
258
+ <option value="all">{{ t('sessions.export.all') }}</option>
259
+ </select>
260
+ <button
265
261
  class="btn-session-export has-label"
266
262
  @click="exportSession(activeSession)"
267
263
  :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]"
@@ -270,25 +266,31 @@
270
266
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8"/><polyline points="4 7 8 10.5 12 7"/><path d="M2.5 12v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V12"/></svg>
271
267
  <span class="session-action-label">{{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export') }}</span>
272
268
  </button>
273
- <div class="session-link-group">
274
- <button
269
+ </div>
270
+ <div class="session-actions session-preview-actions">
271
+ <div class="session-secondary-actions">
272
+ <button class="btn-session-refresh has-label" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession"
273
+ :title="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')"
274
+ :aria-label="sessionDetailLoading ? t('sessions.preview.loading') : t('sessions.preview.refresh')">
275
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 8a5.5 5.5 0 0 1 9.4-3.8"/><path d="M13.5 8a5.5 5.5 0 0 1-9.4 3.8"/><polyline points="2.5 2 2.5 5 5.5 5"/><polyline points="13.5 14 13.5 11 10.5 11"/></svg>
276
+ <span class="session-action-label">{{ t('sessions.preview.refresh') }}</span>
277
+ </button>
278
+ <div class="session-link-group"><button
275
279
  class="btn-session-open"
276
280
  @click="copySessionLink(activeSession)"
277
281
  :disabled="!activeSession || !canBuildStandaloneUrl(activeSession)"
278
282
  :title="t('sessions.preview.copyLink')"
279
283
  :aria-label="t('sessions.preview.copyLink')">
280
284
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 9.5a3.5 3.5 0 0 0 5 0l2-2a3.5 3.5 0 0 0-5-5l-1 1"/><path d="M9.5 6.5a3.5 3.5 0 0 0-5 0l-2 2a3.5 3.5 0 0 0 5 5l1-1"/></svg>
281
- </button>
282
- <button
285
+ </button> <button
283
286
  class="btn-session-open"
284
287
  @click="openSessionLink(activeSession)"
285
288
  :disabled="!activeSession || !canBuildStandaloneUrl(activeSession)"
286
289
  :title="t('sessions.preview.openLink')"
287
290
  :aria-label="t('sessions.preview.openLink')">
288
291
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2h-8.5A1.5 1.5 0 0 0 2 3.5v8A1.5 1.5 0 0 0 3.5 13h8a1.5 1.5 0 0 0 1.5-1.5v-3"/><path d="M13 2v4h-4"/><path d="M13 2L7 8"/></svg>
289
- </button>
290
- </div>
291
- <button
292
+ </button></div>
293
+ <button
292
294
  class="btn-session-open has-label"
293
295
  @click="copySessionPath(activeSession)"
294
296
  :disabled="!activeSession || !getSessionFilePath(activeSession)"
@@ -297,8 +299,23 @@
297
299
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 5.5V12.5a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7.5a1 1 0 0 0-1-1H8L6.5 5H3a1 1 0 0 0-1 1z"/></svg>
298
300
  <span class="session-action-label">{{ t('sessions.preview.copyPath') }}</span>
299
301
  </button>
302
+ </div>
303
+ <div class="session-danger-action"><button
304
+ v-if="isDeleteAvailable(activeSession)"
305
+ class="btn-session-delete has-label"
306
+ @click="deleteSession(activeSession)"
307
+ :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]"
308
+ :title="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))"
309
+ :aria-label="(activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash'))">
310
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 4 4 4 13 4"/><path d="M5.5 4V2.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1V4"/><path d="M12 4v9.5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 13.5V4"/></svg>
311
+ <span class="session-action-label">{{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? t('sessions.preview.deleting') : t('sessions.preview.moving')) : (sessionTrashEnabled === false ? t('sessions.preview.deleteHard') : t('sessions.preview.moveToTrash')) }}</span>
312
+ </button></div>
300
313
  </div>
301
314
  </div>
315
+ <div class="session-export-feedback">
316
+ <small id="session-export-scope">{{ t('sessions.export.scopeHint') }}</small>
317
+ <p v-if="sessionExportNotice && sessionExportNotice.key === getSessionExportKey(activeSession)" class="session-export-notice" role="status">{{ t('sessions.export.truncated') }}</p>
318
+ </div>
302
319
 
303
320
  <div v-if="sessionDetailLoading && !sessionPreviewLoadingMore" class="session-preview-empty">
304
321
  {{ t('sessions.preview.loadingBody') }}
@@ -79,6 +79,12 @@
79
79
  @click="setTheme('dark')">
80
80
  {{ t('settings.theme.dark') }}
81
81
  </button>
82
+ <button
83
+ type="button"
84
+ :class="['toggle-btn', { active: theme === 'ocean' }]"
85
+ @click="setTheme('ocean')">
86
+ {{ t('settings.theme.ocean') }}
87
+ </button>
82
88
  </div>
83
89
  <p class="settings-card-hint">{{ t('settings.theme.hint') }}</p>
84
90
  </div>