codexmate 0.1.25 → 0.1.28

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/cli/config-bootstrap.js +2 -9
  2. package/cli/session-usage.js +10 -4
  3. package/cli.js +772 -225
  4. package/lib/cli-sessions.js +1 -1
  5. package/package.json +3 -3
  6. package/tools/dev/launch.js +147 -0
  7. package/web-ui/app.js +17 -3
  8. package/web-ui/index.html +12 -1
  9. package/web-ui/logic.prompts-token-count.mjs +49 -0
  10. package/web-ui/logic.sessions.mjs +30 -15
  11. package/web-ui/modules/app.computed.dashboard.mjs +1 -0
  12. package/web-ui/modules/app.computed.prompts.mjs +18 -0
  13. package/web-ui/modules/app.computed.session.mjs +3 -1
  14. package/web-ui/modules/app.methods.index.mjs +2 -0
  15. package/web-ui/modules/app.methods.session-actions.mjs +10 -4
  16. package/web-ui/modules/app.methods.session-browser.mjs +34 -45
  17. package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
  18. package/web-ui/modules/app.methods.theme.mjs +38 -0
  19. package/web-ui/modules/app.methods.web-ui-preferences.mjs +9 -0
  20. package/web-ui/modules/i18n/locales/en.mjs +15 -2
  21. package/web-ui/modules/i18n/locales/ja.mjs +15 -2
  22. package/web-ui/modules/i18n/locales/vi.mjs +15 -2
  23. package/web-ui/modules/i18n/locales/zh-tw.mjs +15 -2
  24. package/web-ui/modules/i18n/locales/zh.mjs +15 -2
  25. package/web-ui/partials/index/layout-header.html +4 -2
  26. package/web-ui/partials/index/panel-prompts.html +14 -0
  27. package/web-ui/partials/index/panel-sessions.html +21 -6
  28. package/web-ui/partials/index/panel-settings.html +24 -0
  29. package/web-ui/res/web-ui-render.precompiled.js +115 -23
  30. package/web-ui/session-helpers.mjs +1 -1
  31. package/web-ui/styles/base-theme.css +25 -0
  32. package/web-ui/styles/controls-forms.css +1 -1
  33. package/web-ui/styles/modals-core.css +48 -0
  34. package/web-ui/styles/sessions-list.css +1 -1
  35. package/web-ui/styles/sessions-preview.css +56 -7
  36. package/web-ui/styles/sessions-toolbar-trash.css +2 -8
  37. package/web-ui/styles/sessions-usage.css +1 -1
  38. package/web-ui/styles/settings-panel.css +2 -2
  39. package/web-ui/styles/theme-dark-tokens.css +83 -0
  40. package/web-ui/styles/theme-dark.css +1355 -0
  41. package/web-ui/styles.css +1 -0
@@ -192,7 +192,7 @@ function extractMessageFromRecord(record, source) {
192
192
  }
193
193
  return null;
194
194
  }
