codexmate 0.1.27 → 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 (48) 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/config-bootstrap.js +2 -9
  5. package/cli/session-usage.js +2 -2
  6. package/cli.js +407 -40
  7. package/package.json +1 -1
  8. package/web-ui/app.js +7 -0
  9. package/web-ui/index.html +12 -0
  10. package/web-ui/logic.sessions.mjs +6 -4
  11. package/web-ui/modules/app.computed.session.mjs +52 -33
  12. package/web-ui/modules/app.methods.agents.mjs +36 -0
  13. package/web-ui/modules/app.methods.codex-config.mjs +3 -1
  14. package/web-ui/modules/app.methods.index.mjs +2 -0
  15. package/web-ui/modules/app.methods.providers.mjs +12 -4
  16. package/web-ui/modules/app.methods.session-actions.mjs +10 -8
  17. package/web-ui/modules/app.methods.session-browser.mjs +3 -1
  18. package/web-ui/modules/app.methods.session-trash.mjs +16 -16
  19. package/web-ui/modules/app.methods.theme.mjs +39 -0
  20. package/web-ui/modules/app.methods.web-ui-preferences.mjs +9 -0
  21. package/web-ui/modules/i18n/locales/en.mjs +80 -2
  22. package/web-ui/modules/i18n/locales/ja.mjs +80 -2
  23. package/web-ui/modules/i18n/locales/vi.mjs +80 -2
  24. package/web-ui/modules/i18n/locales/zh-tw.mjs +80 -2
  25. package/web-ui/modules/i18n/locales/zh.mjs +80 -2
  26. package/web-ui/modules/i18n.mjs +7 -0
  27. package/web-ui/partials/index/modal-config-template-agents.html +1 -1
  28. package/web-ui/partials/index/modal-health-check.html +1 -1
  29. package/web-ui/partials/index/modal-openclaw-config.html +50 -50
  30. package/web-ui/partials/index/panel-dashboard.html +1 -1
  31. package/web-ui/partials/index/panel-prompts.html +2 -2
  32. package/web-ui/partials/index/panel-sessions.html +47 -30
  33. package/web-ui/partials/index/panel-settings.html +30 -0
  34. package/web-ui/res/web-ui-render.precompiled.js +244 -157
  35. package/web-ui/session-helpers.mjs +3 -0
  36. package/web-ui/styles/base-theme.css +3 -2
  37. package/web-ui/styles/bridge-pool.css +1 -1
  38. package/web-ui/styles/controls-forms.css +1 -1
  39. package/web-ui/styles/layout-shell.css +0 -1
  40. package/web-ui/styles/sessions-preview.css +77 -0
  41. package/web-ui/styles/sessions-toolbar-trash.css +1 -1
  42. package/web-ui/styles/sessions-usage.css +1 -1
  43. package/web-ui/styles/settings-panel.css +2 -2
  44. package/web-ui/styles/theme-dark-tokens.css +83 -0
  45. package/web-ui/styles/theme-dark.css +1355 -0
  46. package/web-ui/styles/theme-ocean-tokens.css +88 -0
  47. package/web-ui/styles/theme-ocean.css +920 -0
  48. package/web-ui/styles.css +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.27",
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: {},
@@ -575,6 +578,7 @@ document.addEventListener('DOMContentLoaded', () => {
575
578
  kilocodeAutoSaveSignature: '',
576
579
  forceCompactLayout: false,
577
580
  sidebarCollapsed: false,
581
+ theme: 'light',
578
582
  sessionLoadNativeDialog: false,
579
583
  starPrompted: false,
580
584
  webhookConfig: { enabled: false, url: '', events: ['provider-switch', 'claude-md-edit'] },
@@ -604,6 +608,9 @@ document.addEventListener('DOMContentLoaded', () => {
604
608
  if (typeof this.initI18n === 'function') {
605
609
  this.initI18n();
606
610
  }
611
+ if (typeof this.applyDocumentTheme === 'function') {
612
+ this.applyDocumentTheme();
613
+ }
607
614
  if (typeof this.loadWebhookSettings === 'function') {
608
615
  this.loadWebhookSettings();
609
616
  }
package/web-ui/index.html CHANGED
@@ -11,6 +11,18 @@
11
11
  <script src="/res/marked.min.js"></script>
12
12
  <script src="/res/purify.min.js"></script>
13
13
  <link rel="stylesheet" href="/web-ui/styles.css">
14
+ <script>
15
+ // Render hint: skip the light flash for dark-theme users before the
16
+ // server preference store loads. Kept in sync by applyDocumentTheme().
17
+ (function () {
18
+ try {
19
+ var cachedTheme = localStorage.getItem('codexmateThemeCache');
20
+ if (cachedTheme === 'dark' || cachedTheme === 'ocean') {
21
+ document.documentElement.setAttribute('data-theme', cachedTheme);
22
+ }
23
+ } catch (_) {}
24
+ })();
25
+ </script>
14
26
  </head>
15
27
  <body>
16
28
  <!-- @include ./partials/index/layout-header.html -->
@@ -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');
@@ -38,6 +38,7 @@ import { createPluginsMethods } from './plugins.methods.mjs';
38
38
  import { createI18nMethods } from './i18n.mjs';
39
39
  import { createWebhookMethods } from './app.methods.webhook.mjs';
40
40
  import { createWebUiPreferencesMethods } from './app.methods.web-ui-preferences.mjs';
41
+ import { createThemeMethods } from './app.methods.theme.mjs';
41
42
  import {
42
43
  CONFIG_MODE_SET,
43
44
  getProviderConfigModeMeta
@@ -92,6 +93,7 @@ export function createAppMethods() {
92
93
  ...createProvidersMethods({ api }),
93
94
  ...createProviderCacheMethods({ api }),
94
95
  ...createWebUiPreferencesMethods({ api }),
96
+ ...createThemeMethods(),
95
97
  ...createClaudeConfigMethods({ api }),
96
98
  ...createToolConfigPermissionMethods({ api }),
97
99
  ...createOpenclawCoreMethods(),
@@ -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
  }
@@ -0,0 +1,39 @@
1
+ const THEME_STORAGE_CACHE_KEY = 'codexmateThemeCache';
2
+
3
+ export function createThemeMethods() {
4
+ return {
5
+ normalizeTheme(value) {
6
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
7
+ return normalized === 'dark' || normalized === 'ocean' ? normalized : 'light';
8
+ },
9
+ applyDocumentTheme() {
10
+ try {
11
+ if (typeof document === 'undefined' || !document.documentElement) return;
12
+ const next = this.normalizeTheme(this.theme);
13
+ if (next === 'dark' || next === 'ocean') {
14
+ document.documentElement.setAttribute('data-theme', next);
15
+ } else {
16
+ document.documentElement.removeAttribute('data-theme');
17
+ }
18
+ // Render hint only: lets the inline head script skip the light
19
+ // flash before server preferences arrive. Source of truth is
20
+ // the server-side webUi preference store.
21
+ const storage = typeof globalThis !== 'undefined' ? globalThis.localStorage : null;
22
+ if (storage && typeof storage.setItem === 'function' && typeof storage.removeItem === 'function') {
23
+ if (next === 'dark' || next === 'ocean') storage.setItem(THEME_STORAGE_CACHE_KEY, next);
24
+ else storage.removeItem(THEME_STORAGE_CACHE_KEY);
25
+ }
26
+ } catch (_) {}
27
+ },
28
+ setTheme(theme) {
29
+ const next = this.normalizeTheme(theme);
30
+ if (next !== this.theme) {
31
+ this.theme = next;
32
+ this.applyDocumentTheme();
33
+ }
34
+ if (typeof this.persistWebUiPreferences === 'function') {
35
+ this.persistWebUiPreferences({ theme: next });
36
+ }
37
+ }
38
+ };
39
+ }
@@ -352,6 +352,9 @@ export function createWebUiPreferencesMethods(options = {}) {
352
352
  ? source.projectClaudeMdPath
353
353
  : (typeof this.projectClaudeMdPath === 'string' ? this.projectClaudeMdPath : ''),
354
354
  sidebarCollapsed: normalizeBoolean(hasOwn(source, 'sidebarCollapsed') ? source.sidebarCollapsed : this.sidebarCollapsed, false),
355
+ theme: typeof this.normalizeTheme === 'function'
356
+ ? this.normalizeTheme(hasOwn(source, 'theme') ? source.theme : this.theme)
357
+ : 'light',
355
358
  configModeVisibility: typeof this.normalizeConfigModeVisibility === 'function'
356
359
  ? this.normalizeConfigModeVisibility(hasOwn(source, 'configModeVisibility') ? source.configModeVisibility : this.configModeVisibility)
357
360
  : { codex: true, claude: true, openclaw: true, opencode: true, kilocode: true },
@@ -423,6 +426,12 @@ export function createWebUiPreferencesMethods(options = {}) {
423
426
  if (hasOwn(source, 'sidebarCollapsed')) {
424
427
  this.sidebarCollapsed = normalizeBoolean(source.sidebarCollapsed, false);
425
428
  }
429
+ if (typeof source.theme === 'string') {
430
+ this.theme = typeof this.normalizeTheme === 'function'
431
+ ? this.normalizeTheme(source.theme)
432
+ : 'light';
433
+ if (typeof this.applyDocumentTheme === 'function') this.applyDocumentTheme();
434
+ }
426
435
  if (hasOwn(source, 'configModeVisibility') && typeof this.normalizeConfigModeVisibility === 'function') {
427
436
  this.configModeVisibility = this.normalizeConfigModeVisibility(source.configModeVisibility);
428
437
  }