codexmate 0.0.20 → 0.0.21

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 (102) hide show
  1. package/README.en.md +349 -259
  2. package/README.md +284 -252
  3. package/cli/agents-files.js +162 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +580 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/config-health.js +338 -338
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/skills.js +1141 -0
  12. package/cli/zip-commands.js +510 -0
  13. package/cli.js +13101 -13497
  14. package/lib/cli-file-utils.js +151 -151
  15. package/lib/cli-models-utils.js +419 -311
  16. package/lib/cli-network-utils.js +164 -164
  17. package/lib/cli-path-utils.js +69 -0
  18. package/lib/cli-session-utils.js +121 -121
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/cli-utils.js +155 -155
  21. package/lib/download-artifacts.js +77 -0
  22. package/lib/mcp-stdio.js +440 -440
  23. package/lib/task-orchestrator.js +869 -0
  24. package/lib/text-diff.js +303 -303
  25. package/lib/workflow-engine.js +340 -340
  26. package/package.json +74 -70
  27. package/res/json5.min.js +1 -1
  28. package/res/vue.global.prod.js +13 -0
  29. package/web-ui/app.js +530 -397
  30. package/web-ui/index.html +33 -30
  31. package/web-ui/logic.agents-diff.mjs +386 -386
  32. package/web-ui/logic.claude.mjs +168 -108
  33. package/web-ui/logic.mjs +5 -5
  34. package/web-ui/logic.runtime.mjs +124 -124
  35. package/web-ui/logic.sessions.mjs +581 -263
  36. package/web-ui/modules/api.mjs +90 -69
  37. package/web-ui/modules/app.computed.dashboard.mjs +113 -113
  38. package/web-ui/modules/app.computed.index.mjs +15 -13
  39. package/web-ui/modules/app.computed.main-tabs.mjs +195 -0
  40. package/web-ui/modules/app.computed.session.mjs +507 -141
  41. package/web-ui/modules/app.constants.mjs +15 -15
  42. package/web-ui/modules/app.methods.agents.mjs +493 -493
  43. package/web-ui/modules/app.methods.claude-config.mjs +174 -174
  44. package/web-ui/modules/app.methods.codex-config.mjs +640 -640
  45. package/web-ui/modules/app.methods.index.mjs +88 -86
  46. package/web-ui/modules/app.methods.install.mjs +149 -157
  47. package/web-ui/modules/app.methods.navigation.mjs +619 -478
  48. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -514
  49. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -337
  50. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -251
  51. package/web-ui/modules/app.methods.providers.mjs +363 -265
  52. package/web-ui/modules/app.methods.runtime.mjs +323 -323
  53. package/web-ui/modules/app.methods.session-actions.mjs +520 -457
  54. package/web-ui/modules/app.methods.session-browser.mjs +626 -435
  55. package/web-ui/modules/app.methods.session-timeline.mjs +448 -441
  56. package/web-ui/modules/app.methods.session-trash.mjs +422 -419
  57. package/web-ui/modules/app.methods.startup-claude.mjs +412 -406
  58. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  59. package/web-ui/modules/config-mode.computed.mjs +126 -124
  60. package/web-ui/modules/skills.computed.mjs +107 -107
  61. package/web-ui/modules/skills.methods.mjs +481 -481
  62. package/web-ui/partials/index/layout-footer.html +13 -69
  63. package/web-ui/partials/index/layout-header.html +402 -337
  64. package/web-ui/partials/index/modal-config-template-agents.html +125 -125
  65. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  66. package/web-ui/partials/index/modal-health-check.html +72 -72
  67. package/web-ui/partials/index/modal-openclaw-config.html +280 -275
  68. package/web-ui/partials/index/modal-skills.html +184 -184
  69. package/web-ui/partials/index/modals-basic.html +156 -196
  70. package/web-ui/partials/index/panel-config-claude.html +126 -100
  71. package/web-ui/partials/index/panel-config-codex.html +237 -237
  72. package/web-ui/partials/index/panel-config-openclaw.html +78 -84
  73. package/web-ui/partials/index/panel-docs.html +130 -0
  74. package/web-ui/partials/index/panel-market.html +174 -174
  75. package/web-ui/partials/index/panel-orchestration.html +397 -0
  76. package/web-ui/partials/index/panel-sessions.html +292 -387
  77. package/web-ui/partials/index/panel-settings.html +190 -166
  78. package/web-ui/partials/index/panel-usage.html +213 -0
  79. package/web-ui/session-helpers.mjs +559 -362
  80. package/web-ui/source-bundle.cjs +233 -233
  81. package/web-ui/styles/base-theme.css +271 -373
  82. package/web-ui/styles/controls-forms.css +360 -354
  83. package/web-ui/styles/docs-panel.css +182 -0
  84. package/web-ui/styles/feedback.css +108 -108
  85. package/web-ui/styles/health-check-dialog.css +144 -144
  86. package/web-ui/styles/layout-shell.css +376 -330
  87. package/web-ui/styles/modals-core.css +464 -449
  88. package/web-ui/styles/navigation-panels.css +348 -381
  89. package/web-ui/styles/openclaw-structured.css +266 -266
  90. package/web-ui/styles/responsive.css +450 -416
  91. package/web-ui/styles/sessions-list.css +400 -414
  92. package/web-ui/styles/sessions-preview.css +411 -405
  93. package/web-ui/styles/sessions-toolbar-trash.css +243 -243
  94. package/web-ui/styles/sessions-usage.css +628 -276
  95. package/web-ui/styles/skills-list.css +296 -298
  96. package/web-ui/styles/skills-market.css +335 -335
  97. package/web-ui/styles/task-orchestration.css +776 -0
  98. package/web-ui/styles/titles-cards.css +408 -407
  99. package/web-ui/styles.css +18 -16
  100. package/web-ui.html +17 -17
  101. package/res/screenshot.png +0 -0
  102. package/res/vue.global.js +0 -18552