195
- if (source === 'codebuddy') {
195
+ if (source === 'codebuddy' || source === 'workbuddy') {
196
196
  if (record.type === 'message') {
197
197
  const role = normalizeRole(record.role);
198
198
  const content = record.message ? record.message.content : record.content;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.25",
3
+ "version": "0.1.28",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
7
- "codexmate": "./cli.js"
7
+ "codexmate": "./tools/dev/launch.js"
8
8
  },
9
9
  "files": [
10
10
  "cli.js",
@@ -26,7 +26,7 @@
26
26
  "./res/*": "./web-ui/res/*"
27
27
  },
28
28
  "scripts": {
29
- "dev": "node cli.js run",
29
+ "dev": "node tools/dev/launch.js run",
30
30
  "start": "node cli.js",
31
31
  "reset": "node tools/dev/reset-main.js",
32
32
  "release:npm": "node tools/release/publish-npm.js",
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * codexmate 启动包装器(launcher)
5
+ * 作用:以 spawn 方式原样转发 `node cli.js <argv>`;
6
+ * 仅当子进程因「package.json 中已声明的依赖缺失」(Cannot find module /
7
+ * MODULE_NOT_FOUND)而失败时,在包根目录自动执行一次 npm install,
8
+ * 然后重跑原命令一次;其余任何错误原样透传退出码,行为与直跑 cli.js 一致。
9
+ * 本文件必须保持零第三方依赖(它要能跑在 npm install 之前)。
10
+ */
11
+ const { spawn } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
16
+ const CLI_ENTRY = path.join(PACKAGE_ROOT, 'cli.js');
17
+ const RETRY_FLAG = 'CODEXMATE_LAUNCH_RETRIED';
18
+ const STDERR_KEEP_BYTES = 128 * 1024;
19
+ // 匹配 "Cannot find module 'X'" / "Cannot find module \"X\"" / MODULE_NOT_FOUND 路径
20
+ const MISSING_MODULE_RE = /(?:Cannot find module|MODULE_NOT_FOUND)(?:[^\S\r\n]*['"]([^'"\r\n]+)['"])?/;
21
+
22
+ function isWindows() {
23
+ return process.platform === 'win32';
24
+ }
25
+
26
+ function forwardSignalsTo(child) {
27
+ const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
28
+ signals.forEach(function (sig) {
29
+ process.on(sig, function () {
30
+ try { child.kill(sig); } catch (e) { /* 子进程可能已退出 */ }
31
+ });
32
+ });
33
+ }
34
+
35
+ function extractMissingPackageName(stderrText) {
36
+ const m = MISSING_MODULE_RE.exec(stderrText);
37
+ if (!m || !m[1]) return null;
38
+ const raw = m[1];
39
+ // 相对/绝对路径的 require 失败属于代码 bug,不属于环境缺依赖,不触发重试
40
+ if (raw.startsWith('.') || raw.startsWith('/') || path.isAbsolute(raw)) return null;
41
+ const parts = raw.split('/');
42
+ // scoped 包(@scope/name)取前两段,普通包取第一段
43
+ const pkgName = raw.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
44
+ return pkgName;
45
+ }
46
+
47
+ function isDeclaredDependency(pkgName) {
48
+ try {
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
50
+ const deps = Object.assign({}, pkg.dependencies, pkg.devDependencies);
51
+ return Object.prototype.hasOwnProperty.call(deps, pkgName);
52
+ } catch (e) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function runNpmInstall(cb) {
58
+ const child = spawn('npm', ['install'], {
59
+ cwd: PACKAGE_ROOT,
60
+ stdio: 'inherit',
61
+ shell: isWindows()
62
+ });
63
+ forwardSignalsTo(child);
64
+ child.on('error', function (err) {
65
+ process.stderr.write('[codexmate-launch] npm install 无法启动: ' + err.message + '\n');
66
+ cb(false);
67
+ });
68
+ child.on('exit', function (code) {
69
+ cb(code === 0);
70
+ });
71
+ }
72
+
73
+ function spawnCli(args, opts, onExit) {
74
+ const pipeStderr = !!opts.pipeStderr;
75
+ const child = spawn(process.execPath, args, {
76
+ cwd: process.cwd(),
77
+ env: opts.env || process.env,
78
+ stdio: ['inherit', 'inherit', pipeStderr ? 'pipe' : 'inherit']
79
+ });
80
+ let stderrText = '';
81
+ if (pipeStderr) {
82
+ child.stderr.on('data', function (chunk) {
83
+ process.stderr.write(chunk); // 实时原样转发,绝不静默吞掉
84
+ stderrText += chunk;
85
+ if (stderrText.length > STDERR_KEEP_BYTES) {
86
+ stderrText = stderrText.slice(-STDERR_KEEP_BYTES);
87
+ }
88
+ });
89
+ }
90
+ forwardSignalsTo(child);
91
+ child.on('error', function (err) {
92
+ process.stderr.write('[codexmate-launch] 无法启动 cli.js: ' + err.message + '\n');
93
+ process.exit(127);
94
+ });
95
+ child.on('exit', function (code, signal) {
96
+ onExit(code, signal, stderrText);
97
+ });
98
+ return child;
99
+ }
100
+
101
+ function runFinal(args) {
102
+ const env = Object.assign({}, process.env);
103
+ env[RETRY_FLAG] = '1';
104
+ spawnCli(args, { env: env, pipeStderr: false }, function (code, signal) {
105
+ if (signal) {
106
+ process.exit(128 + 15);
107
+ }
108
+ process.exit(code === null ? 1 : code);
109
+ });
110
+ }
111
+
112
+ function main() {
113
+ const args = [CLI_ENTRY].concat(process.argv.slice(2));
114
+
115
+ // 重入护栏:已被 launcher 重跑过的进程不再重试,杜绝死循环
116
+ if (process.env[RETRY_FLAG] === '1') {
117
+ runFinal(args);
118
+ return;
119
+ }
120
+
121
+ spawnCli(args, { pipeStderr: true }, function (code, signal, stderrText) {
122
+ if (signal) {
123
+ process.exit(128 + 15);
124
+ }
125
+ const exitCode = code === null ? 1 : code;
126
+ if (exitCode === 0) {
127
+ process.exit(0);
128
+ }
129
+ const missing = extractMissingPackageName(stderrText);
130
+ if (!missing || !isDeclaredDependency(missing)) {
131
+ process.exit(exitCode); // 非环境性缺依赖:退出码原样透传
132
+ }
133
+ process.stderr.write(
134
+ '[codexmate-launch] 检测到已声明的依赖缺失: ' + missing +
135
+ ',自动执行 npm install 后重试一次...\n'
136
+ );
137
+ runNpmInstall(function (ok) {
138
+ if (!ok) {
139
+ process.stderr.write('[codexmate-launch] npm install 失败,保留上方原始报错并以原退出码退出。\n');
140
+ process.exit(exitCode);
141
+ }
142
+ runFinal(args);
143
+ });
144
+ });
145
+ }
146
+
147
+ main();
package/web-ui/app.js CHANGED
@@ -299,6 +299,7 @@ document.addEventListener('DOMContentLoaded', () => {
299
299
  sessionFilterSource: 'all',
300
300
  sessionPathFilter: '',
301
301
  sessionQuery: '',
302
+ sessionPreviewScrollAtTop: true,
302
303
  sessionRoleFilter: 'all',
303
304
  sessionTimePreset: 'all',
304
305
  sessionSortMode: 'time',
@@ -308,19 +309,28 @@ document.addEventListener('DOMContentLoaded', () => {
308
309
  all: [],
309
310
  codex: [],
310
311
  claude: [],
311
- gemini: []
312
+ gemini: [],
313
+ codebuddy: [],
314
+ pi: [],
315
+ opencode: []
312
316
  },
313
317
  sessionPathOptionsLoadedMap: {
314
318
  all: false,
315
319
  codex: false,
316
320
  claude: false,
317
- gemini: false
321
+ gemini: false,
322
+ codebuddy: false,
323
+ pi: false,
324
+ opencode: false
318
325
  },
319
326
  sessionPathRequestSeqMap: {
320
327
  all: 0,
321
328
  codex: 0,
322
329
  claude: 0,
323
- gemini: 0
330
+ gemini: 0,
331
+ codebuddy: 0,
332
+ pi: 0,
333
+ opencode: 0
324
334
  },
325
335
  sessionExporting: {},
326
336
  sessionConverting: {},
@@ -565,6 +575,7 @@ document.addEventListener('DOMContentLoaded', () => {
565
575
  kilocodeAutoSaveSignature: '',
566
576
  forceCompactLayout: false,
567
577
  sidebarCollapsed: false,
578
+ theme: 'light',
568
579
  sessionLoadNativeDialog: false,
569
580
  starPrompted: false,
570
581
  webhookConfig: { enabled: false, url: '', events: ['provider-switch', 'claude-md-edit'] },
@@ -594,6 +605,9 @@ document.addEventListener('DOMContentLoaded', () => {
594
605
  if (typeof this.initI18n === 'function') {
595
606
  this.initI18n();
596
607
  }
608
+ if (typeof this.applyDocumentTheme === 'function') {
609
+ this.applyDocumentTheme();
610
+ }
597
611
  if (typeof this.loadWebhookSettings === 'function') {
598
612
  this.loadWebhookSettings();
599
613
  }
package/web-ui/index.html CHANGED
@@ -11,6 +11,17 @@
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
+ if (localStorage.getItem('codexmateThemeCache') === 'dark') {
20
+ document.documentElement.setAttribute('data-theme', 'dark');
21
+ }
22
+ } catch (_) {}
23
+ })();
24
+ </script>
14
25
  </head>
15
26
  <body>
16
27
  <!-- @include ./partials/index/layout-header.html -->
@@ -38,6 +49,6 @@
38
49
  <!-- @include ./partials/index/modal-skills.html -->
39
50
  <!-- @include ./partials/index/modal-health-check.html -->
40
51
  <!-- @include ./partials/index/modal-confirm-toast.html -->
41
- <script type="module" src="/web-ui/app.js?cb=3"></script>
52
+ <script type="module" src="/web-ui/app.js"></script>
42
53
  </body>
43
54
  </html>
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Heuristic token-count estimation for the prompts panel.
3
+ *
4
+ * The Web UI ships zero npm dependencies and all frontend libraries are
5
+ * vendored single files, so a real BPE tokenizer (2-3 MB per encoding) is not
6
+ * an acceptable trade-off for this feature. Instead we use a lightweight
7
+ * heuristic that is accurate enough for "is my instruction file 2k or 20k
8
+ * tokens" style decisions:
9
+ *
10
+ * - CJK ideographs / kana / fullwidth forms count as ~1 token per
11
+ * character (matches the common ~1 token per CJK char behavior of
12
+ * mainstream tokenizers);
13
+ * - everything else (Latin words, markdown syntax, whitespace, code) is
14
+ * estimated at ~4 characters per token.
15
+ *
16
+ * Values are meant to be displayed with an "approx." qualifier in the UI.
17
+ */
18
+
19
+ // CJK ideographs, Hangul jamo/syllables, kana, CJK punctuation/symbols and
20
+ // fullwidth forms all tokenize at roughly one token per character.
21
+ const CJK_TOKEN_RE = /[\u1100-\u11FF\u2E80-\u2EFF\u3000-\u303F\u3040-\u30FF\u3130-\u318F\u3400-\u4DBF\u4E00-\u9FFF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF]/g;
22
+
23
+ // Non-CJK text averages roughly 4 characters per token for typical
24
+ // English + markdown + code instruction files.
25
+ const NON_CJK_CHARS_PER_TOKEN = 4;
26
+
27
+ export function estimatePromptTokenCount(text) {
28
+ const source = typeof text === 'string' ? text : '';
29
+ if (!source) {
30
+ return 0;
31
+ }
32
+ const cjkMatches = source.match(CJK_TOKEN_RE);
33
+ const cjkTokenCount = cjkMatches ? cjkMatches.length : 0;
34
+ const nonCjkCharCount = source.length - cjkTokenCount;
35
+ return cjkTokenCount + Math.ceil(nonCjkCharCount / NON_CJK_CHARS_PER_TOKEN);
36
+ }
37
+
38
+ export function buildPromptTokenDiffStats(originalText, currentText) {
39
+ const before = estimatePromptTokenCount(originalText);
40
+ const current = estimatePromptTokenCount(currentText);
41
+ const delta = current - before;
42
+ return {
43
+ before,
44
+ current,
45
+ delta,
46
+ deltaLabel: delta === 0 ? '±0' : (delta > 0 ? `+${delta}` : String(delta)),
47
+ deltaKind: delta === 0 ? 'same' : (delta > 0 ? 'add' : 'del')
48
+ };
49
+ }
@@ -29,14 +29,14 @@ function shouldUseFastSessionBrowseLimit(options = {}) {
29
29
 
30
30
  export function isSessionQueryEnabled(source) {
31
31
  const normalized = normalizeSessionSource(source, '');
32
- return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all';
32
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'all';
33
33
  }
34
34
 
35
35
  export function normalizeSessionSource(source, fallback = 'all') {
36
36
  const normalized = typeof source === 'string'
37
37
  ? source.trim().toLowerCase()
38
38
  : '';
39
- if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'pi' || normalized === 'all') {
39
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy' || normalized === 'workbuddy' || normalized === 'pi' || normalized === 'opencode' || normalized === 'all') {
40
40
  return normalized;
41
41
  }
42
42
  return fallback;
@@ -57,6 +57,18 @@ function isConcreteSessionModelName(value) {
57
57
  return normalized.toLowerCase() !== '<synthetic>';
58
58
  }
59
59
 
60
+ function getSessionSourceLabel(source) {
61
+ if (source === 'claude') return 'Claude Code';
62
+ if (source === 'gemini') return 'Gemini CLI';
63
+ if (source === 'codebuddy') return 'CodeBuddy Code';
64
+ if (source === 'workbuddy') return 'WorkBuddy AI';
65
+ if (source === 'pi') return 'Pi';
66
+ if (source === 'opencode') return 'OpenCode';
67
+ return 'Codex';
68
+ }
69
+
70
+ const SESSION_USAGE_SOURCE_ORDER = ['codex', 'claude', 'gemini', 'codebuddy', 'workbuddy', 'pi', 'opencode'];
71
+
60
72
  function collectSessionModelNames(session) {
61
73
  if (!session || typeof session !== 'object') {
62
74
  return [];
@@ -209,7 +221,7 @@ export function buildUsageHeatmap(sessions = [], options = {}) {
209
221
  for (const session of list) {
210
222
  if (!session || typeof session !== 'object') continue;
211
223
  const source = normalizeSessionSource(session.source, '');
212
- if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
224
+ if (!SESSION_USAGE_SOURCE_ORDER.includes(source)) continue;
213
225
  const updatedAtMs = Date.parse(session.updatedAt || '');
214
226
  if (!Number.isFinite(updatedAtMs)) continue;
215
227
  normalized.push({
@@ -309,7 +321,7 @@ export function buildUsageHourlyHeatmap(sessions = [], options = {}) {
309
321
  for (const session of list) {
310
322
  if (!session || typeof session !== 'object') continue;
311
323
  const source = normalizeSessionSource(session.source, '');
312
- if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
324
+ if (!SESSION_USAGE_SOURCE_ORDER.includes(source)) continue;
313
325
  const updatedAtMs = Date.parse(session.updatedAt || '');
314
326
  if (!Number.isFinite(updatedAtMs)) continue;
315
327
  const dayStart = toUtcDayStartMs(updatedAtMs);
@@ -402,7 +414,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
402
414
  for (const [sessionIndex, session] of list.entries()) {
403
415
  if (!session || typeof session !== 'object') continue;
404
416
  const source = normalizeSessionSource(session.source, '');
405
- if (source !== 'codex' && source !== 'claude' && source !== 'pi') continue;
417
+ if (!SESSION_USAGE_SOURCE_ORDER.includes(source)) continue;
406
418
  const updatedAtMs = Date.parse(session.updatedAt || '');
407
419
  if (!Number.isFinite(updatedAtMs)) continue;
408
420
  const createdAtMs = Date.parse(session.createdAt || '');
@@ -437,6 +449,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
437
449
  return ns.updatedAtMs >= rangeStartMs && ns.updatedAtMs <= rangeEndMs;
438
450
  });
439
451
 
452
+ const sourceSessionTotals = Object.fromEntries(SESSION_USAGE_SOURCE_ORDER.map((source) => [source, 0]));
440
453
  let codexTotal = 0;
441
454
  let claudeTotal = 0;
442
455
  let messageTotal = 0;
@@ -449,8 +462,8 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
449
462
  const modelMap = new Map();
450
463
  const missingModelProviderMap = new Map();
451
464
  const missingModelSessionMap = new Map();
452
- const sourceMessageTotals = { codex: 0, claude: 0 };
453
- const missingModelSourceTotals = { codex: 0, claude: 0 };
465
+ const sourceMessageTotals = Object.fromEntries(SESSION_USAGE_SOURCE_ORDER.map((source) => [source, 0]));
466
+ const missingModelSourceTotals = Object.fromEntries(SESSION_USAGE_SOURCE_ORDER.map((source) => [source, 0]));
454
467
  let missingModelSessions = 0;
455
468
  let providerOnlySessions = 0;
456
469
  const hourCounts = Array.from({ length: 24 }, (_, hour) => ({
@@ -485,10 +498,11 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
485
498
  : 0;
486
499
  bucket.totalSessions += 1;
487
500
  bucket.totalMessages += messageCount;
501
+ sourceSessionTotals[source] = (sourceSessionTotals[source] || 0) + 1;
488
502
  if (source === 'codex') {
489
503
  bucket.codex += 1;
490
504
  codexTotal += 1;
491
- } else {
505
+ } else if (source === 'claude') {
492
506
  bucket.claude += 1;
493
507
  claudeTotal += 1;
494
508
  }
@@ -519,7 +533,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
519
533
  });
520
534
  }
521
535
 
522
- const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
536
+ const sourceLabel = getSessionSourceLabel(source);
523
537
  const normalizedTitle = typeof session.title === 'string' && session.title.trim()
524
538
  ? session.title.trim()
525
539
  : (typeof session.sessionId === 'string' && session.sessionId.trim() ? session.sessionId.trim() : '未命名会话');
@@ -564,11 +578,12 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
564
578
  topSessionsByMessages.push({ ...sessionEntry });
565
579
  }
566
580
 
567
- const totalSessions = codexTotal + claudeTotal;
568
- const sourceShare = [
569
- { key: 'codex', label: 'Codex', value: codexTotal },
570
- { key: 'claude', label: 'Claude', value: claudeTotal }
571
- ].map((item) => ({
581
+ const totalSessions = SESSION_USAGE_SOURCE_ORDER.reduce((sum, source) => sum + (sourceSessionTotals[source] || 0), 0);
582
+ const sourceShare = SESSION_USAGE_SOURCE_ORDER.map((source) => ({
583
+ key: source,
584
+ label: getSessionSourceLabel(source),
585
+ value: sourceSessionTotals[source] || 0
586
+ })).map((item) => ({
572
587
  ...item,
573
588
  percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0,
574
589
  messageTotal: sourceMessageTotals[item.key] || 0,
@@ -591,7 +606,7 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
591
606
  .map(([modelId, meta]) => {
592
607
  const sourceLabels = [...meta.sources]
593
608
  .sort((a, b) => a.localeCompare(b, 'en-US'))
594
- .map((source) => (source === 'codex' ? 'Codex' : 'Claude Code'));
609
+ .map((source) => getSessionSourceLabel(source));
595
610
  return {
596
611
  key: modelId,
597
612
  model: modelId,
@@ -196,6 +196,7 @@ export function createDashboardComputed() {
196
196
  if (this.sessionFilterSource === 'claude') return this.t('dashboard.sessionSource.claude');
197
197
  if (this.sessionFilterSource === 'gemini') return this.t('dashboard.sessionSource.gemini');
198
198
  if (this.sessionFilterSource === 'codebuddy') return this.t('dashboard.sessionSource.codebuddy');
199
+ if (this.sessionFilterSource === 'workbuddy') return this.t('dashboard.sessionSource.workbuddy');
199
200
  return this.t('dashboard.sessionSource.all');
200
201
  },
201
202
  inspectorSessionPathLabel() {
@@ -1,5 +1,23 @@
1
+ import { estimatePromptTokenCount, buildPromptTokenDiffStats } from '../logic.prompts-token-count.mjs';
2
+
1
3
  export function createPromptsComputed() {
2
4
  return {
5
+ agentsTokenCount() {
6
+ return estimatePromptTokenCount(this.agentsContent);
7
+ },
8
+
9
+ sysPromptTokenCount() {
10
+ return estimatePromptTokenCount(this.sysPromptContent);
11
+ },
12
+
13
+ agentsDiffTokenStats() {
14
+ return buildPromptTokenDiffStats(this.agentsOriginalContent, this.agentsContent);
15
+ },
16
+
17
+ sysPromptDiffTokenStats() {
18
+ return buildPromptTokenDiffStats(this.sysPromptOriginalContent, this.sysPromptContent);
19
+ },
20
+
3
21
  promptsContextHint() {
4
22
  if (!this.agentsLoading && !this.agentsDiffVisible && this.hasAgentsContentChanged()) {
5
23
  return { text: this.t('modal.agents.unsaved.detectedHint'), warn: true };
@@ -323,7 +323,9 @@ export function createSessionComputed() {
323
323
  { value: "claude", label: this.t("sessions.source.claudeCode") },
324
324
  { value: "gemini", label: this.t("sessions.source.gemini") },
325
325
  { value: "codebuddy", label: this.t("sessions.source.codebuddy") },
326
- { value: "pi", label: this.t("sessions.source.pi") }
326
+ { value: "workbuddy", label: this.t("sessions.source.workbuddy") },
327
+ { value: "pi", label: this.t("sessions.source.pi") },
328
+ { value: "opencode", label: this.t("sessions.source.opencode") }
327
329
  ];
328
330
  },
329
331
  activeSessionExportKey() {
@@ -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(),
@@ -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 !== 'pi') {
32
- error = 'source 仅支持 codex、claude、gemini、codebuddy 或 pi';
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';
33
33
  }
34
34
  if (!sessionId && !filePath) {
35
35
  error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
@@ -75,7 +75,13 @@ export function createSessionActionMethods(options = {}) {
75
75
  ? 'Gemini CLI'
76
76
  : (context.params.source === 'pi'
77
77
  ? 'Pi'
78
- : (context.params.source === 'codebuddy' ? 'CodeBuddy Code' : context.params.source))));
78
+ : (context.params.source === 'opencode'
79
+ ? 'OpenCode'
80
+ : (context.params.source === 'codebuddy'
81
+ ? 'CodeBuddy Code'
82
+ : (context.params.source === 'workbuddy'
83
+ ? 'WorkBuddy AI'
84
+ : context.params.source))))));
79
85
  this.activeSession = {
80
86
  source: context.params.source,
81
87
  sourceLabel,
@@ -104,7 +110,7 @@ export function createSessionActionMethods(options = {}) {
104
110
  buildSessionStandaloneUrl(session) {
105
111
  if (!session) return '';
106
112
  const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
107
- if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'pi')) return '';
113
+ if (!source || (source !== 'codex' && source !== 'claude' && source !== 'gemini' && source !== 'codebuddy' && source !== 'workbuddy' && source !== 'pi' && source !== 'opencode')) return '';
108
114
  const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
109
115
  const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
110
116
  if (!sessionId && !filePath) return '';