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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -333,6 +333,9 @@ document.addEventListener('DOMContentLoaded', () => {
333
333
  opencode: 0
334
334
  },
335
335
  sessionExporting: {},
336
+ sessionExportRange: 'default',
337
+ sessionExportNotice: null,
338
+ sessionSearchInfo: null,
336
339
  sessionConverting: {},
337
340
  sessionImportingNative: {},
338
341
  sessionCloning: {},
package/web-ui/index.html CHANGED
@@ -16,8 +16,9 @@
16
16
  // server preference store loads. Kept in sync by applyDocumentTheme().
17
17
  (function () {
18
18
  try {
19
- if (localStorage.getItem('codexmateThemeCache') === 'dark') {
20
- document.documentElement.setAttribute('data-theme', 'dark');
19
+ var cachedTheme = localStorage.getItem('codexmateThemeCache');
20
+ if (cachedTheme === 'dark' || cachedTheme === 'ocean') {
21
+ document.documentElement.setAttribute('data-theme', cachedTheme);
21
22
  }
22
23
  } catch (_) {}
23
24
  })();
@@ -1,3 +1,4 @@
1
+ export const DEFAULT_SESSION_CONTENT_SCAN_LIMIT = 50;
1
2
  export const DEFAULT_SESSION_LIST_LIMIT = 200;
2
3
  export const DEFAULT_SESSION_LIST_FAST_LIMIT = 20;
3
4
 
@@ -29,14 +30,14 @@ function shouldUseFastSessionBrowseLimit(options = {}) {
29
30
 
30
31
  export function isSessionQueryEnabled(source) {
31
32
  const normalized = normalizeSessionSource(source, '');
32
- return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'all';
33
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'zed' || normalized === 'all';
33
34
  }
34
35
 
35
36
  export function normalizeSessionSource(source, fallback = 'all') {
36
37
  const normalized = typeof source === 'string'
37
38
  ? source.trim().toLowerCase()
38
39
  : '';
39
- if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'all') {
40
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'zed' || normalized === 'all') {
40
41
  return normalized;
41
42
  }
42
43
  return fallback;
@@ -64,10 +65,11 @@ function getSessionSourceLabel(source) {
64
65
  if (source === 'workbuddy') return 'WorkBuddy AI';
65
66
  if (source === 'pi') return 'Pi';
66
67
  if (source === 'opencode') return 'OpenCode';
68
+ if (source === 'zed') return 'Zed';
67
69
  return 'Codex';
68
70
  }
69
71
 
70
- const SESSION_USAGE_SOURCE_ORDER = ['codex', 'claude', 'gemini', 'codebuddy', 'workbuddy', 'pi', 'opencode'];
72
+ const SESSION_USAGE_SOURCE_ORDER = ['codex', 'claude', 'gemini', 'codebuddy', 'workbuddy', 'pi', 'opencode', 'zed'];
71
73
 
72
74
  function collectSessionModelNames(session) {
73
75
  if (!session || typeof session !== 'object') {
@@ -154,7 +156,7 @@ export function buildSessionListParams(options = {}) {
154
156
  query: queryValue,
155
157
  queryMode: 'and',
156
158
  queryScope: 'content',
157
- contentScanLimit: 50,
159
+ contentScanLimit: DEFAULT_SESSION_CONTENT_SCAN_LIMIT,
158
160
  roleFilter,
159
161
  timeRangePreset,
160
162
  limit,
@@ -250,48 +250,65 @@ export function buildSessionWorkspaceSummary(session, messages, options = {}) {
250
250
  return summary;
251
251
  }
252
252
 
253
+ const DURATION_UNIT_TABLES = {
254
+ en: {
255
+ zero: '0m',
256
+ underOne: '<1m',
257
+ spaced: true,
258
+ spaceCompact: false,
259
+ compact: [['d', 1440], ['h', 60], ['m', 1]],
260
+ long: [['day', 1440], ['hr', 60], ['min', 1]]
261
+ },
262
+ ja: {
263
+ zero: '0分',
264
+ underOne: '<1分',
265
+ spaced: false,
266
+ spaceCompact: false,
267
+ compact: [['日', 1440], ['時間', 60], ['分', 1]],
268
+ long: [['日', 1440], ['時間', 60], ['分', 1]]
269
+ },
270
+ vi: {
271
+ zero: '0 phút',
272
+ underOne: '<1 phút',
273
+ spaced: true,
274
+ spaceCompact: true,
275
+ compact: [['ngày', 1440], ['giờ', 60], ['phút', 1]],
276
+ long: [['ngày', 1440], ['giờ', 60], ['phút', 1]]
277
+ },
278
+ zh: {
279
+ zero: '0分',
280
+ underOne: '<1分',
281
+ spaced: false,
282
+ spaceCompact: false,
283
+ compact: [['天', 1440], ['时', 60], ['分', 1]],
284
+ long: [['天', 1440], ['小时', 60], ['分', 1]]
285
+ },
286
+ 'zh-tw': {
287
+ zero: '0分',
288
+ underOne: '<1分',
289
+ spaced: false,
290
+ spaceCompact: false,
291
+ compact: [['天', 1440], ['時', 60], ['分', 1]],
292
+ long: [['天', 1440], ['小時', 60], ['分', 1]]
293
+ }
294
+ };
295
+
253
296
  function formatUsageDuration(value, options = {}) {
254
297
  const normalizedLang = typeof options.lang === 'string' ? options.lang.trim().toLowerCase() : '';
255
- const isEn = normalizedLang === 'en';
298
+ const unitSet = DURATION_UNIT_TABLES[normalizedLang] || DURATION_UNIT_TABLES.zh;
256
299
  const numeric = Number(value);
257
300
  if (!Number.isFinite(numeric) || numeric <= 0) {
258
- return isEn ? '0m' : '0分';
301
+ return unitSet.zero;
259
302
  }
260
303
  const totalMinutes = Math.floor(numeric / 60000);
261
304
  if (totalMinutes <= 0) {
262
- return isEn ? '<1m' : '<1分';
305
+ return unitSet.underOne;
263
306
  }
264
307
  const maxParts = Number.isFinite(Number(options.maxParts))
265
308
  ? Math.max(1, Math.floor(Number(options.maxParts)))
266
309
  : 2;
267
310
  const compact = options.compact !== false;
268
- const units = compact
269
- ? (
270
- isEn
271
- ? [
272
- { label: 'd', value: 24 * 60 },
273
- { label: 'h', value: 60 },
274
- { label: 'm', value: 1 }
275
- ]
276
- : [
277
- { label: '天', value: 24 * 60 },
278
- { label: '时', value: 60 },
279
- { label: '分', value: 1 }
280
- ]
281
- )
282
- : (
283
- isEn
284
- ? [
285
- { label: 'day', value: 24 * 60 },
286
- { label: 'hr', value: 60 },
287
- { label: 'min', value: 1 }
288
- ]
289
- : [
290
- { label: '天', value: 24 * 60 },
291
- { label: '小时', value: 60 },
292
- { label: '分', value: 1 }
293
- ]
294
- );
311
+ const units = (compact ? unitSet.compact : unitSet.long).map(([label, minutes]) => ({ label, value: minutes }));
295
312
  let remainingMinutes = totalMinutes;
296
313
  const parts = [];
297
314
  for (const unit of units) {
@@ -302,13 +319,14 @@ function formatUsageDuration(value, options = {}) {
302
319
  if (count <= 0) {
303
320
  continue;
304
321
  }
305
- parts.push(compact ? `${count}${unit.label}` : (isEn ? `${count} ${unit.label}` : `${count}${unit.label}`));
322
+ parts.push((compact ? unitSet.spaceCompact : unitSet.spaced) ? `${count} ${unit.label}` : `${count}${unit.label}`);
306
323
  remainingMinutes -= count * unit.value;
307
324
  if (parts.length >= maxParts) {
308
325
  break;
309
326
  }
310
327
  }
311
- return parts.length ? parts.join(compact ? '' : ' ') : (isEn ? '0m' : '0分');
328
+ const separator = compact ? (unitSet.spaceCompact ? ' ' : '') : ' ';
329
+ return parts.length ? parts.join(separator) : unitSet.zero;
312
330
  }
313
331
 
314
332
  export function createSessionComputed() {
@@ -325,7 +343,8 @@ export function createSessionComputed() {
325
343
  { value: "codebuddy", label: this.t("sessions.source.codebuddy") },
326
344
  { value: "workbuddy", label: this.t("sessions.source.workbuddy") },
327
345
  { value: "pi", label: this.t("sessions.source.pi") },
328
- { value: "opencode", label: this.t("sessions.source.opencode") }
346
+ { value: "opencode", label: this.t("sessions.source.opencode") },
347
+ { value: "zed", label: this.t("sessions.source.zed") }
329
348
  ];
330
349
  },
331
350
  activeSessionExportKey() {
@@ -252,6 +252,38 @@ export function createAgentsMethods(options = {}) {
252
252
  this.agentsDiffFingerprint = '';
253
253
  this._agentsDiffPreviewRequestToken = null;
254
254
  },
255
+ trapModalFocus(event) {
256
+ const overlays = typeof document !== 'undefined' && typeof document.querySelectorAll === 'function'
257
+ ? Array.from(document.querySelectorAll('.modal-overlay'))
258
+ : [];
259
+ if (!overlays.length) {
260
+ return;
261
+ }
262
+ const modal = overlays[overlays.length - 1];
263
+ const focusableSelector = 'button:not(:disabled), [href], input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
264
+ const focusables = Array.from(modal.querySelectorAll(focusableSelector))
265
+ .filter((el) => el.offsetParent !== null || el === document.activeElement);
266
+ if (!focusables.length) {
267
+ return;
268
+ }
269
+ const first = focusables[0];
270
+ const last = focusables[focusables.length - 1];
271
+ const active = document.activeElement;
272
+ if (!modal.contains(active)) {
273
+ event.preventDefault();
274
+ first.focus();
275
+ return;
276
+ }
277
+ if (event.shiftKey && active === first) {
278
+ event.preventDefault();
279
+ last.focus();
280
+ return;
281
+ }
282
+ if (!event.shiftKey && active === last) {
283
+ event.preventDefault();
284
+ first.focus();
285
+ }
286
+ },
255
287
  handleGlobalKeydown(event) {
256
288
  if (!event) {
257
289
  return;
@@ -293,6 +325,10 @@ export function createAgentsMethods(options = {}) {
293
325
  }
294
326
  return;
295
327
  }
328
+ if (key === 'Tab') {
329
+ this.trapModalFocus(event);
330
+ return;
331
+ }
296
332
  if (key !== 'Escape') {
297
333
  return;
298
334
  }
@@ -75,7 +75,8 @@ export function createCodexConfigMethods(options = {}) {
75
75
  const res = await api('export-session', {
76
76
  source: session.source,
77
77
  sessionId: session.sessionId,
78
- filePath: session.filePath
78
+ filePath: session.filePath,
79
+ ...(this.sessionExportRange === 'all' ? { maxMessages: 'all' } : {})
79
80
  });
80
81
  if (res.error) {
81
82
  this.showMessage(res.error, 'error');
@@ -84,6 +85,7 @@ export function createCodexConfigMethods(options = {}) {
84
85
 
85
86
  const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
86
87
  this.downloadTextFile(fileName, res.content || '');
88
+ this.sessionExportNotice = res.truncated ? { key } : null;
87
89
  if (res.truncated) {
88
90
  const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
89
91
  this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
@@ -479,6 +479,14 @@ export function createProvidersMethods(options = {}) {
479
479
 
480
480
  async resetConfig() {
481
481
  if (this.resetConfigLoading) return;
482
+ const confirmed = await this.requestConfirmDialog({
483
+ title: this.t('settings.reset.confirmTitle'),
484
+ message: this.t('settings.reset.confirmMessage'),
485
+ confirmText: this.t('settings.reset.button'),
486
+ cancelText: this.t('confirm.cancel'),
487
+ danger: true
488
+ });
489
+ if (!confirmed) return;
482
490
  this.resetConfigLoading = true;
483
491
  try {
484
492
  const res = await api('reset-config');
@@ -486,7 +494,7 @@ export function createProvidersMethods(options = {}) {
486
494
  this.showMessage(res.error, 'error');
487
495
  return;
488
496
  }
489
- const backup = res.backupFile ? `(已备份: ${res.backupFile})` : '';
497
+ const backup = res.backupFile ? this.t('toast.provider.resetBackup', { file: res.backupFile }) : '';
490
498
  this.showMessage(this.t('toast.provider.resetSuccess', { backup }), 'success');
491
499
  await this.loadAll();
492
500
  } catch (e) {
@@ -498,7 +506,7 @@ export function createProvidersMethods(options = {}) {
498
506
 
499
507
  async addModel() {
500
508
  if (!this.newModelName || !this.newModelName.trim()) {
501
- return this.showMessage('请输入模型', 'error');
509
+ return this.showMessage(this.t('toast.provider.modelRequired'), 'error');
502
510
  }
503
511
  try {
504
512
  const modelName = this.newModelName.trim();
@@ -523,7 +531,7 @@ export function createProvidersMethods(options = {}) {
523
531
  this.closeModelModal();
524
532
  }
525
533
  } catch (_) {
526
- this.showMessage('新增模型失败', 'error');
534
+ this.showMessage(this.t('toast.provider.addModelFail'), 'error');
527
535
  }
528
536
  },
529
537
 
@@ -546,7 +554,7 @@ export function createProvidersMethods(options = {}) {
546
554
  this.showMessage(this.t('toast.operation.success'), 'success');
547
555
  }
548
556
  } catch (_) {
549
- this.showMessage('删除模型失败', 'error');
557
+ this.showMessage(this.t('toast.provider.deleteModelFail'), 'error');
550
558
  }
551
559
  },
552
560
 
@@ -28,8 +28,8 @@ export function createSessionActionMethods(options = {}) {
28
28
  let error = '';
29
29
  if (!source) {
30
30
  error = '缺少 source 参数';
31
- } else if (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode') {
32
- error = 'source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi 或 opencode';
31
+ } else if (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode' && source !== 'zed') {
32
+ error = 'source 仅支持 codex、claude、gemini、codebuddy、workbuddy、pi、opencode 或 zed';
33
33
  }
34
34
  if (!sessionId && !filePath) {
35
35
  error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
@@ -77,11 +77,13 @@ export function createSessionActionMethods(options = {}) {
77
77
  ? 'Pi'
78
78
  : (context.params.source === 'opencode'
79
79
  ? 'OpenCode'
80
- : (context.params.source === 'codebuddy'
81
- ? 'CodeBuddy Code'
82
- : (context.params.source === 'workbuddy'
83
- ? 'WorkBuddy AI'
84
- : context.params.source))))));
80
+ : (context.params.source === 'zed'
81
+ ? 'Zed'
82
+ : (context.params.source === 'codebuddy'
83
+ ? 'CodeBuddy Code'
84
+ : (context.params.source === 'workbuddy'
85
+ ? 'WorkBuddy AI'
86
+ : context.params.source)))))))
85
87
  this.activeSession = {
86
88
  source: context.params.source,
87
89
  sourceLabel,
@@ -110,7 +112,7 @@ export function createSessionActionMethods(options = {}) {
110
112
  buildSessionStandaloneUrl(session) {
111
113
  if (!session) return '';
112
114
  const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
113
- if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode')) return '';
115
+ if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode' && source !== 'zed')) return '';
114
116
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
115
117
  const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
116
118
  if (!sessionId && !filePath) return '';
@@ -524,7 +524,9 @@ export function createSessionBrowserMethods(options = {}) {
524
524
  ? this.t('sessions.source.pi')
525
525
  : (this.sessionFilterSource === 'opencode'
526
526
  ? this.t('sessions.source.opencode')
527
- : this.sessionFilterSource))))));
527
+ : (this.sessionFilterSource === 'zed'
528
+ ? this.t('sessions.source.zed')
529
+ : this.sessionFilterSource)))))));
528
530
  chips.push({ key: 'source', title: this.t('sessions.filters.source'), value: label });
529
531
  }
530
532
  if (this.sessionPathFilter) {
@@ -292,7 +292,7 @@ export function createSessionTrashMethods(options = {}) {
292
292
  this.sessionTrashCountLoadedOnce = true;
293
293
  } catch (e) {
294
294
  if (this.isLatestSessionTrashCountRequestToken(requestToken) && options.silent !== true) {
295
- this.showMessage('加载回收站数量失败', 'error');
295
+ this.showMessage(this.t('settings.trash.loadCountFail'), 'error');
296
296
  }
297
297
  } finally {
298
298
  this.sessionTrashCountLoading = false;
@@ -365,7 +365,7 @@ export function createSessionTrashMethods(options = {}) {
365
365
  } catch (e) {
366
366
  if (this.isLatestSessionTrashListRequestToken(requestToken)) {
367
367
  this.sessionTrashLastLoadFailed = true;
368
- this.showMessage('加载回收站失败', 'error');
368
+ this.showMessage(this.t('settings.trash.loadFail'), 'error');
369
369
  }
370
370
  } finally {
371
371
  this.sessionTrashLoading = false;
@@ -392,7 +392,7 @@ export function createSessionTrashMethods(options = {}) {
392
392
  this.showMessage(res.error, 'error');
393
393
  return;
394
394
  }
395
- this.showMessage('会话已恢复', 'success');
395
+ this.showMessage(this.t('settings.trash.restored'), 'success');
396
396
  if (typeof this.invalidateSessionsUsageData === 'function') {
397
397
  this.invalidateSessionsUsageData({ preserveList: true });
398
398
  }
@@ -402,7 +402,7 @@ export function createSessionTrashMethods(options = {}) {
402
402
  await this.loadSessions();
403
403
  }
404
404
  } catch (e) {
405
- this.showMessage('恢复失败', 'error');
405
+ this.showMessage(this.t('settings.trash.restoreFail'), 'error');
406
406
  } finally {
407
407
  this.sessionTrashRestoring[key] = false;
408
408
  }
@@ -414,10 +414,10 @@ export function createSessionTrashMethods(options = {}) {
414
414
  return;
415
415
  }
416
416
  const confirmed = await this.requestConfirmDialog({
417
- title: '彻底删除回收站记录',
418
- message: '该会话将从回收站永久删除,且无法恢复。',
419
- confirmText: '彻底删除',
420
- cancelText: '取消',
417
+ title: this.t('settings.trash.purgeConfirmTitle'),
418
+ message: this.t('settings.trash.purgeConfirmMessage'),
419
+ confirmText: this.t('settings.trash.purge'),
420
+ cancelText: this.t('confirm.cancel'),
421
421
  danger: true
422
422
  });
423
423
  if (!confirmed) {
@@ -430,11 +430,11 @@ export function createSessionTrashMethods(options = {}) {
430
430
  this.showMessage(res.error, 'error');
431
431
  return;
432
432
  }
433
- this.showMessage('已彻底删除', 'success');
433
+ this.showMessage(this.t('settings.trash.purged'), 'success');
434
434
  this.invalidateSessionTrashRequests();
435
435
  await this.loadSessionTrash({ forceRefresh: true });
436
436
  } catch (e) {
437
- this.showMessage('彻底删除失败', 'error');
437
+ this.showMessage(this.t('settings.trash.purgeFail'), 'error');
438
438
  } finally {
439
439
  this.sessionTrashPurging[key] = false;
440
440
  }
@@ -446,10 +446,10 @@ export function createSessionTrashMethods(options = {}) {
446
446
  return;
447
447
  }
448
448
  const confirmed = await this.requestConfirmDialog({
449
- title: '清空回收站',
450
- message: '该操作会永久删除回收站中的全部会话,且无法恢复。',
451
- confirmText: '全部清空',
452
- cancelText: '取消',
449
+ title: this.t('settings.trash.clearConfirmTitle'),
450
+ message: this.t('settings.trash.clearConfirmMessage'),
451
+ confirmText: this.t('settings.trash.clearConfirmButton'),
452
+ cancelText: this.t('confirm.cancel'),
453
453
  danger: true
454
454
  });
455
455
  if (!confirmed) {
@@ -462,11 +462,11 @@ export function createSessionTrashMethods(options = {}) {
462
462
  this.showMessage(res.error, 'error');
463
463
  return;
464
464
  }
465
- this.showMessage('回收站已清空', 'success');
465
+ this.showMessage(this.t('settings.trash.cleared'), 'success');
466
466
  this.invalidateSessionTrashRequests();
467
467
  await this.loadSessionTrash({ forceRefresh: true });
468
468
  } catch (e) {
469
- this.showMessage('清空回收站失败', 'error');
469
+ this.showMessage(this.t('settings.trash.clearFail'), 'error');
470
470
  } finally {
471
471
  this.sessionTrashClearing = false;
472
472
  }
@@ -3,14 +3,15 @@ const THEME_STORAGE_CACHE_KEY = 'codexmateThemeCache';
3
3
  export function createThemeMethods() {
4
4
  return {
5
5
  normalizeTheme(value) {
6
- return value === 'dark' ? 'dark' : 'light';
6
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
7
+ return normalized === 'dark' || normalized === 'ocean' ? normalized : 'light';
7
8
  },
8
9
  applyDocumentTheme() {
9
10
  try {
10
11
  if (typeof document === 'undefined' || !document.documentElement) return;
11
12
  const next = this.normalizeTheme(this.theme);
12
- if (next === 'dark') {
13
- document.documentElement.setAttribute('data-theme', 'dark');
13
+ if (next === 'dark' || next === 'ocean') {
14
+ document.documentElement.setAttribute('data-theme', next);
14
15
  } else {
15
16
  document.documentElement.removeAttribute('data-theme');
16
17
  }
@@ -19,7 +20,7 @@ export function createThemeMethods() {
19
20
  // the server-side webUi preference store.
20
21
  const storage = typeof globalThis !== 'undefined' ? globalThis.localStorage : null;
21
22
  if (storage && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function') {
22
- if (next === 'dark') storage.setItem(THEME_STORAGE_CACHE_KEY, 'dark');
23
+ if (next === 'dark' || next === 'ocean') storage.setItem(THEME_STORAGE_CACHE_KEY, next);
23
24
  else storage.removeItem(THEME_STORAGE_CACHE_KEY);
24
25
  }
25
26
  } catch (_) {}
@@ -520,6 +520,11 @@ const en = Object.freeze({
520
520
  'toast.provider.updateFail': 'Failed to update',
521
521
  'toast.provider.resetSuccess': 'Config reset{backup}',
522
522
  'toast.provider.resetFail': 'Reset failed',
523
+
524
+ 'toast.provider.resetBackup': ' (backup: {file})',
525
+ 'toast.provider.modelRequired': 'Please enter model',
526
+ 'toast.provider.addModelFail': 'Failed to add model',
527
+ 'toast.provider.deleteModelFail': 'Failed to delete model',
523
528
  'toast.provider.fieldsRequired': 'Name, URL, API Key and model are required',
524
529
  'toast.provider.urlRequired': 'URL is required',
525
530
  'toast.claude.modelRequired': 'Please enter model',
@@ -690,12 +695,13 @@ const en = Object.freeze({
690
695
  'sessions.source.workbuddy': 'WorkBuddy AI',
691
696
  'sessions.source.pi': 'Pi',
692
697
  'sessions.source.opencode': 'OpenCode',
698
+ 'sessions.source.zed': 'Zed',
693
699
  'sessions.loadingList': 'Loading sessions...',
694
700
  'sessions.empty': 'No sessions found',
695
701
  'sessions.unknownTime': 'unknown time',
696
702
 
697
703
 
698
- 'sessions.query.placeholder.enabled': 'Search keywords (Codex/Claude/Gemini/CodeBuddy, e.g. claude code)',
704
+ 'sessions.query.placeholder.enabled': "Search keywords (e.g. claude code)",
699
705
  'sessions.query.placeholder.disabled': 'Keyword search is not available for this source',
700
706
  'sessions.pin': 'Pin',
701
707
  'sessions.unpin': 'Unpin',
@@ -721,6 +727,17 @@ const en = Object.freeze({
721
727
  'sessions.preview.moveToTrash': 'Move to trash',
722
728
  'sessions.preview.deleting': 'Deleting...',
723
729
  'sessions.preview.moving': 'Moving...',
730
+ 'sessions.export.range': "Export range",
731
+ 'sessions.export.default': "Latest 1000 messages",
732
+ 'sessions.export.all': "All extractable messages",
733
+ 'sessions.export.scopeHint': "All removes the message limit; it is not a complete state backup.",
734
+ 'sessions.export.truncated': "This export was truncated. Choose all and export again for earlier messages.",
735
+ 'sessions.search.coverage': "Scanned {scanned} / {candidates} candidate sessions",
736
+ 'sessions.search.partial': "Some candidates remain unscanned. Narrow the source or project.",
737
+ 'sessions.search.scopeHint': "Covers this candidate set only, not all historical records.",
738
+ 'sessions.empty.filtered': "No records match the current filters.",
739
+ 'sessions.empty.source': "No displayable records found for this source. Check local sessions and refresh.",
740
+ 'sessions.empty.clearFilters': "Clear filters",
724
741
  'sessions.preview.export': 'Export',
725
742
  'sessions.preview.exporting': 'Exporting...',
726
743
  'sessions.preview.convert': 'Create derived',
@@ -1137,6 +1154,7 @@ const en = Object.freeze({
1137
1154
  'settings.theme.meta': 'Choose the Web UI color theme',
1138
1155
  'settings.theme.light': 'Light',
1139
1156
  'settings.theme.dark': 'Dark',
1157
+ 'settings.theme.ocean': 'Ocean',
1140
1158
  'settings.theme.hint': 'Theme preference is saved automatically and persists across reloads.',
1141
1159
  'settings.sharePrefix.title': 'Share command prefix',
1142
1160
  'settings.sharePrefix.meta': 'Used as the prefix for “Copy share command” in the Web UI',
@@ -1213,6 +1231,20 @@ const en = Object.freeze({
1213
1231
  'settings.trash.retentionLabel': 'Retention days',
1214
1232
  'settings.trash.retentionUnit': 'days',
1215
1233
  'settings.trash.retentionHint': 'Range 1-365 days, default 30. Expired entries are purged on each trash load.',
1234
+
1235
+ 'settings.trash.loadCountFail': 'Failed to load trash count',
1236
+ 'settings.trash.loadFail': 'Failed to load trash',
1237
+ 'settings.trash.restored': 'Session restored',
1238
+ 'settings.trash.restoreFail': 'Restore failed',
1239
+ 'settings.trash.purged': 'Purged',
1240
+ 'settings.trash.purgeFail': 'Purge failed',
1241
+ 'settings.trash.cleared': 'Trash emptied',
1242
+ 'settings.trash.clearFail': 'Failed to empty trash',
1243
+ 'settings.trash.purgeConfirmTitle': 'Purge trash entry',
1244
+ 'settings.trash.purgeConfirmMessage': 'This session will be permanently deleted from the trash and cannot be restored.',
1245
+ 'settings.trash.clearConfirmTitle': 'Empty trash',
1246
+ 'settings.trash.clearConfirmMessage': 'All sessions in the trash will be permanently deleted and cannot be restored.',
1247
+ 'settings.trash.clearConfirmButton': 'Empty all',
1216
1248
  'settings.trash.count': '{count} item(s)',
1217
1249
  'settings.trash.retentionShort': 'Kept {days} days',
1218
1250
  'settings.trash.disabled': 'Trash is disabled',
@@ -1253,6 +1285,9 @@ const en = Object.freeze({
1253
1285
  'settings.reset.button': 'Reset config',
1254
1286
  'settings.reset.loading': 'Resetting...',
1255
1287
 
1288
+ 'settings.reset.confirmTitle': 'Confirm config reset',
1289
+ 'settings.reset.confirmMessage': 'This will back up config.toml, then overwrite the current config with defaults. Continue?',
1290
+
1256
1291
  // Market (Skills)
1257
1292
  'market.title': 'Skills Overview',
1258
1293
  'market.subtitle': 'Switch target and inspect local skills.',
@@ -1363,6 +1398,44 @@ const en = Object.freeze({
1363
1398
  'modal.openclaw.quick.subtitle': '3 steps: fill provider/model, write to editor, save & apply.',
1364
1399
  'modal.openclaw.quick.step2': 'Write to editor',
1365
1400
  'modal.openclaw.structured.writeHint': 'Writing to editor may reformat JSON and drop comments.',
1401
+
1402
+ 'modal.openclaw.next': 'Next →',
1403
+ 'modal.openclaw.prev': '← Previous',
1404
+ 'modal.openclaw.finish': 'Finish ✓',
1405
+ 'modal.openclaw.quick.advancedOptions': 'Advanced options',
1406
+ 'modal.openclaw.quick.providerNamePlaceholder': 'e.g. custom-myapi',
1407
+ 'modal.openclaw.quick.modelIdHint': 'Required, e.g. gpt-4',
1408
+ 'modal.openclaw.quick.modelNameHint': 'Optional, used for display',
1409
+ 'modal.openclaw.structured.title': 'Structured config (advanced)',
1410
+ 'modal.openclaw.structured.primaryModel': 'Primary model',
1411
+ 'modal.openclaw.structured.fallbackModels': 'Fallback models',
1412
+ 'modal.openclaw.structured.providerModelPlaceholder': 'e.g. provider/model',
1413
+ 'modal.openclaw.structured.addFallback': 'Add fallback',
1414
+ 'modal.openclaw.structured.workspacePlaceholder': 'e.g. ~/.openclaw/workspace',
1415
+ 'modal.openclaw.structured.timeoutPlaceholder': 'e.g. 600',
1416
+ 'modal.openclaw.structured.contextTokensPlaceholder': 'e.g. 4096',
1417
+ 'modal.openclaw.structured.maxConcurrentPlaceholder': 'e.g. 2',
1418
+ 'modal.openclaw.structured.envVars': 'Environment variables',
1419
+ 'modal.openclaw.structured.addEnvVar': 'Add variable',
1420
+ 'modal.openclaw.structured.addAllow': 'Add allow',
1421
+ 'modal.openclaw.structured.addDeny': 'Add deny',
1422
+ 'modal.openclaw.structured.allowPlaceholder': 'e.g. fs.read*',
1423
+ 'modal.openclaw.structured.denyPlaceholder': 'e.g. net.*',
1424
+ 'modal.openclaw.structured.providersTitle': 'Providers (read-only)',
1425
+ 'modal.openclaw.structured.noProviders': 'No providers config found (may use environment variables or external config).',
1426
+ 'modal.openclaw.structured.sourceLabel': 'Source',
1427
+ 'modal.openclaw.structured.active': 'Active',
1428
+ 'modal.openclaw.structured.noFields': 'No fields configured',
1429
+ 'modal.openclaw.structured.missingProviders': 'Active providers not shown in config: {list}.',
1430
+ 'modal.openclaw.structured.agentsTitle': 'Agents (read-only)',
1431
+ 'modal.openclaw.structured.noAgents': 'No agents.list config found.',
1432
+ 'modal.openclaw.structured.themeLabel': 'Theme',
1433
+ 'modal.openclaw.structured.emojiLabel': 'Emoji',
1434
+ 'modal.openclaw.structured.avatarLabel': 'Avatar',
1435
+ 'modal.openclaw.editorLabel': 'OpenClaw config (JSON5)',
1436
+ 'modal.openclaw.editorPlaceholder': 'Edit OpenClaw config here (JSON5)',
1437
+ 'modal.openclaw.defaultConfigHint': 'The default config always maps to the current openclaw.json. Use "Save & apply" directly.',
1438
+ 'modal.openclaw.saveOnlyHint': 'Saving only writes to the local config library. Click "Save & apply" to write openclaw.json.',
1366
1439
  'openclaw.action.applyAria': 'Apply OpenClaw config: {name}',
1367
1440
  'openclaw.action.editAria': 'Edit OpenClaw config: {name}',
1368
1441
  'openclaw.action.deleteAria': 'Delete OpenClaw config: {name}'