coding-tool-x 3.7.4 → 3.7.6

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 (51) hide show
  1. package/dist/web/assets/{Analytics-BmPIW7oK.js → Analytics-wCRLv26m.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CG4j4ua4.js → ConfigTemplates-aa1blEHh.js} +1 -1
  3. package/dist/web/assets/Home-CqnBXFxj.css +1 -0
  4. package/dist/web/assets/Home-eIxO50-k.js +1 -0
  5. package/dist/web/assets/{PluginManager-CCJ7mo35.js → PluginManager-BAwGFNjN.js} +1 -1
  6. package/dist/web/assets/ProjectList-CRiAEixW.js +1 -0
  7. package/dist/web/assets/ProjectList-Dw2omgOH.css +1 -0
  8. package/dist/web/assets/SessionList-BQ0FW6Gd.js +1 -0
  9. package/dist/web/assets/SessionList-kYJ6-Ies.css +1 -0
  10. package/dist/web/assets/{SkillManager-DmOTHcQ7.js → SkillManager-Bhzs1O5S.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DLVj0zlu.js → WorkspaceManager-CLdX-pTT.js} +1 -1
  12. package/dist/web/assets/index-B_nFqixU.css +1 -0
  13. package/dist/web/assets/index-ClMNA9Hf.js +2 -0
  14. package/dist/web/index.html +2 -2
  15. package/package.json +1 -1
  16. package/src/server/api/codex-projects.js +7 -11
  17. package/src/server/api/codex-sessions.js +30 -21
  18. package/src/server/api/dashboard.js +24 -36
  19. package/src/server/api/gemini-projects.js +7 -11
  20. package/src/server/api/gemini-sessions.js +28 -26
  21. package/src/server/api/omp-channels.js +50 -7
  22. package/src/server/api/omp-projects.js +6 -11
  23. package/src/server/api/omp-sessions.js +31 -17
  24. package/src/server/api/opencode-projects.js +6 -11
  25. package/src/server/api/opencode-sessions.js +28 -21
  26. package/src/server/api/projects.js +7 -28
  27. package/src/server/api/sessions.js +32 -16
  28. package/src/server/services/channel-balance.js +2 -1
  29. package/src/server/services/codex-sessions.js +23 -10
  30. package/src/server/services/dashboard-snapshot-worker.js +216 -0
  31. package/src/server/services/gemini-sessions.js +15 -3
  32. package/src/server/services/native-oauth-adapters.js +2 -1
  33. package/src/server/services/omp-auth-provider-worker.js +29 -0
  34. package/src/server/services/omp-auth-providers.js +137 -11
  35. package/src/server/services/omp-config.js +7 -4
  36. package/src/server/services/omp-sessions.js +38 -14
  37. package/src/server/services/opencode-sessions.js +9 -6
  38. package/src/server/services/plugins-service.js +2 -1
  39. package/src/server/services/project-snapshots.js +8 -1
  40. package/src/server/services/session-snapshot-worker.js +158 -0
  41. package/src/server/services/session-snapshots.js +145 -0
  42. package/src/server/services/sessions.js +6 -4
  43. package/src/server/services/snapshot-cache.js +123 -13
  44. package/dist/web/assets/Home-B9yAAZrD.js +0 -1
  45. package/dist/web/assets/Home-DSTUSxrT.css +0 -1
  46. package/dist/web/assets/ProjectList-D3a0oRum.css +0 -1
  47. package/dist/web/assets/ProjectList-DVUs3o8x.js +0 -1
  48. package/dist/web/assets/SessionList-D12muXvx.css +0 -1
  49. package/dist/web/assets/SessionList-DwssBydw.js +0 -1
  50. package/dist/web/assets/index-BjdNYwSs.js +0 -2
  51. package/dist/web/assets/index-kQCC5dlu.css +0 -1
@@ -16,6 +16,14 @@ const {
16
16
  } = require('../services/omp-sessions');
17
17
  const { loadAliases } = require('../services/alias');
