codexmate 0.1.25 → 0.1.27
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.
- package/cli/session-usage.js +10 -4
- package/cli.js +733 -199
- package/lib/cli-sessions.js +1 -1
- package/package.json +3 -3
- package/tools/dev/launch.js +147 -0
- package/web-ui/app.js +13 -3
- package/web-ui/index.html +1 -1
- package/web-ui/logic.prompts-token-count.mjs +49 -0
- package/web-ui/logic.sessions.mjs +30 -15
- package/web-ui/modules/app.computed.dashboard.mjs +1 -0
- package/web-ui/modules/app.computed.prompts.mjs +18 -0
- package/web-ui/modules/app.computed.session.mjs +3 -1
- package/web-ui/modules/app.methods.session-actions.mjs +10 -4
- package/web-ui/modules/app.methods.session-browser.mjs +34 -45
- package/web-ui/modules/app.methods.session-timeline.mjs +1 -0
- package/web-ui/modules/i18n/locales/en.mjs +9 -1
- package/web-ui/modules/i18n/locales/ja.mjs +9 -1
- package/web-ui/modules/i18n/locales/vi.mjs +9 -1
- package/web-ui/modules/i18n/locales/zh-tw.mjs +9 -1
- package/web-ui/modules/i18n/locales/zh.mjs +9 -1
- package/web-ui/partials/index/layout-header.html +4 -2
- package/web-ui/partials/index/panel-prompts.html +14 -0
- package/web-ui/partials/index/panel-sessions.html +19 -4
- package/web-ui/res/web-ui-render.precompiled.js +89 -21
- package/web-ui/session-helpers.mjs +1 -1
- package/web-ui/styles/base-theme.css +24 -0
- package/web-ui/styles/modals-core.css +48 -0
- package/web-ui/styles/sessions-list.css +1 -1
- package/web-ui/styles/sessions-preview.css +56 -7
- package/web-ui/styles/sessions-toolbar-trash.css +1 -7
package/lib/cli-sessions.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"codexmate": "./
|
|
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
|
|
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: {},
|
package/web-ui/index.html
CHANGED
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
<!-- @include ./partials/index/modal-skills.html -->
|
|
39
39
|
<!-- @include ./partials/index/modal-health-check.html -->
|
|
40
40
|
<!-- @include ./partials/index/modal-confirm-toast.html -->
|
|
41
|
-
<script type="module" src="/web-ui/app.js
|
|
41
|
+
<script type="module" src="/web-ui/app.js"></script>
|
|
42
42
|
</body>
|
|
43
43
|
</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
|
|
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
|
|
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
|
|
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 =
|
|
453
|
-
const missingModelSourceTotals =
|
|
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
|
|
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 =
|
|
568
|
-
const sourceShare =
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
|
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: "
|
|
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() {
|
|
@@ -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 或
|
|
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 === '
|
|
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 '';
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
buildSessionFilterCacheState,
|
|
3
3
|
isSessionQueryEnabled,
|
|
4
4
|
normalizeSessionMessageRole,
|
|
5
|
-
normalizeSessionPathFilter
|
|
5
|
+
normalizeSessionPathFilter,
|
|
6
|
+
normalizeSessionSource
|
|
6
7
|
} from '../logic.mjs';
|
|
7
8
|
import {
|
|
8
9
|
applySessionsFilterUrlState,
|
|
9
|
-
buildSessionsFilterShareUrl,
|
|
10
10
|
normalizeSessionRoleFilter,
|
|
11
11
|
normalizeSessionTimePreset,
|
|
12
12
|
readSessionsFilterUrlState,
|
|
@@ -118,9 +118,7 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
118
118
|
},
|
|
119
119
|
|
|
120
120
|
syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
|
|
121
|
-
const targetSource = source
|
|
122
|
-
? 'claude'
|
|
123
|
-
: (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
|
|
121
|
+
const targetSource = normalizeSessionSource(source, 'all');
|
|
124
122
|
const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
|
|
125
123
|
? this.sessionPathOptionsMap[targetSource]
|
|
126
124
|
: [];
|
|
@@ -135,9 +133,7 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
135
133
|
},
|
|
136
134
|
|
|
137
135
|
refreshSessionPathOptions(source) {
|
|
138
|
-
const targetSource = source
|
|
139
|
-
? 'claude'
|
|
140
|
-
: (source === 'gemini' ? 'gemini' : (source === 'pi' ? 'pi' : (source === 'all' ? 'all' : 'codex')));
|
|
136
|
+
const targetSource = normalizeSessionSource(source, 'all');
|
|
141
137
|
const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
|
|
142
138
|
? [...this.sessionPathOptionsMap[targetSource]]
|
|
143
139
|
: [];
|
|
@@ -159,9 +155,7 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
159
155
|
},
|
|
160
156
|
|
|
161
157
|
async loadSessionPathOptions(options = {}) {
|
|
162
|
-
const source = options.source
|
|
163
|
-
? 'claude'
|
|
164
|
-
: (options.source === 'gemini' ? 'gemini' : (options.source === 'pi' ? 'pi' : (options.source === 'all' ? 'all' : 'codex')));
|
|
158
|
+
const source = normalizeSessionSource(options.source, 'all');
|
|
165
159
|
const forceRefresh = !!options.forceRefresh;
|
|
166
160
|
const loaded = !!this.sessionPathOptionsLoadedMap[source];
|
|
167
161
|
if (!forceRefresh && loaded) {
|
|
@@ -454,17 +448,28 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
454
448
|
this.loadSessions();
|
|
455
449
|
},
|
|
456
450
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
451
|
+
getSessionPreviewScrollContainer() {
|
|
452
|
+
return this.sessionPreviewScrollEl || (this.$refs && this.$refs.sessionPreviewScroll) || null;
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
scrollSessionPreviewToTop() {
|
|
456
|
+
const scrollEl = this.getSessionPreviewScrollContainer();
|
|
457
|
+
if (!scrollEl) return;
|
|
458
|
+
if (typeof scrollEl.scrollTo === 'function') {
|
|
459
|
+
scrollEl.scrollTo({ top: 0, behavior: 'smooth' });
|
|
460
|
+
} else {
|
|
461
|
+
scrollEl.scrollTop = 0;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
scrollSessionPreviewToBottom() {
|
|
466
|
+
const scrollEl = this.getSessionPreviewScrollContainer();
|
|
467
|
+
if (!scrollEl) return;
|
|
468
|
+
if (typeof scrollEl.scrollTo === 'function') {
|
|
469
|
+
scrollEl.scrollTo({ top: scrollEl.scrollHeight, behavior: 'smooth' });
|
|
470
|
+
} else {
|
|
471
|
+
scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
466
472
|
}
|
|
467
|
-
return escaped;
|
|
468
473
|
},
|
|
469
474
|
|
|
470
475
|
async onSessionSourceChange(event) {
|
|
@@ -513,9 +518,13 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
513
518
|
? this.t('sessions.source.gemini')
|
|
514
519
|
: (this.sessionFilterSource === 'codebuddy'
|
|
515
520
|
? this.t('sessions.source.codebuddy')
|
|
516
|
-
: (this.sessionFilterSource === '
|
|
517
|
-
? this.t('sessions.source.
|
|
518
|
-
: this.sessionFilterSource
|
|
521
|
+
: (this.sessionFilterSource === 'workbuddy'
|
|
522
|
+
? this.t('sessions.source.workbuddy')
|
|
523
|
+
: (this.sessionFilterSource === 'pi'
|
|
524
|
+
? this.t('sessions.source.pi')
|
|
525
|
+
: (this.sessionFilterSource === 'opencode'
|
|
526
|
+
? this.t('sessions.source.opencode')
|
|
527
|
+
: this.sessionFilterSource))))));
|
|
519
528
|
chips.push({ key: 'source', title: this.t('sessions.filters.source'), value: label });
|
|
520
529
|
}
|
|
521
530
|
if (this.sessionPathFilter) {
|
|
@@ -567,27 +576,6 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
567
576
|
await this.onSessionSourceChange();
|
|
568
577
|
},
|
|
569
578
|
|
|
570
|
-
async copySessionsFilterShareUrl() {
|
|
571
|
-
const url = buildSessionsFilterShareUrl(this);
|
|
572
|
-
if (!url) {
|
|
573
|
-
this.showMessage(typeof this.t === 'function' ? this.t('sessions.filters.urlBuildFail') : 'Failed to build link', 'error');
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
try {
|
|
577
|
-
if (navigator.clipboard && window.isSecureContext) {
|
|
578
|
-
await navigator.clipboard.writeText(url);
|
|
579
|
-
this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
} catch (_) {}
|
|
583
|
-
const ok = typeof this.fallbackCopyText === 'function' ? this.fallbackCopyText(url) : false;
|
|
584
|
-
if (ok) {
|
|
585
|
-
this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.ok') : 'Copied', 'success');
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
this.showMessage(typeof this.t === 'function' ? this.t('toast.copy.fail') : 'Copy failed', 'error');
|
|
589
|
-
},
|
|
590
|
-
|
|
591
579
|
normalizeSessionMessage(message) {
|
|
592
580
|
const fallback = {
|
|
593
581
|
role: 'assistant',
|
|
@@ -934,6 +922,7 @@ export function createSessionBrowserMethods(options = {}) {
|
|
|
934
922
|
return;
|
|
935
923
|
}
|
|
936
924
|
this.activeSession = session;
|
|
925
|
+
this.sessionPreviewScrollAtTop = true;
|
|
937
926
|
emitSessionLoadNativeDialog(this, 'selectSession:activate', `sessionId=${session.sessionId || ''}`);
|
|
938
927
|
if (typeof this.expandVisibleSessionList === 'function') {
|
|
939
928
|
this.expandVisibleSessionList(0, { ensureActive: true });
|
|
@@ -262,6 +262,7 @@ export function createSessionTimelineMethods() {
|
|
|
262
262
|
) return;
|
|
263
263
|
const scrollEl = this.sessionPreviewScrollEl || this.$refs.sessionPreviewScroll;
|
|
264
264
|
if (!scrollEl) return;
|
|
265
|
+
this.sessionPreviewScrollAtTop = Number(scrollEl.scrollTop || 0) <= 8;
|
|
265
266
|
|
|
266
267
|
if (
|
|
267
268
|
this.canLoadMoreSessionMessages
|