@@ -1,263 +1,581 @@
1
- export function isSessionQueryEnabled(source) {
2
- const normalized = normalizeSessionSource(source, '');
3
- return normalized === 'codex' || normalized === 'claude' || normalized === 'all';
4
- }
5
-
6
- export function normalizeSessionSource(source, fallback = 'all') {
7
- const normalized = typeof source === 'string'
8
- ? source.trim().toLowerCase()
9
- : '';
10
- if (normalized === 'codex' || normalized === 'claude' || normalized === 'all') {
11
- return normalized;
12
- }
13
- return fallback;
14
- }
15
-
16
- export function normalizeSessionPathFilter(pathFilter) {
17
- return typeof pathFilter === 'string' ? pathFilter.trim() : '';
18
- }
19
-
20
- export function buildSessionFilterCacheState(source, pathFilter) {
21
- return {
22
- source: normalizeSessionSource(source, 'all'),
23
- pathFilter: normalizeSessionPathFilter(pathFilter)
24
- };
25
- }
26
-
27
- export function buildSessionListParams(options = {}) {
28
- const {
29
- source = 'all',
30
- pathFilter = '',
31
- query = '',
32
- roleFilter = 'all',
33
- timeRangePreset = 'all',
34
- limit = 200
35
- } = options;
36
- const normalizedSource = normalizeSessionSource(source, 'all');
37
- const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
38
- const queryValue = isSessionQueryEnabled(normalizedSource) ? query : '';
39
- return {
40
- source: normalizedSource,
41
- pathFilter: normalizedPathFilter,
42
- query: queryValue,
43
- queryMode: 'and',
44
- queryScope: 'content',
45
- contentScanLimit: 50,
46
- roleFilter,
47
- timeRangePreset,
48
- limit,
49
- forceRefresh: true
50
- };
51
- }
52
-
53
- export function normalizeSessionMessageRole(role) {
54
- const value = typeof role === 'string' ? role.trim().toLowerCase() : '';
55
- if (value === 'user' || value === 'assistant' || value === 'system') {
56
- return value;
57
- }
58
- return 'assistant';
59
- }
60
-
61
- function toRoleMeta(role) {
62
- if (role === 'user') {
63
- return { role: 'user', roleLabel: 'User', roleShort: 'U' };
64
- }
65
- if (role === 'assistant') {
66
- return { role: 'assistant', roleLabel: 'Assistant', roleShort: 'A' };
67
- }
68
- if (role === 'system') {
69
- return { role: 'system', roleLabel: 'System', roleShort: 'S' };
70
- }
71
- return { role: 'mixed', roleLabel: 'Mixed', roleShort: 'M' };
72
- }
73
-
74
- function clampTimelinePercent(percent) {
75
- return Math.max(6, Math.min(94, percent));
76
- }
77
-
78
- export function formatSessionTimelineTimestamp(timestamp) {
79
- const value = typeof timestamp === 'string' ? timestamp.trim() : '';
80
- if (!value) return '';
81
-
82
- const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
83
- if (matched) {
84
- const second = matched[6] || '00';
85
- return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
86
- }
87
-
88
- return value;
89
- }
90
-
91
- export function buildUsageChartGroups(sessions = [], options = {}) {
92
- const list = Array.isArray(sessions) ? sessions : [];
93
- const range = typeof options.range === 'string' ? options.range.trim().toLowerCase() : '7d';
94
- const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
95
- const dayMs = 24 * 60 * 60 * 1000;
96
- const rangeDays = range === '30d' ? 30 : 7;
97
- const buckets = [];
98
- for (let i = rangeDays - 1; i >= 0; i -= 1) {
99
- const stamp = new Date(now - (i * dayMs));
100
- const key = `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
101
- buckets.push({
102
- key,
103
- label: key.slice(5),
104
- codex: 0,
105
- claude: 0,
106
- totalMessages: 0,
107
- totalSessions: 0
108
- });
109
- }
110
- const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]));
111
- let codexTotal = 0;
112
- let claudeTotal = 0;
113
- let messageTotal = 0;
114
- const pathMap = new Map();
115
-
116
- for (const session of list) {
117
- if (!session || typeof session !== 'object') continue;
118
- const source = normalizeSessionSource(session.source, '');
119
- if (source !== 'codex' && source !== 'claude') continue;
120
- const updatedAtMs = Date.parse(session.updatedAt || '');
121
- if (!Number.isFinite(updatedAtMs)) continue;
122
- const stamp = new Date(updatedAtMs);
123
- const key = `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
124
- const bucket = bucketMap.get(key);
125
- if (!bucket) continue;
126
- const messageCount = Number.isFinite(Number(session.messageCount))
127
- ? Math.max(0, Math.floor(Number(session.messageCount)))
128
- : 0;
129
- bucket.totalSessions += 1;
130
- bucket.totalMessages += messageCount;
131
- if (source === 'codex') {
132
- bucket.codex += 1;
133
- codexTotal += 1;
134
- } else {
135
- bucket.claude += 1;
136
- claudeTotal += 1;
137
- }
138
- messageTotal += messageCount;
139
- const cwd = normalizeSessionPathFilter(session.cwd);
140
- if (cwd) {
141
- pathMap.set(cwd, (Number(pathMap.get(cwd)) || 0) + 1);
142
- }
143
- }
144
-
145
- const totalSessions = codexTotal + claudeTotal;
146
- const sourceShare = [
147
- { key: 'codex', label: 'Codex', value: codexTotal },
148
- { key: 'claude', label: 'Claude', value: claudeTotal }
149
- ].map((item) => ({
150
- ...item,
151
- percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0
152
- }));
153
-
154
- const topPaths = [...pathMap.entries()]
155
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'zh-Hans-CN'))
156
- .slice(0, 5)
157
- .map(([pathValue, count]) => ({ path: pathValue, count }));
158
-
159
- const maxSessionBucket = buckets.reduce((max, item) => Math.max(max, item.totalSessions), 0);
160
- const maxMessageBucket = buckets.reduce((max, item) => Math.max(max, item.totalMessages), 0);
161
-
162
- return {
163
- range,
164
- buckets,
165
- summary: {
166
- totalSessions,
167
- totalMessages: messageTotal,
168
- codexTotal,
169
- claudeTotal,
170
- activeDays: buckets.filter((item) => item.totalSessions > 0).length
171
- },
172
- sourceShare,
173
- topPaths,
174
- maxSessionBucket,
175
- maxMessageBucket
176
- };
177
- }
178
-
179
- export function buildSessionTimelineNodes(messages = [], options = {}) {
180
- const list = Array.isArray(messages) ? messages : [];
181
- const getKey = typeof options.getKey === 'function'
182
- ? options.getKey
183
- : ((_message, index) => `msg-${index}`);
184
- const total = list.length;
185
- const rawMaxMarkers = Number(options.maxMarkers);
186
- const maxMarkers = Number.isFinite(rawMaxMarkers)
187
- ? Math.max(1, Math.min(80, Math.floor(rawMaxMarkers)))
188
- : 30;
189
-
190
- const buildSingleNode = (message, index) => {
191
- const role = normalizeSessionMessageRole(message && (message.normalizedRole || message.role));
192
- const roleMeta = toRoleMeta(role);
193
- const key = String(getKey(message, index) || `msg-${index}`);
194
- const displayTime = formatSessionTimelineTimestamp(message && message.timestamp ? message.timestamp : '');
195
- const title = displayTime
196
- ? `#${index + 1} · ${roleMeta.roleLabel} · ${displayTime}`
197
- : `#${index + 1} · ${roleMeta.roleLabel}`;
198
- const percent = total <= 1 ? 0 : (index / (total - 1)) * 100;
199
- return {
200
- key,
201
- role: roleMeta.role,
202
- roleLabel: roleMeta.roleLabel,
203
- roleShort: roleMeta.roleShort,
204
- displayTime,
205
- title,
206
- percent,
207
- safePercent: clampTimelinePercent(percent)
208
- };
209
- };
210
-
211
- if (total <= maxMarkers) {
212
- return list.map((message, index) => buildSingleNode(message, index));
213
- }
214
-
215
- const nodes = [];
216
- const bucketWidth = total / maxMarkers;
217
- for (let bucket = 0; bucket < maxMarkers; bucket += 1) {
218
- let start = Math.floor(bucket * bucketWidth);
219
- if (nodes.length && start <= nodes[nodes.length - 1].endIndex) {
220
- start = nodes[nodes.length - 1].endIndex + 1;
221
- }
222
- if (start >= total) {
223
- break;
224
- }
225
- let end = Math.floor((bucket + 1) * bucketWidth) - 1;
226
- end = Math.max(start, Math.min(total - 1, end));
227
- const targetIndex = Math.min(total - 1, start + Math.floor((end - start) / 2));
228
- const targetMessage = list[targetIndex] || null;
229
- const key = String(getKey(targetMessage, targetIndex) || `msg-${targetIndex}`);
230
- const percent = total <= 1 ? 0 : (targetIndex / (total - 1)) * 100;
231
- const messagesInGroup = end - start + 1;
232
- const roleSet = new Set();
233
- for (let i = start; i <= end; i += 1) {
234
- roleSet.add(normalizeSessionMessageRole(list[i] && (list[i].normalizedRole || list[i].role)));
235
- }
236
- const roleValue = roleSet.size === 1 ? Array.from(roleSet)[0] : 'mixed';
237
- const roleMeta = toRoleMeta(roleValue);
238
- const firstTime = formatSessionTimelineTimestamp(list[start] && list[start].timestamp ? list[start].timestamp : '');
239
- const lastTime = formatSessionTimelineTimestamp(list[end] && list[end].timestamp ? list[end].timestamp : '');
240
- let displayTime = '';
241
- if (firstTime && lastTime) {
242
- displayTime = firstTime === lastTime ? firstTime : `${firstTime} ~ ${lastTime}`;
243
- } else {
244
- displayTime = firstTime || lastTime;
245
- }
246
- const titleBase = `#${start + 1}-${end + 1} · ${messagesInGroup} msgs · ${roleMeta.roleLabel}`;
247
- const title = displayTime ? `${titleBase} · ${displayTime}` : titleBase;
248
- nodes.push({
249
- key,
250
- role: roleMeta.role,
251
- roleLabel: roleMeta.roleLabel,
252
- roleShort: roleMeta.roleShort,
253
- displayTime,
254
- title,
255
- percent,
256
- safePercent: clampTimelinePercent(percent),
257
- startIndex: start,
258
- endIndex: end,
259
- messageCount: messagesInGroup
260
- });
261
- }
262
- return nodes;
263
- }
1
+ export const DEFAULT_SESSION_LIST_LIMIT = 200;
2
+ export const DEFAULT_SESSION_LIST_FAST_LIMIT = 20;
3
+
4
+ function shouldUseFastSessionBrowseLimit(options = {}) {
5
+ if (options.forceRefresh) {
6
+ return false;
7
+ }
8
+ const normalizedSource = normalizeSessionSource(options.source, 'all');
9
+ if (normalizedSource !== 'all') {
10
+ return false;
11
+ }
12
+ const pathFilter = normalizeSessionPathFilter(options.pathFilter);
13
+ if (pathFilter) {
14
+ return false;
15
+ }
16
+ const query = typeof options.query === 'string' ? options.query.trim() : '';
17
+ if (query) {
18
+ return false;
19
+ }
20
+ const roleFilter = typeof options.roleFilter === 'string' ? options.roleFilter.trim().toLowerCase() : 'all';
21
+ if (roleFilter && roleFilter !== 'all') {
22
+ return false;
23
+ }
24
+ const timeRangePreset = typeof options.timeRangePreset === 'string'
25
+ ? options.timeRangePreset.trim().toLowerCase()
26
+ : 'all';
27
+ return !timeRangePreset || timeRangePreset === 'all';
28
+ }
29
+
30
+ export function isSessionQueryEnabled(source) {
31
+ const normalized = normalizeSessionSource(source, '');
32
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'all';
33
+ }
34
+
35
+ export function normalizeSessionSource(source, fallback = 'all') {
36
+ const normalized = typeof source === 'string'
37
+ ? source.trim().toLowerCase()
38
+ : '';
39
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'all') {
40
+ return normalized;
41
+ }
42
+ return fallback;
43
+ }
44
+
45
+ export function normalizeSessionPathFilter(pathFilter) {
46
+ return typeof pathFilter === 'string' ? pathFilter.trim() : '';
47
+ }
48
+
49
+ function isConcreteSessionModelName(value) {
50
+ if (typeof value !== 'string') {
51
+ return false;
52
+ }
53
+ const normalized = value.trim();
54
+ if (!normalized) {
55
+ return false;
56
+ }
57
+ return normalized.toLowerCase() !== '<synthetic>';
58
+ }
59
+
60
+ function collectSessionModelNames(session) {
61
+ if (!session || typeof session !== 'object') {
62
+ return [];
63
+ }
64
+ const values = Array.isArray(session.models)
65
+ ? [...session.models, session.model, session.modelName, session.modelId]
66
+ : [session.model, session.modelName, session.modelId];
67
+ const models = [];
68
+ for (const value of values) {
69
+ if (!isConcreteSessionModelName(value)) {
70
+ continue;
71
+ }
72
+ const normalized = value.trim();
73
+ if (models.includes(normalized)) {
74
+ continue;
75
+ }
76
+ models.push(normalized);
77
+ }
78
+ return models;
79
+ }
80
+
81
+ export function buildSessionFilterCacheState(source, pathFilter) {
82
+ return {
83
+ source: normalizeSessionSource(source, 'all'),
84
+ pathFilter: normalizeSessionPathFilter(pathFilter)
85
+ };
86
+ }
87
+
88
+ export function buildSessionListParams(options = {}) {
89
+ const fallbackLimit = shouldUseFastSessionBrowseLimit(options)
90
+ ? DEFAULT_SESSION_LIST_FAST_LIMIT
91
+ : DEFAULT_SESSION_LIST_LIMIT;
92
+ const {
93
+ source = 'all',
94
+ pathFilter = '',
95
+ query = '',
96
+ roleFilter = 'all',
97
+ timeRangePreset = 'all',
98
+ limit = fallbackLimit,
99
+ forceRefresh = false
100
+ } = options;
101
+ const normalizedSource = normalizeSessionSource(source, 'all');
102
+ const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
103
+ const queryValue = isSessionQueryEnabled(normalizedSource) ? query : '';
104
+ return {
105
+ source: normalizedSource,
106
+ pathFilter: normalizedPathFilter,
107
+ query: queryValue,
108
+ queryMode: 'and',
109
+ queryScope: 'content',
110
+ contentScanLimit: 50,
111
+ roleFilter,
112
+ timeRangePreset,
113
+ limit,
114
+ forceRefresh: !!forceRefresh
115
+ };
116
+ }
117
+
118
+ export function normalizeSessionMessageRole(role) {
119
+ const value = typeof role === 'string' ? role.trim().toLowerCase() : '';
120
+ if (value === 'user' || value === 'assistant' || value === 'system') {
121
+ return value;
122
+ }
123
+ return 'assistant';
124
+ }
125
+
126
+ function toRoleMeta(role) {
127
+ if (role === 'user') {
128
+ return { role: 'user', roleLabel: 'User', roleShort: 'U' };
129
+ }
130
+ if (role === 'assistant') {
131
+ return { role: 'assistant', roleLabel: 'Assistant', roleShort: 'A' };
132
+ }
133
+ if (role === 'system') {
134
+ return { role: 'system', roleLabel: 'System', roleShort: 'S' };
135
+ }
136
+ return { role: 'mixed', roleLabel: 'Mixed', roleShort: 'M' };
137
+ }
138
+
139
+ function clampTimelinePercent(percent) {
140
+ return Math.max(6, Math.min(94, percent));
141
+ }
142
+
143
+ export function formatSessionTimelineTimestamp(timestamp) {
144
+ const value = typeof timestamp === 'string' ? timestamp.trim() : '';
145
+ if (!value) return '';
146
+
147
+ const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
148
+ if (matched) {
149
+ const second = matched[6] || '00';
150
+ return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
151
+ }
152
+
153
+ return value;
154
+ }
155
+
156
+ function normalizeUsageRange(range) {
157
+ const normalized = typeof range === 'string' ? range.trim().toLowerCase() : '7d';
158
+ if (normalized === '30d' || normalized === 'all') {
159
+ return normalized;
160
+ }
161
+ return '7d';
162
+ }
163
+
164
+ function toUtcDayStartMs(value) {
165
+ const stamp = new Date(value);
166
+ return Date.UTC(stamp.getUTCFullYear(), stamp.getUTCMonth(), stamp.getUTCDate());
167
+ }
168
+
169
+ function formatUtcDayKey(value) {
170
+ const stamp = new Date(value);
171
+ return `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
172
+ }
173
+
174
+ function buildUsageBuckets(normalizedSessions, options = {}) {
175
+ const range = normalizeUsageRange(options.range);
176
+ const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
177
+ const dayMs = 24 * 60 * 60 * 1000;
178
+ const buckets = [];
179
+
180
+ if (range === 'all') {
181
+ const validDayStarts = normalizedSessions
182
+ .map((session) => toUtcDayStartMs(session.updatedAtMs))
183
+ .filter((value) => Number.isFinite(value));
184
+ const firstDayStart = validDayStarts.length ? Math.min(...validDayStarts) : toUtcDayStartMs(now);
185
+ const lastDayStart = validDayStarts.length ? Math.max(...validDayStarts) : toUtcDayStartMs(now);
186
+ for (let stamp = firstDayStart; stamp <= lastDayStart; stamp += dayMs) {
187
+ const key = formatUtcDayKey(stamp);
188
+ buckets.push({
189
+ key,
190
+ label: key.slice(5),
191
+ codex: 0,
192
+ claude: 0,
193
+ totalMessages: 0,
194
+ totalSessions: 0
195
+ });
196
+ }
197
+ return { range, buckets };
198
+ }
199
+
200
+ const rangeDays = range === '30d' ? 30 : 7;
201
+ for (let i = rangeDays - 1; i >= 0; i -= 1) {
202
+ const stamp = new Date(now - (i * dayMs));
203
+ const key = formatUtcDayKey(stamp);
204
+ buckets.push({
205
+ key,
206
+ label: key.slice(5),
207
+ codex: 0,
208
+ claude: 0,
209
+ totalMessages: 0,
210
+ totalSessions: 0
211
+ });
212
+ }
213
+ return { range, buckets };
214
+ }
215
+
216
+ export function buildUsageChartGroups(sessions = [], options = {}) {
217
+ const list = Array.isArray(sessions) ? sessions : [];
218
+ const normalizedSessions = [];
219
+ for (const [sessionIndex, session] of list.entries()) {
220
+ if (!session || typeof session !== 'object') continue;
221
+ const source = normalizeSessionSource(session.source, '');
222
+ if (source !== 'codex' && source !== 'claude') continue;
223
+ const updatedAtMs = Date.parse(session.updatedAt || '');
224
+ if (!Number.isFinite(updatedAtMs)) continue;
225
+ const createdAtMs = Date.parse(session.createdAt || '');
226
+ const sessionStartedAtMs = Number.isFinite(createdAtMs) ? createdAtMs : updatedAtMs;
227
+ const sessionEndedAtMs = Math.max(updatedAtMs, sessionStartedAtMs);
228
+ normalizedSessions.push({
229
+ session,
230
+ sessionIndex,
231
+ source,
232
+ updatedAtMs,
233
+ createdAtMs,
234
+ sessionStartedAtMs,
235
+ sessionEndedAtMs,
236
+ bucketKey: formatUtcDayKey(updatedAtMs)
237
+ });
238
+ }
239
+ const { range, buckets } = buildUsageBuckets(normalizedSessions, options);
240
+ const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]));
241
+ let codexTotal = 0;
242
+ let claudeTotal = 0;
243
+ let messageTotal = 0;
244
+ let totalTokens = 0;
245
+ let totalContextWindow = 0;
246
+ let activeDurationMs = 0;
247
+ let earliestSessionMs = Number.POSITIVE_INFINITY;
248
+ let latestSessionMs = 0;
249
+ const pathMap = new Map();
250
+ const modelMap = new Map();
251
+ const missingModelProviderMap = new Map();
252
+ const missingModelSessionMap = new Map();
253
+ const sourceMessageTotals = { codex: 0, claude: 0 };
254
+ const missingModelSourceTotals = { codex: 0, claude: 0 };
255
+ let missingModelSessions = 0;
256
+ let providerOnlySessions = 0;
257
+ const hourCounts = Array.from({ length: 24 }, (_, hour) => ({
258
+ key: String(hour).padStart(2, '0'),
259
+ label: String(hour).padStart(2, '0'),
260
+ count: 0
261
+ }));
262
+ const weekdayLabels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
263
+ const weekdayCounts = Array.from({ length: 7 }, (_, index) => ({
264
+ key: String(index),
265
+ label: weekdayLabels[index],
266
+ count: 0
267
+ }));
268
+ const recentSessions = [];
269
+ const topSessionsByMessages = [];
270
+ const filteredSessions = [];
271
+
272
+ for (const normalized of normalizedSessions) {
273
+ const { session, sessionIndex, source, updatedAtMs, sessionStartedAtMs, sessionEndedAtMs, bucketKey } = normalized;
274
+ const stamp = new Date(updatedAtMs);
275
+ const bucket = bucketMap.get(bucketKey);
276
+ if (!bucket) continue;
277
+ const sessionModels = collectSessionModelNames(session);
278
+ if (sessionModels.length === 0) continue;
279
+ filteredSessions.push(session);
280
+ const messageCount = Number.isFinite(Number(session.messageCount))
281
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
282
+ : 0;
283
+ const sessionTotalTokens = Number.isFinite(Number(session.totalTokens))
284
+ ? Math.max(0, Math.floor(Number(session.totalTokens)))
285
+ : 0;
286
+ const sessionContextWindow = Number.isFinite(Number(session.contextWindow))
287
+ ? Math.max(0, Math.floor(Number(session.contextWindow)))
288
+ : 0;
289
+ bucket.totalSessions += 1;
290
+ bucket.totalMessages += messageCount;
291
+ if (source === 'codex') {
292
+ bucket.codex += 1;
293
+ codexTotal += 1;
294
+ } else {
295
+ bucket.claude += 1;
296
+ claudeTotal += 1;
297
+ }
298
+ messageTotal += messageCount;
299
+ totalTokens += sessionTotalTokens;
300
+ totalContextWindow += sessionContextWindow;
301
+ sourceMessageTotals[source] += messageCount;
302
+ activeDurationMs += Math.max(0, sessionEndedAtMs - sessionStartedAtMs);
303
+ earliestSessionMs = Math.min(earliestSessionMs, sessionStartedAtMs);
304
+ latestSessionMs = Math.max(latestSessionMs, sessionEndedAtMs);
305
+
306
+ const utcHour = stamp.getUTCHours();
307
+ if (hourCounts[utcHour]) {
308
+ hourCounts[utcHour].count += 1;
309
+ }
310
+ const dayIndex = (stamp.getUTCDay() + 6) % 7;
311
+ if (weekdayCounts[dayIndex]) {
312
+ weekdayCounts[dayIndex].count += 1;
313
+ }
314
+
315
+ const cwd = normalizeSessionPathFilter(session.cwd);
316
+ if (cwd) {
317
+ const prev = pathMap.get(cwd) || { count: 0, messageTotal: 0, updatedAtMs: 0 };
318
+ pathMap.set(cwd, {
319
+ count: prev.count + 1,
320
+ messageTotal: prev.messageTotal + messageCount,
321
+ updatedAtMs: Math.max(prev.updatedAtMs, updatedAtMs)
322
+ });
323
+ }
324
+
325
+ const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
326
+ const normalizedTitle = typeof session.title === 'string' && session.title.trim()
327
+ ? session.title.trim()
328
+ : (typeof session.sessionId === 'string' && session.sessionId.trim() ? session.sessionId.trim() : '未命名会话');
329
+ for (const modelId of sessionModels) {
330
+ const prev = modelMap.get(modelId) || {
331
+ count: 0,
332
+ messageTotal: 0,
333
+ tokenTotal: 0,
334
+ sources: new Set()
335
+ };
336
+ prev.count += 1;
337
+ prev.messageTotal += messageCount;
338
+ prev.tokenTotal += sessionTotalTokens;
339
+ prev.sources.add(source);
340
+ modelMap.set(modelId, prev);
341
+ }
342
+
343
+ const sessionEntry = {
344
+ key: [
345
+ source,
346
+ session.sessionId || '',
347
+ session.filePath || normalizedTitle,
348
+ String(updatedAtMs),
349
+ String(messageCount),
350
+ String(sessionIndex)
351
+ ].join(':'),
352
+ title: normalizedTitle,
353
+ source,
354
+ sourceLabel,
355
+ cwd,
356
+ messageCount,
357
+ updatedAt: session.updatedAt || '',
358
+ updatedAtMs,
359
+ updatedAtLabel: formatSessionTimelineTimestamp(session.updatedAt || ''),
360
+ hasExactMessageCount: session.__messageCountExact === true
361
+ };
362
+ recentSessions.push(sessionEntry);
363
+ topSessionsByMessages.push({ ...sessionEntry });
364
+ }
365
+
366
+ const totalSessions = codexTotal + claudeTotal;
367
+ const sourceShare = [
368
+ { key: 'codex', label: 'Codex', value: codexTotal },
369
+ { key: 'claude', label: 'Claude', value: claudeTotal }
370
+ ].map((item) => ({
371
+ ...item,
372
+ percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0,
373
+ messageTotal: sourceMessageTotals[item.key] || 0,
374
+ messagePercent: messageTotal > 0 ? Math.round(((sourceMessageTotals[item.key] || 0) / messageTotal) * 100) : 0,
375
+ avgMessages: item.value > 0 ? Math.round(((sourceMessageTotals[item.key] || 0) / item.value) * 10) / 10 : 0
376
+ }));
377
+
378
+ const topPaths = [...pathMap.entries()]
379
+ .sort((a, b) => b[1].count - a[1].count || b[1].messageTotal - a[1].messageTotal || a[0].localeCompare(b[0], 'zh-Hans-CN'))
380
+ .slice(0, 5)
381
+ .map(([pathValue, meta]) => ({
382
+ path: pathValue,
383
+ count: meta.count,
384
+ messageTotal: meta.messageTotal,
385
+ updatedAtLabel: meta.updatedAtMs ? formatSessionTimelineTimestamp(new Date(meta.updatedAtMs).toISOString()) : ''
386
+ }));
387
+
388
+ const usedModels = [...modelMap.entries()]
389
+ .sort((a, b) => b[1].count - a[1].count)
390
+ .map(([modelId, meta]) => {
391
+ const sourceLabels = [...meta.sources]
392
+ .sort((a, b) => a.localeCompare(b, 'en-US'))
393
+ .map((source) => (source === 'codex' ? 'Codex' : 'Claude Code'));
394
+ return {
395
+ key: modelId,
396
+ model: modelId,
397
+ count: meta.count,
398
+ messageTotal: meta.messageTotal,
399
+ tokenTotal: meta.tokenTotal,
400
+ sourceLabels
401
+ };
402
+ });
403
+
404
+ const sortedRecentSessions = recentSessions
405
+ .sort((a, b) => b.updatedAtMs - a.updatedAtMs || b.messageCount - a.messageCount || a.title.localeCompare(b.title, 'zh-Hans-CN'))
406
+ .slice(0, 6);
407
+
408
+ const missingModelProviders = [...missingModelProviderMap.values()]
409
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label, 'zh-Hans-CN'));
410
+ const missingModelSessionsPreview = [...missingModelSessionMap.values()]
411
+ .sort((a, b) => b.updatedAtMs - a.updatedAtMs || a.title.localeCompare(b.title, 'zh-Hans-CN'))
412
+ .slice(0, 5);
413
+
414
+ const modelCoverage = {
415
+ totalSessions,
416
+ modeledSessions: Math.max(0, totalSessions - missingModelSessions),
417
+ missingModelSessions,
418
+ providerOnlySessions,
419
+ missingModelSourceTotals,
420
+ missingModelProviders,
421
+ missingModelSessionsPreview,
422
+ coveragePercent: totalSessions > 0 ? Math.round(((totalSessions - missingModelSessions) / totalSessions) * 100) : 0
423
+ };
424
+
425
+ const sortedTopSessionsByMessages = topSessionsByMessages
426
+ .sort((a, b) => b.messageCount - a.messageCount || b.updatedAtMs - a.updatedAtMs || a.title.localeCompare(b.title, 'zh-Hans-CN'))
427
+ .slice(0, 6);
428
+
429
+ const maxSessionBucket = buckets.reduce((max, item) => Math.max(max, item.totalSessions), 0);
430
+ const maxMessageBucket = buckets.reduce((max, item) => Math.max(max, item.totalMessages), 0);
431
+ const maxHourCount = hourCounts.reduce((max, item) => Math.max(max, item.count), 0);
432
+ const maxWeekdayCount = weekdayCounts.reduce((max, item) => Math.max(max, item.count), 0);
433
+ const busiestDay = [...buckets]
434
+ .sort((a, b) => b.totalSessions - a.totalSessions || b.totalMessages - a.totalMessages || a.key.localeCompare(b.key, 'zh-Hans-CN'))[0] || null;
435
+ const busiestHour = [...hourCounts]
436
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key, 'zh-Hans-CN'))[0] || null;
437
+ const activeDays = buckets.filter((item) => item.totalSessions > 0).length;
438
+ const avgMessagesPerSession = totalSessions > 0 ? Math.round((messageTotal / totalSessions) * 10) / 10 : 0;
439
+ const avgSessionsPerActiveDay = activeDays > 0 ? Math.round((totalSessions / activeDays) * 10) / 10 : 0;
440
+ const totalDurationMs = Number.isFinite(earliestSessionMs) && latestSessionMs > 0
441
+ ? Math.max(0, latestSessionMs - earliestSessionMs)
442
+ : 0;
443
+
444
+ return {
445
+ range,
446
+ buckets,
447
+ filteredSessions,
448
+ summary: {
449
+ totalSessions,
450
+ totalMessages: messageTotal,
451
+ totalTokens,
452
+ totalContextWindow,
453
+ activeDurationMs,
454
+ totalDurationMs,
455
+ codexTotal,
456
+ claudeTotal,
457
+ activeDays,
458
+ avgMessagesPerSession,
459
+ avgSessionsPerActiveDay,
460
+ busiestDay: busiestDay
461
+ ? {
462
+ key: busiestDay.key,
463
+ label: busiestDay.label,
464
+ totalSessions: busiestDay.totalSessions,
465
+ totalMessages: busiestDay.totalMessages
466
+ }
467
+ : null,
468
+ busiestHour: busiestHour
469
+ ? {
470
+ key: busiestHour.key,
471
+ label: `${busiestHour.label}:00`,
472
+ count: busiestHour.count
473
+ }
474
+ : null
475
+ },
476
+ sourceShare,
477
+ usedModels,
478
+ modelCoverage,
479
+ topPaths,
480
+ recentSessions: sortedRecentSessions,
481
+ topSessionsByMessages: sortedTopSessionsByMessages,
482
+ hourActivity: hourCounts.map((item) => ({
483
+ ...item,
484
+ percent: maxHourCount > 0 ? Math.round((item.count / maxHourCount) * 100) : 0
485
+ })),
486
+ weekdayActivity: weekdayCounts.map((item) => ({
487
+ ...item,
488
+ percent: maxWeekdayCount > 0 ? Math.round((item.count / maxWeekdayCount) * 100) : 0
489
+ })),
490
+ maxSessionBucket,
491
+ maxMessageBucket,
492
+ maxHourCount,
493
+ maxWeekdayCount
494
+ };
495
+ }
496
+
497
+ export function buildSessionTimelineNodes(messages = [], options = {}) {
498
+ const list = Array.isArray(messages) ? messages : [];
499
+ const getKey = typeof options.getKey === 'function'
500
+ ? options.getKey
501
+ : ((_message, index) => `msg-${index}`);
502
+ const total = list.length;
503
+ const rawMaxMarkers = Number(options.maxMarkers);
504
+ const maxMarkers = Number.isFinite(rawMaxMarkers)
505
+ ? Math.max(1, Math.min(80, Math.floor(rawMaxMarkers)))
506
+ : 30;
507
+
508
+ const buildSingleNode = (message, index) => {
509
+ const role = normalizeSessionMessageRole(message && (message.normalizedRole || message.role));
510
+ const roleMeta = toRoleMeta(role);
511
+ const key = String(getKey(message, index) || `msg-${index}`);
512
+ const displayTime = formatSessionTimelineTimestamp(message && message.timestamp ? message.timestamp : '');
513
+ const title = displayTime
514
+ ? `#${index + 1} · ${roleMeta.roleLabel} · ${displayTime}`
515
+ : `#${index + 1} · ${roleMeta.roleLabel}`;
516
+ const percent = total <= 1 ? 0 : (index / (total - 1)) * 100;
517
+ return {
518
+ key,
519
+ role: roleMeta.role,
520
+ roleLabel: roleMeta.roleLabel,
521
+ roleShort: roleMeta.roleShort,
522
+ displayTime,
523
+ title,
524
+ percent,
525
+ safePercent: clampTimelinePercent(percent)
526
+ };
527
+ };
528
+
529
+ if (total <= maxMarkers) {
530
+ return list.map((message, index) => buildSingleNode(message, index));
531
+ }
532
+
533
+ const nodes = [];
534
+ const bucketWidth = total / maxMarkers;
535
+ for (let bucket = 0; bucket < maxMarkers; bucket += 1) {
536
+ let start = Math.floor(bucket * bucketWidth);
537
+ if (nodes.length && start <= nodes[nodes.length - 1].endIndex) {
538
+ start = nodes[nodes.length - 1].endIndex + 1;
539
+ }
540
+ if (start >= total) {
541
+ break;
542
+ }
543
+ let end = Math.floor((bucket + 1) * bucketWidth) - 1;
544
+ end = Math.max(start, Math.min(total - 1, end));
545
+ const targetIndex = Math.min(total - 1, start + Math.floor((end - start) / 2));
546
+ const targetMessage = list[targetIndex] || null;
547
+ const key = String(getKey(targetMessage, targetIndex) || `msg-${targetIndex}`);
548
+ const percent = total <= 1 ? 0 : (targetIndex / (total - 1)) * 100;
549
+ const messagesInGroup = end - start + 1;
550
+ const roleSet = new Set();
551
+ for (let i = start; i <= end; i += 1) {
552
+ roleSet.add(normalizeSessionMessageRole(list[i] && (list[i].normalizedRole || list[i].role)));
553
+ }
554
+ const roleValue = roleSet.size === 1 ? Array.from(roleSet)[0] : 'mixed';
555
+ const roleMeta = toRoleMeta(roleValue);
556
+ const firstTime = formatSessionTimelineTimestamp(list[start] && list[start].timestamp ? list[start].timestamp : '');
557
+ const lastTime = formatSessionTimelineTimestamp(list[end] && list[end].timestamp ? list[end].timestamp : '');
558
+ let displayTime = '';
559
+ if (firstTime && lastTime) {
560
+ displayTime = firstTime === lastTime ? firstTime : `${firstTime} ~ ${lastTime}`;
561
+ } else {
562
+ displayTime = firstTime || lastTime;
563
+ }
564
+ const titleBase = `#${start + 1}-${end + 1} · ${messagesInGroup} msgs · ${roleMeta.roleLabel}`;
565
+ const title = displayTime ? `${titleBase} · ${displayTime}` : titleBase;
566
+ nodes.push({
567
+ key,
568
+ role: roleMeta.role,
569
+ roleLabel: roleMeta.roleLabel,
570
+ roleShort: roleMeta.roleShort,
571
+ displayTime,
572
+ title,
573
+ percent,
574
+ safePercent: clampTimelinePercent(percent),
575
+ startIndex: start,
576
+ endIndex: end,
577
+ messageCount: messagesInGroup
578
+ });
579
+ }
580
+ return nodes;
581
+ }