18
18
  const { broadcastLog } = require('../websocket-server');
19
+ const {
20
+ defaultProjectInfo,
21
+ emptySessionList,
22
+ getSessionListSnapshot,
23
+ invalidateSessionSnapshots,
24
+ runSessionSnapshotWorker
25
+ } = require('../services/session-snapshots');
26
+ const { invalidateProjectSnapshots } = require('../services/project-snapshots');
19
27
 
20
28
  function isNotFoundError(error) {
21
29
  if (!error || !error.message) return false;
@@ -81,27 +89,22 @@ module.exports = () => {
81
89
  }
82
90
  });
83
91
 
84
- router.get('/:projectName', (req, res) => {
92
+ router.get('/:projectName', async (req, res) => {
85
93
  try {
86
94
  if (!isOmpInstalled()) {
87
95
  return res.status(404).json({ error: 'OMP CLI not installed' });
88
96
  }
89
97
  const { projectName } = req.params;
90
- const sessions = getSessionsByProject(projectName);
91
- const aliases = loadAliases();
92
- const project = getProjects().find(p => p.name === projectName);
93
- const totalSize = sessions.reduce((sum, session) => sum + (session.size || 0), 0);
94
- res.json({
95
- sessions,
96
- totalSize,
97
- aliases,
98
- projectInfo: {
99
- name: projectName,
100
- fullPath: project?.fullPath || project?.path || projectName,
101
- path: project?.path || projectName,
102
- displayName: project?.displayName || projectName
103
- }
98
+ const force = req.query?.fresh === '1';
99
+ const snapshot = await getSessionListSnapshot('omp', projectName, {
100
+ fallbackValue: emptySessionList(projectName, {
101
+ aliases: loadAliases(),
102
+ projectInfo: defaultProjectInfo(projectName)
103
+ }),
104
+ force,
105
+ refresh: () => runSessionSnapshotWorker('omp', projectName, {}, { force })
104
106
  });
107
+ res.json({ ...snapshot.value, meta: snapshot.meta });
105
108
  } catch (err) {
106
109
  console.error('[OMP API] Failed to get sessions:', err);
107
110
  res.status(500).json({ error: err.message });
@@ -206,7 +209,10 @@ module.exports = () => {
206
209
  if (!isOmpInstalled()) {
207
210
  return res.status(404).json({ error: 'OMP CLI not installed' });
208
211
  }
209
- res.json(deleteSession(req.params.sessionId));
212
+ const result = deleteSession(req.params.sessionId);
213
+ invalidateSessionSnapshots('omp', req.params.projectName);
214
+ invalidateProjectSnapshots('omp');
215
+ res.json(result);
210
216
  } catch (err) {
211
217
  if (isNotFoundError(err)) {
212
218
  return res.status(404).json({ error: err.message });
@@ -244,6 +250,10 @@ module.exports = () => {
244
250
  failed.push({ sessionId, error: err.message });
245
251
  }
246
252
  });
253
+ if (deletedSessionIds.length > 0) {
254
+ invalidateSessionSnapshots('omp', req.params.projectName);
255
+ invalidateProjectSnapshots('omp');
256
+ }
247
257
  res.json({
248
258
  success: failed.length === 0,
249
259
  requestedCount: uniqueSessionIds.length,
@@ -262,7 +272,10 @@ module.exports = () => {
262
272
  if (!isOmpInstalled()) {
263
273
  return res.status(404).json({ error: 'OMP CLI not installed' });
264
274
  }
265
- res.json(forkSession(req.params.sessionId, req.body || {}));
275
+ const result = forkSession(req.params.sessionId, req.body || {});
276
+ invalidateSessionSnapshots('omp', req.params.projectName);
277
+ invalidateProjectSnapshots('omp');
278
+ res.json(result);
266
279
  } catch (err) {
267
280
  if (isNotFoundError(err)) {
268
281
  return res.status(404).json({ error: err.message });
@@ -282,6 +295,7 @@ module.exports = () => {
282
295
  return res.status(400).json({ error: 'order must be an array' });
283
296
  }
284
297
  saveSessionOrder(req.params.projectName, order);
298
+ invalidateSessionSnapshots('omp', req.params.projectName);
285
299
  res.json({ success: true });
286
300
  } catch (err) {
287
301
  console.error('[OMP API] Failed to save session order:', err);
@@ -1,7 +1,6 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const {
4
- getProjects,
5
4
  saveProjectOrder,
6
5
  deleteProject,
7
6
  isOpenCodeInstalled
@@ -11,20 +10,14 @@ const {
11
10
  getProjectListSnapshot,
12
11
  invalidateProjectSnapshots
13
12
  } = require('../services/project-snapshots');
13
+ const { invalidateSessionSnapshots } = require('../services/session-snapshots');
14
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
14
15
 
15
16
  function isNotFoundError(error) {
16
17
  return !!(error && error.message === 'Project not found');
17
18
  }
18
19
 
19
20
  module.exports = (config) => {
20
- function buildProjectsPayload() {
21
- const projects = getProjects();
22
- return {
23
- projects,
24
- currentProject: projects[0] ? projects[0].name : null
25
- };
26
- }
27
-
28
21
  /**
29
22
  * GET /api/opencode/projects
30
23
  * 获取所有 OpenCode 项目列表
@@ -40,10 +33,11 @@ module.exports = (config) => {
40
33
  });
41
34
  }
42
35
 
36
+ const force = req.query?.fresh === '1';
43
37
  const snapshot = await getProjectListSnapshot('opencode', {
44
38
  fallbackValue: emptyProjectList(null),
45
- force: req.query?.fresh === '1',
46
- refresh: buildProjectsPayload
39
+ force,
40
+ refresh: () => runDashboardSnapshotWorker('projects', 'opencode', config, { force })
47
41
  });
48
42
  res.json({ ...snapshot.value, meta: snapshot.meta });
49
43
  } catch (err) {
@@ -100,6 +94,7 @@ module.exports = (config) => {
100
94
  const { projectName } = req.params;
101
95
  const result = deleteProject(projectName);
102
96
  invalidateProjectSnapshots('opencode');
97
+ invalidateSessionSnapshots('opencode', projectName);
103
98
  res.json(result);
104
99
  } catch (err) {
105
100
  if (isNotFoundError(err)) {
@@ -15,6 +15,14 @@ const {
15
15
  const { loadAliases } = require('../services/alias');
16
16
  const { broadcastLog } = require('../websocket-server');
17
17
  const { HOME_DIR } = require('../../config/paths');
18
+ const {
19
+ defaultProjectInfo,
20
+ emptySessionList,
21
+ getSessionListSnapshot,
22
+ invalidateSessionSnapshots,
23
+ runSessionSnapshotWorker
24
+ } = require('../services/session-snapshots');
25
+ const { invalidateProjectSnapshots } = require('../services/project-snapshots');
18
26
 
19
27
  function isNotFoundError(error) {
20
28
  if (!error || !error.message) {
@@ -117,34 +125,23 @@ module.exports = (config) => {
117
125
  * GET /api/opencode/sessions/:projectName
118
126
  * 获取项目的所有会话
119
127
  */
120
- router.get('/:projectName', (req, res) => {
128
+ router.get('/:projectName', async (req, res) => {
121
129
  try {
122
130
  if (!isOpenCodeInstalled()) {
123
131
  return res.status(404).json({ error: 'OpenCode CLI not installed' });
124
132
  }
125
133
 
126
134
  const { projectName } = req.params;
127
- const sessions = getSessionsByProject(projectName);
128
- const aliases = loadAliases();
129
- const projects = getProjects();
130
- const project = projects.find(p => p.name === projectName);
131
-
132
- // 计算总大小
133
- const totalSize = sessions.reduce((sum, session) => {
134
- return sum + (session.size || 0);
135
- }, 0);
136
-
137
- res.json({
138
- sessions,
139
- totalSize,
140
- aliases,
141
- projectInfo: {
142
- name: projectName,
143
- fullPath: project?.fullPath || projectName,
144
- path: project?.path || projectName,
145
- displayName: project?.displayName || projectName
146
- }
135
+ const force = req.query?.fresh === '1';
136
+ const snapshot = await getSessionListSnapshot('opencode', projectName, {
137
+ fallbackValue: emptySessionList(projectName, {
138
+ aliases: loadAliases(),
139
+ projectInfo: defaultProjectInfo(projectName)
140
+ }),
141
+ force,
142
+ refresh: () => runSessionSnapshotWorker('opencode', projectName, {}, { force })
147
143
  });
144
+ res.json({ ...snapshot.value, meta: snapshot.meta });
148
145
  } catch (err) {
149
146
  console.error('[OpenCode API] Failed to get sessions:', err);
150
147
  res.status(500).json({ error: err.message });
@@ -294,6 +291,8 @@ module.exports = (config) => {
294
291
 
295
292
  const { sessionId } = req.params;
296
293
  const result = deleteSession(sessionId);
294
+ invalidateSessionSnapshots('opencode', req.params.projectName);
295
+ invalidateProjectSnapshots('opencode');
297
296
 
298
297
  res.json(result);
299
298
  } catch (err) {
@@ -347,6 +346,11 @@ module.exports = (config) => {
347
346
  }
348
347
  });
349
348
 
349
+ if (deletedSessionIds.length > 0) {
350
+ invalidateSessionSnapshots('opencode', req.params.projectName);
351
+ invalidateProjectSnapshots('opencode');
352
+ }
353
+
350
354
  res.json({
351
355
  success: failed.length === 0,
352
356
  requestedCount: uniqueSessionIds.length,
@@ -372,6 +376,8 @@ module.exports = (config) => {
372
376
 
373
377
  const { sessionId } = req.params;
374
378
  const result = forkSession(sessionId);
379
+ invalidateSessionSnapshots('opencode', req.params.projectName);
380
+ invalidateProjectSnapshots('opencode');
375
381
  res.json(result);
376
382
  } catch (err) {
377
383
  if (isNotFoundError(err)) {
@@ -401,6 +407,7 @@ module.exports = (config) => {
401
407
  }
402
408
 
403
409
  saveSessionOrder(projectName, order);
410
+ invalidateSessionSnapshots('opencode', projectName);
404
411
  res.json({ success: true });
405
412
  } catch (err) {
406
413
  console.error('[OpenCode API] Failed to save session order:', err);
@@ -1,45 +1,23 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
- const { getProjectsWithStats, saveProjectOrder, getProjectOrder, deleteProject } = require('../services/sessions');
3
+ const { saveProjectOrder, deleteProject } = require('../services/sessions');
4
4
  const {
5
5
  emptyProjectList,
6
6
  getProjectListSnapshot,
7
7
  invalidateProjectSnapshots
8
8
  } = require('../services/project-snapshots');
9
+ const { invalidateSessionSnapshots } = require('../services/session-snapshots');
10
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
9
11
 
10
12
  module.exports = (config) => {
11
- async function buildProjectsPayload() {
12
- const projects = await getProjectsWithStats(config);
13
- const order = getProjectOrder(config);
14
-
15
- // Sort projects by saved order
16
- let sortedProjects = projects;
17
- if (order && order.length > 0) {
18
- const orderMap = new Map(order.map((name, idx) => [name, idx]));
19
- sortedProjects = [...projects].sort((a, b) => {
20
- const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
21
- const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
22
- if (aIdx === bIdx) {
23
- // Both are new, sort by lastUsed
24
- return (b.lastUsed || 0) - (a.lastUsed || 0);
25
- }
26
- return aIdx - bIdx;
27
- });
28
- }
29
-
30
- return {
31
- projects: sortedProjects,
32
- currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
33
- };
34
- }
35
-
36
13
  // GET /api/projects - Get all projects with stats
37
14
  router.get('/', async (req, res) => {
38
15
  try {
16
+ const force = req.query?.fresh === '1';
39
17
  const snapshot = await getProjectListSnapshot('claude', {
40
18
  fallbackValue: emptyProjectList(config.currentProject || null),
41
- force: req.query?.fresh === '1',
42
- refresh: buildProjectsPayload
19
+ force,
20
+ refresh: () => runDashboardSnapshotWorker('projects', 'claude', config, { force })
43
21
  });
44
22
  res.json({ ...snapshot.value, meta: snapshot.meta });
45
23
  } catch (error) {
@@ -130,6 +108,7 @@ module.exports = (config) => {
130
108
  const { projectName } = req.params;
131
109
  deleteProject(config, projectName);
132
110
  invalidateProjectSnapshots('claude');
111
+ invalidateSessionSnapshots('claude', projectName);
133
112
  res.json({ success: true });
134
113
  } catch (error) {
135
114
  console.error('Error deleting project:', error);
@@ -3,10 +3,18 @@ const router = express.Router();
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
5
  const readline = require('readline');
6
- const { getSessionsForProject, deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
6
+ const { deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
7
7
  const { loadAliases } = require('../services/alias');
8
8
  const { broadcastLog } = require('../websocket-server');
9
9
  const { NATIVE_PATHS } = require('../../config/paths');
10
+ const {
11
+ defaultProjectInfo,
12
+ emptySessionList,
13
+ getSessionListSnapshot,
14
+ invalidateSessionSnapshots,
15
+ runSessionSnapshotWorker
16
+ } = require('../services/session-snapshots');
17
+ const { invalidateProjectSnapshots } = require('../services/project-snapshots');
10
18
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
11
19
 
12
20
  function resolveClaudeSessionFile(projectName, sessionId, fullPath) {
@@ -158,22 +166,16 @@ module.exports = (config) => {
158
166
  router.get('/:projectName', async (req, res) => {
159
167
  try {
160
168
  const { projectName } = req.params;
161
- const result = await getSessionsForProject(config, projectName);
162
- const aliases = loadAliases();
163
-
164
- // Parse project path info
165
- const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
166
-
167
- res.json({
168
- sessions: result.sessions,
169
- totalSize: result.totalSize,
170
- aliases,
171
- projectInfo: {
172
- name: projectName,
173
- displayName,
174
- fullPath
175
- }
169
+ const force = req.query?.fresh === '1';
170
+ const snapshot = await getSessionListSnapshot('claude', projectName, {
171
+ fallbackValue: emptySessionList(projectName, {
172
+ aliases: loadAliases(),
173
+ projectInfo: defaultProjectInfo(projectName)
174
+ }),
175
+ force,
176
+ refresh: () => runSessionSnapshotWorker('claude', projectName, config, { force })
176
177
  });
178
+ res.json({ ...snapshot.value, meta: snapshot.meta });
177
179
  } catch (error) {
178
180
  console.error('Error fetching sessions:', error);
179
181
  res.status(500).json({ error: error.message });
@@ -185,6 +187,8 @@ module.exports = (config) => {
185
187
  try {
186
188
  const { projectName, sessionId } = req.params;
187
189
  const result = deleteSession(config, projectName, sessionId);
190
+ invalidateSessionSnapshots('claude', projectName);
191
+ invalidateProjectSnapshots('claude');
188
192
  res.json(result);
189
193
  } catch (error) {
190
194
  console.error('Error deleting session:', error);
@@ -228,6 +232,11 @@ module.exports = (config) => {
228
232
  }
229
233
  });
230
234
 
235
+ if (deletedSessionIds.length > 0) {
236
+ invalidateSessionSnapshots('claude', projectName);
237
+ invalidateProjectSnapshots('claude');
238
+ }
239
+
231
240
  res.json({
232
241
  success: failed.length === 0,
233
242
  requestedCount: uniqueSessionIds.length,
@@ -246,6 +255,8 @@ module.exports = (config) => {
246
255
  try {
247
256
  const { projectName, sessionId } = req.params;
248
257
  const result = forkSession(config, projectName, sessionId, normalizeForkOptions(req.body));
258
+ invalidateSessionSnapshots('claude', projectName);
259
+ invalidateProjectSnapshots('claude');
249
260
  res.json(result);
250
261
  } catch (error) {
251
262
  console.error('Error forking session:', error);
@@ -330,6 +341,10 @@ module.exports = (config) => {
330
341
  timestamp: Date.now()
331
342
  });
332
343
 
344
+ const snapshotSource = toolType === 'codex' || toolType === 'gemini' ? toolType : 'claude';
345
+ invalidateSessionSnapshots(snapshotSource, projectName);
346
+ invalidateProjectSnapshots(snapshotSource);
347
+
333
348
  res.json({
334
349
  success: true,
335
350
  sessionId: newSessionId,
@@ -352,6 +367,7 @@ module.exports = (config) => {
352
367
  return res.status(400).json({ error: 'order must be an array' });
353
368
  }
354
369
  saveSessionOrder(projectName, order);
370
+ invalidateSessionSnapshots('claude', projectName);
355
371
  res.json({ success: true });
356
372
  } catch (error) {
357
373
  console.error('Error saving session order:', error);
@@ -1737,7 +1737,8 @@ async function refreshChannelBalance(source, channelId) {
1737
1737
 
1738
1738
  const entry = await refreshSnapshot(
1739
1739
  buildBalanceSnapshotKey(normalizedSource, channel),
1740
- () => refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true })
1740
+ () => refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true }),
1741
+ { bypassInflight: true }
1741
1742
  );
1742
1743
  const balance = entry.value;
1743
1744
  return {
@@ -227,10 +227,14 @@ function normalizeSession(codexSession) {
227
227
  * 聚合项目列表
228
228
  * @returns {Array} 项目对象数组
229
229
  */
230
- function getProjects() {
231
- const cached = globalCache.get(CODEX_PROJECTS_CACHE_KEY);
232
- if (cached) {
233
- return cached;
230
+ function getProjects(options = {}) {
231
+ if (options.force) {
232
+ invalidateCodexSessionCaches();
233
+ } else {
234
+ const cached = globalCache.get(CODEX_PROJECTS_CACHE_KEY);
235
+ if (cached) {
236
+ return cached;
237
+ }
234
238
  }
235
239
 
236
240
  const sessions = getAllSessions();
@@ -302,11 +306,15 @@ function getProjects() {
302
306
  * @param {string} projectName - 项目名称
303
307
  * @returns {Array} 归一化的会话数组
304
308
  */
305
- function getSessionsByProject(projectName) {
309
+ function getSessionsByProject(projectName, options = {}) {
306
310
  const cacheKey = getCodexSessionsCacheKey(projectName);
307
- const cached = globalCache.get(cacheKey);
308
- if (cached) {
309
- return cached;
311
+ if (options.force) {
312
+ invalidateCodexSessionCaches({ projectName });
313
+ } else {
314
+ const cached = globalCache.get(cacheKey);
315
+ if (cached) {
316
+ return cached;
317
+ }
310
318
  }
311
319
 
312
320
  const sessions = getAllSessions();
@@ -975,9 +983,14 @@ function calculateProjectAndSessionCounts() {
975
983
  /**
976
984
  * 获取 Codex 项目与会话数量(用于仪表盘轻量统计)
977
985
  */
978
- function getProjectAndSessionCounts() {
986
+ function getProjectAndSessionCounts(options = {}) {
979
987
  const now = Date.now();
980
- if (countsCache.expiresAt > now) {
988
+ if (options.force) {
989
+ countsCache.expiresAt = 0;
990
+ scanFilesCache.expiresAt = 0;
991
+ sessionFileIndexCache.expiresAt = 0;
992
+ }
993
+ if (!options.force && countsCache.expiresAt > now) {
981
994
  return countsCache.value;
982
995
  }
983
996
 
@@ -0,0 +1,216 @@
1
+ 'use strict';
2
+
3
+ const { fork } = require('child_process');
4
+
5
+ const DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS = 180 * 1000;
6
+
7
+ function sortClaudeProjects(projects, order = []) {
8
+ if (!Array.isArray(order) || order.length === 0) {
9
+ return projects;
10
+ }
11
+ const orderMap = new Map(order.map((name, idx) => [name, idx]));
12
+ return [...projects].sort((a, b) => {
13
+ const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
14
+ const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
15
+ if (aIdx === bIdx) {
16
+ return (b.lastUsed || 0) - (a.lastUsed || 0);
17
+ }
18
+ return aIdx - bIdx;
19
+ });
20
+ }
21
+
22
+ async function buildProjectsPayload(source, config = {}, options = {}) {
23
+ switch (source) {
24
+ case 'claude': {
25
+ const { getProjectsWithStats, getProjectOrder } = require('./sessions');
26
+ const projects = await getProjectsWithStats(config, { force: options.force === true });
27
+ const sortedProjects = sortClaudeProjects(projects, getProjectOrder(config));
28
+ return {
29
+ projects: sortedProjects,
30
+ currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
31
+ };
32
+ }
33
+ case 'codex': {
34
+ const { getProjects } = require('./codex-sessions');
35
+ const projects = getProjects({ force: options.force === true });
36
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
37
+ }
38
+ case 'gemini': {
39
+ const { getProjects } = require('./gemini-sessions');
40
+ const projects = getProjects({ force: options.force === true });
41
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
42
+ }
43
+ case 'opencode': {
44
+ const { getProjects } = require('./opencode-sessions');
45
+ const projects = getProjects({ force: options.force === true });
46
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
47
+ }
48
+ case 'omp': {
49
+ const { getProjects } = require('./omp-sessions');
50
+ const projects = getProjects({ force: options.force === true });
51
+ return { projects, currentProject: projects[0] ? projects[0].name : null };
52
+ }
53
+ default:
54
+ throw new Error(`Unsupported project snapshot source: ${source}`);
55
+ }
56
+ }
57
+
58
+ function buildCountsPayload(source, config = {}, options = {}) {
59
+ switch (source) {
60
+ case 'claude':
61
+ return require('./sessions').getProjectAndSessionCounts(config, { force: options.force === true });
62
+ case 'codex':
63
+ return require('./codex-sessions').getProjectAndSessionCounts({ force: options.force === true });
64
+ case 'gemini':
65
+ return require('./gemini-sessions').getProjectAndSessionCounts({ force: options.force === true });
66
+ case 'opencode':
67
+ return require('./opencode-sessions').getProjectAndSessionCounts({ force: options.force === true });
68
+ case 'omp':
69
+ return require('./omp-sessions').getProjectAndSessionCounts({ force: options.force === true });
70
+ default:
71
+ throw new Error(`Unsupported counts snapshot source: ${source}`);
72
+ }
73
+ }
74
+
75
+ function buildTodayStatsPayload(source) {
76
+ switch (source) {
77
+ case 'claude':
78
+ return require('./claude-statistics-service').getTodayStatistics();
79
+ case 'codex':
80
+ return require('./codex-statistics-service').getTodayStatistics();
81
+ case 'gemini':
82
+ return require('./gemini-statistics-service').getTodayStatistics();
83
+ case 'opencode':
84
+ return require('./opencode-statistics-service').getTodayStatistics();
85
+ case 'omp':
86
+ return require('./omp-statistics-service').getTodayStatistics();
87
+ default:
88
+ throw new Error(`Unsupported today stats snapshot source: ${source}`);
89
+ }
90
+ }
91
+
92
+ function buildChannelsPayload(source) {
93
+ switch (source) {
94
+ case 'claude':
95
+ return require('./channels').getAllChannels();
96
+ case 'codex':
97
+ return require('./codex-channels').getChannels();
98
+ case 'gemini':
99
+ return require('./gemini-channels').getChannels();
100
+ case 'opencode':
101
+ return require('./opencode-channels').getChannels();
102
+ case 'omp':
103
+ return require('./omp-channels').getChannels();
104
+ default:
105
+ throw new Error(`Unsupported channels snapshot source: ${source}`);
106
+ }
107
+ }
108
+
109
+ async function buildPayload({ kind, source, config, options }) {
110
+ const snapshotOptions = options || {};
111
+ switch (kind) {
112
+ case 'projects':
113
+ return buildProjectsPayload(source, config || {}, snapshotOptions);
114
+ case 'counts':
115
+ return buildCountsPayload(source, config || {}, snapshotOptions);
116
+ case 'todayStats':
117
+ return buildTodayStatsPayload(source);
118
+ case 'channels':
119
+ return buildChannelsPayload(source);
120
+ default:
121
+ throw new Error(`Unsupported dashboard snapshot kind: ${kind}`);
122
+ }
123
+ }
124
+
125
+ function runDashboardSnapshotWorker(kind, source, config = {}, options = {}) {
126
+ if (process.env.NODE_ENV === 'test') {
127
+ return Promise.resolve(buildPayload({ kind, source, config, options }));
128
+ }
129
+
130
+ return new Promise((resolve, reject) => {
131
+ const child = fork(__filename, [], {
132
+ stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
133
+ env: {
134
+ ...process.env,
135
+ CC_TOOL_DASHBOARD_SNAPSHOT_WORKER: '1'
136
+ }
137
+ });
138
+
139
+ let settled = false;
140
+ let stderr = '';
141
+ let timeout = null;
142
+
143
+ const finish = (error, value) => {
144
+ if (settled) return;
145
+ settled = true;
146
+ if (timeout) {
147
+ clearTimeout(timeout);
148
+ }
149
+ child.removeAllListeners();
150
+ if (!child.killed) {
151
+ child.kill();
152
+ }
153
+ if (error) {
154
+ reject(error);
155
+ } else {
156
+ resolve(value);
157
+ }
158
+ };
159
+
160
+ timeout = setTimeout(() => {
161
+ finish(new Error(`Dashboard snapshot refresh timed out for ${kind}/${source}`));
162
+ }, DASHBOARD_SNAPSHOT_WORKER_TIMEOUT_MS);
163
+
164
+ child.stderr.on('data', (chunk) => {
165
+ stderr += chunk.toString();
166
+ if (stderr.length > 4096) {
167
+ stderr = stderr.slice(-4096);
168
+ }
169
+ });
170
+
171
+ child.on('message', (message) => {
172
+ if (!message || typeof message !== 'object') return;
173
+ if (message.ok) {
174
+ finish(null, message.value);
175
+ } else {
176
+ finish(new Error(message.error || `Dashboard snapshot refresh failed for ${kind}/${source}`));
177
+ }
178
+ });
179
+
180
+ child.on('error', (error) => finish(error));
181
+ child.on('exit', (code, signal) => {
182
+ if (settled) return;
183
+ const suffix = stderr ? `: ${stderr.trim()}` : '';
184
+ finish(new Error(`Dashboard snapshot worker exited (${code || signal || 'unknown'})${suffix}`));
185
+ });
186
+
187
+ child.send({ kind, source, config, options });
188
+ });
189
+ }
190
+
191
+ function attachWorkerHandler() {
192
+ process.on('message', async (message) => {
193
+ try {
194
+ const value = await buildPayload(message || {});
195
+ if (process.send) {
196
+ process.send({ ok: true, value }, () => process.exit(0));
197
+ return;
198
+ }
199
+ } catch (error) {
200
+ if (process.send) {
201
+ process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
202
+ return;
203
+ }
204
+ }
205
+ process.exit(0);
206
+ });
207
+ }
208
+
209
+ if (process.env.CC_TOOL_DASHBOARD_SNAPSHOT_WORKER === '1' || require.main === module) {
210
+ attachWorkerHandler();
211
+ }
212
+
213
+ module.exports = {
214
+ buildPayload,
215
+ runDashboardSnapshotWorker
216
+ };