coding-tool-x 3.7.3 → 3.7.5

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 (44) hide show
  1. package/dist/web/assets/{Analytics-BqDjJUJl.js → Analytics-DnnufrQi.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-DV2KFuC4.js → ConfigTemplates-BTVhMSfe.js} +1 -1
  3. package/dist/web/assets/Home-CqnBXFxj.css +1 -0
  4. package/dist/web/assets/Home-D03PEREx.js +1 -0
  5. package/dist/web/assets/{PluginManager-Bd3hhc5A.js → PluginManager-BiMqEiyQ.js} +1 -1
  6. package/dist/web/assets/{ProjectList-BB4Whns8.js → ProjectList-C6BA3V49.js} +1 -1
  7. package/dist/web/assets/{ProjectList-D3a0oRum.css → ProjectList-DQ5ZCJO2.css} +1 -1
  8. package/dist/web/assets/SessionList--9Bl6EhV.css +1 -0
  9. package/dist/web/assets/SessionList-DChG04jz.js +1 -0
  10. package/dist/web/assets/{SkillManager-DQjRXOFg.js → SkillManager-DAy8k5CL.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-CZeXnxu1.js → WorkspaceManager-Bs2D6R9j.js} +1 -1
  12. package/dist/web/assets/index-DnDBHGCC.js +2 -0
  13. package/dist/web/assets/index-U3TOywtB.css +1 -0
  14. package/dist/web/index.html +2 -2
  15. package/package.json +1 -1
  16. package/src/config/paths.js +2 -0
  17. package/src/server/api/codex-projects.js +22 -9
  18. package/src/server/api/codex-sessions.js +30 -21
  19. package/src/server/api/dashboard.js +174 -94
  20. package/src/server/api/gemini-projects.js +20 -8
  21. package/src/server/api/gemini-sessions.js +28 -26
  22. package/src/server/api/omp-projects.js +19 -7
  23. package/src/server/api/omp-sessions.js +31 -17
  24. package/src/server/api/opencode-projects.js +19 -8
  25. package/src/server/api/opencode-sessions.js +28 -21
  26. package/src/server/api/projects.js +18 -22
  27. package/src/server/api/sessions.js +32 -16
  28. package/src/server/services/channel-balance.js +52 -5
  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/omp-sessions.js +31 -11
  33. package/src/server/services/opencode-sessions.js +9 -6
  34. package/src/server/services/project-snapshots.js +52 -0
  35. package/src/server/services/session-snapshot-worker.js +158 -0
  36. package/src/server/services/session-snapshots.js +137 -0
  37. package/src/server/services/sessions.js +6 -4
  38. package/src/server/services/snapshot-cache.js +361 -0
  39. package/dist/web/assets/Home-D7hovkk9.js +0 -1
  40. package/dist/web/assets/Home-Jom_ZcdY.css +0 -1
  41. package/dist/web/assets/SessionList-CvurKSRK.js +0 -1
  42. package/dist/web/assets/SessionList-D12muXvx.css +0 -1
  43. package/dist/web/assets/index-BovYCFsd.js +0 -2
  44. package/dist/web/assets/index-kQCC5dlu.css +0 -1
@@ -5,30 +5,119 @@ const { loadConfig } = require('../../config/loader');
5
5
  // Services
6
6
  const { loadUIConfig } = require('../services/ui-config');
7
7
  const { loadFavorites } = require('../services/favorites');
8
- const { getAllChannels } = require('../services/channels');
9
8
  const { getProxyStatus } = require('../proxy-server');
10
9
  const { getCodexProxyStatus } = require('../codex-proxy-server');
11
10
  const { getGeminiProxyStatus } = require('../gemini-proxy-server');
12
11
  const { getOpenCodeProxyStatus } = require('../opencode-proxy-server');
13
12
  const { getOmpProxyStatus } = require('../omp-proxy-server');
14
- const { getProjectAndSessionCounts: getClaudeCounts } = require('../services/sessions');
15
- const { getProjectAndSessionCounts: getCodexCounts } = require('../services/codex-sessions');
16
- const { getProjectAndSessionCounts: getGeminiCounts } = require('../services/gemini-sessions');
17
- const { getProjectAndSessionCounts: getOpenCodeCounts } = require('../services/opencode-sessions');
18
- const { getProjectAndSessionCounts: getOmpCounts } = require('../services/omp-sessions');
19
-
20
- // Channel-specific services
21
- const { getChannels: getCodexChannels } = require('../services/codex-channels');
22
- const { getChannels: getGeminiChannels } = require('../services/gemini-channels');
23
- const { getChannels: getOpenCodeChannels } = require('../services/opencode-channels');
24
- const { getChannels: getOmpChannels } = require('../services/omp-channels');
25
-
26
- // Statistics
27
- const { getTodayStatistics: getClaudeTodayStatistics } = require('../services/claude-statistics-service');
28
- const { getTodayStatistics: getCodexTodayStatistics } = require('../services/codex-statistics-service');
29
- const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
30
- const { getTodayStatistics: getOpenCodeTodayStatistics } = require('../services/opencode-statistics-service');
31
- const { getTodayStatistics: getOmpTodayStatistics } = require('../services/omp-statistics-service');
13
+ const { getSnapshot } = require('../services/snapshot-cache');
14
+ const { projectCountKey } = require('../services/project-snapshots');
15
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
16
+
17
+ const SNAPSHOT_TTL_MS = 60 * 1000;
18
+ const DASHBOARD_SNAPSHOT_DEFER_MS = 0;
19
+ const EMPTY_COUNTS = { projectCount: 0, sessionCount: 0 };
20
+ const EMPTY_STATS = { requests: 0, tokens: 0, cost: 0, byModel: {}, byChannel: {} };
21
+
22
+ function safeRead(label, reader, fallback) {
23
+ try {
24
+ return reader();
25
+ } catch (error) {
26
+ console.warn(`[Dashboard] ${label} failed:`, error.message);
27
+ return fallback;
28
+ }
29
+ }
30
+
31
+ function channelSnapshotKey(source) {
32
+ return `dashboard:channels:${source}`;
33
+ }
34
+
35
+ function todayStatsSnapshotKey(source) {
36
+ return `dashboard:today-stats:${source}`;
37
+ }
38
+
39
+ function normalizeChannelTokens(tokens) {
40
+ if (tokens && typeof tokens === 'object') {
41
+ return Number(tokens.total ?? tokens.input ?? 0) || 0;
42
+ }
43
+ return Number(tokens) || 0;
44
+ }
45
+
46
+ function normalizeByChannel(byChannel = {}) {
47
+ if (!byChannel || typeof byChannel !== 'object') {
48
+ return {};
49
+ }
50
+
51
+ return Object.fromEntries(Object.entries(byChannel).map(([channelId, item = {}]) => [
52
+ channelId,
53
+ {
54
+ ...item,
55
+ requests: Number(item.requests) || 0,
56
+ tokens: normalizeChannelTokens(item.tokens),
57
+ cost: Number(item.cost) || 0
58
+ }
59
+ ]));
60
+ }
61
+
62
+ // 格式化统计数据:取 summary、byModel 和 byChannel 中的数据
63
+ function formatStats(stats) {
64
+ if (stats && stats.summary) {
65
+ return {
66
+ requests: stats.summary.requests || 0,
67
+ tokens: normalizeChannelTokens(stats.summary.tokens),
68
+ cost: stats.summary.cost || 0,
69
+ byModel: stats.byModel || {},
70
+ byChannel: normalizeByChannel(stats.byChannel || {})
71
+ };
72
+ }
73
+ return { ...EMPTY_STATS };
74
+ }
75
+
76
+ async function readDashboardSnapshot(kind, source, key, fallbackValue, config = {}, options = {}) {
77
+ const force = options.force === true;
78
+ return getSnapshot(key, {
79
+ ttlMs: SNAPSHOT_TTL_MS,
80
+ fallbackValue,
81
+ force,
82
+ staleWhileForce: force,
83
+ deferMs: DASHBOARD_SNAPSHOT_DEFER_MS,
84
+ refresh: () => runDashboardSnapshotWorker(kind, source, config, { force })
85
+ });
86
+ }
87
+
88
+ function summarizePart(partSnapshots = {}) {
89
+ const items = {};
90
+ let stale = false;
91
+ let refreshing = false;
92
+ let generatedAt = null;
93
+
94
+ Object.entries(partSnapshots).forEach(([source, snapshot]) => {
95
+ const meta = snapshot.meta || {};
96
+ items[source] = meta;
97
+ stale = stale || Boolean(meta.stale);
98
+ refreshing = refreshing || Boolean(meta.refreshing);
99
+ if (meta.generatedAt && (!generatedAt || meta.generatedAt > generatedAt)) {
100
+ generatedAt = meta.generatedAt;
101
+ }
102
+ });
103
+
104
+ return {
105
+ generatedAt,
106
+ stale,
107
+ refreshing,
108
+ items
109
+ };
110
+ }
111
+
112
+ function dashboardMeta(parts) {
113
+ const partValues = Object.values(parts);
114
+ return {
115
+ generatedAt: new Date().toISOString(),
116
+ stale: partValues.some(part => part.stale),
117
+ refreshing: partValues.some(part => part.refreshing),
118
+ parts
119
+ };
120
+ }
32
121
 
33
122
  /**
34
123
  * GET /api/dashboard/init
@@ -37,21 +126,24 @@ const { getTodayStatistics: getOmpTodayStatistics } = require('../services/omp-s
37
126
  router.get('/init', async (req, res) => {
38
127
  try {
39
128
  const config = loadConfig();
129
+ const force = req.query?.fresh === '1' || req.query?.force === '1';
130
+
131
+ const uiConfig = safeRead('uiConfig', () => loadUIConfig(), {});
132
+ const favorites = safeRead('favorites', () => loadFavorites(), {});
133
+ const proxyStatus = {
134
+ claude: safeRead('claude proxy status', () => getProxyStatus(), {}),
135
+ codex: safeRead('codex proxy status', () => getCodexProxyStatus(), {}),
136
+ gemini: safeRead('gemini proxy status', () => getGeminiProxyStatus(), {}),
137
+ opencode: safeRead('opencode proxy status', () => getOpenCodeProxyStatus(), {}),
138
+ omp: safeRead('omp proxy status', () => getOmpProxyStatus(), {})
139
+ };
40
140
 
41
- // 并行获取所有数据
42
141
  const [
43
- uiConfig,
44
- favorites,
45
142
  claudeChannels,
46
143
  codexChannels,
47
144
  geminiChannels,
48
145
  opencodeChannels,
49
146
  ompChannels,
50
- claudeProxyStatus,
51
- codexProxyStatus,
52
- geminiProxyStatus,
53
- opencodeProxyStatus,
54
- ompProxyStatus,
55
147
  claudeTodayStats,
56
148
  codexTodayStats,
57
149
  geminiTodayStats,
@@ -63,52 +155,45 @@ router.get('/init', async (req, res) => {
63
155
  opencodeCounts,
64
156
  ompCounts
65
157
  ] = await Promise.all([
66
- // UI Config
67
- Promise.resolve(loadUIConfig()),
68
-
69
- // Favorites
70
- Promise.resolve(loadFavorites()),
71
-
72
- // Channels
73
- Promise.resolve(getAllChannels()),
74
- Promise.resolve(getCodexChannels()),
75
- Promise.resolve(getGeminiChannels()),
76
- Promise.resolve(getOpenCodeChannels()),
77
- Promise.resolve(getOmpChannels()),
78
-
79
- // Proxy Status
80
- Promise.resolve(getProxyStatus()),
81
- Promise.resolve(getCodexProxyStatus()),
82
- Promise.resolve(getGeminiProxyStatus()),
83
- Promise.resolve(getOpenCodeProxyStatus()),
84
- Promise.resolve(getOmpProxyStatus()),
85
-
86
- // Today Stats (所有平台)
87
- Promise.resolve(getClaudeTodayStatistics()),
88
- Promise.resolve(getCodexTodayStatistics()),
89
- Promise.resolve(getGeminiTodayStatistics()),
90
- Promise.resolve(getOpenCodeTodayStatistics()),
91
- Promise.resolve(getOmpTodayStatistics()),
92
-
93
- // 轻量级统计
94
- Promise.resolve(getClaudeCounts(config)),
95
- Promise.resolve(getCodexCounts()),
96
- Promise.resolve(getGeminiCounts()),
97
- Promise.resolve(getOpenCodeCounts()),
98
- Promise.resolve(getOmpCounts())
158
+ readDashboardSnapshot('channels', 'claude', channelSnapshotKey('claude'), [], config, { force }),
159
+ readDashboardSnapshot('channels', 'codex', channelSnapshotKey('codex'), { channels: [] }, config, { force }),
160
+ readDashboardSnapshot('channels', 'gemini', channelSnapshotKey('gemini'), { channels: [] }, config, { force }),
161
+ readDashboardSnapshot('channels', 'opencode', channelSnapshotKey('opencode'), { channels: [] }, config, { force }),
162
+ readDashboardSnapshot('channels', 'omp', channelSnapshotKey('omp'), { channels: [] }, config, { force }),
163
+ readDashboardSnapshot('todayStats', 'claude', todayStatsSnapshotKey('claude'), null, config, { force }),
164
+ readDashboardSnapshot('todayStats', 'codex', todayStatsSnapshotKey('codex'), null, config, { force }),
165
+ readDashboardSnapshot('todayStats', 'gemini', todayStatsSnapshotKey('gemini'), null, config, { force }),
166
+ readDashboardSnapshot('todayStats', 'opencode', todayStatsSnapshotKey('opencode'), null, config, { force }),
167
+ readDashboardSnapshot('todayStats', 'omp', todayStatsSnapshotKey('omp'), null, config, { force }),
168
+ readDashboardSnapshot('counts', 'claude', projectCountKey('claude'), EMPTY_COUNTS, config, { force }),
169
+ readDashboardSnapshot('counts', 'codex', projectCountKey('codex'), EMPTY_COUNTS, config, { force }),
170
+ readDashboardSnapshot('counts', 'gemini', projectCountKey('gemini'), EMPTY_COUNTS, config, { force }),
171
+ readDashboardSnapshot('counts', 'opencode', projectCountKey('opencode'), EMPTY_COUNTS, config, { force }),
172
+ readDashboardSnapshot('counts', 'omp', projectCountKey('omp'), EMPTY_COUNTS, config, { force })
99
173
  ]);
100
174
 
101
- // 格式化统计数据:取 summary 和 byModel 中的数据
102
- const formatStats = (stats) => {
103
- if (stats && stats.summary) {
104
- return {
105
- requests: stats.summary.requests || 0,
106
- tokens: stats.summary.tokens || 0,
107
- cost: stats.summary.cost || 0,
108
- byModel: stats.byModel || {}
109
- };
110
- }
111
- return { requests: 0, tokens: 0, cost: 0, byModel: {} };
175
+ const parts = {
176
+ counts: summarizePart({
177
+ claude: claudeCounts,
178
+ codex: codexCounts,
179
+ gemini: geminiCounts,
180
+ opencode: opencodeCounts,
181
+ omp: ompCounts
182
+ }),
183
+ todayStats: summarizePart({
184
+ claude: claudeTodayStats,
185
+ codex: codexTodayStats,
186
+ gemini: geminiTodayStats,
187
+ opencode: opencodeTodayStats,
188
+ omp: ompTodayStats
189
+ }),
190
+ channels: summarizePart({
191
+ claude: claudeChannels,
192
+ codex: codexChannels,
193
+ gemini: geminiChannels,
194
+ opencode: opencodeChannels,
195
+ omp: ompChannels
196
+ })
112
197
  };
113
198
 
114
199
  res.json({
@@ -117,33 +202,28 @@ router.get('/init', async (req, res) => {
117
202
  uiConfig,
118
203
  favorites,
119
204
  channels: {
120
- claude: claudeChannels,
121
- codex: codexChannels,
122
- gemini: geminiChannels,
123
- opencode: opencodeChannels.channels || [],
124
- omp: ompChannels.channels || []
125
- },
126
- proxyStatus: {
127
- claude: claudeProxyStatus,
128
- codex: codexProxyStatus,
129
- gemini: geminiProxyStatus,
130
- opencode: opencodeProxyStatus,
131
- omp: ompProxyStatus
205
+ claude: claudeChannels.value,
206
+ codex: codexChannels.value,
207
+ gemini: geminiChannels.value,
208
+ opencode: opencodeChannels.value.channels || [],
209
+ omp: ompChannels.value.channels || []
132
210
  },
211
+ proxyStatus,
133
212
  counts: {
134
- claude: claudeCounts || { projectCount: 0, sessionCount: 0 },
135
- codex: codexCounts || { projectCount: 0, sessionCount: 0 },
136
- gemini: geminiCounts || { projectCount: 0, sessionCount: 0 },
137
- opencode: opencodeCounts || { projectCount: 0, sessionCount: 0 },
138
- omp: ompCounts || { projectCount: 0, sessionCount: 0 }
213
+ claude: claudeCounts.value || EMPTY_COUNTS,
214
+ codex: codexCounts.value || EMPTY_COUNTS,
215
+ gemini: geminiCounts.value || EMPTY_COUNTS,
216
+ opencode: opencodeCounts.value || EMPTY_COUNTS,
217
+ omp: ompCounts.value || EMPTY_COUNTS
139
218
  },
140
219
  todayStats: {
141
- claude: formatStats(claudeTodayStats),
142
- codex: formatStats(codexTodayStats),
143
- gemini: formatStats(geminiTodayStats),
144
- opencode: formatStats(opencodeTodayStats),
145
- omp: formatStats(ompTodayStats)
146
- }
220
+ claude: formatStats(claudeTodayStats.value),
221
+ codex: formatStats(codexTodayStats.value),
222
+ gemini: formatStats(geminiTodayStats.value),
223
+ opencode: formatStats(opencodeTodayStats.value),
224
+ omp: formatStats(ompTodayStats.value)
225
+ },
226
+ meta: dashboardMeta(parts)
147
227
  }
148
228
  });
149
229
  } catch (error) {
@@ -1,30 +1,39 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
- const { getProjects, saveProjectOrder, deleteProject } = require('../services/gemini-sessions');
3
+ const { saveProjectOrder, deleteProject } = require('../services/gemini-sessions');
4
4
  const { isGeminiInstalled } = require('../services/gemini-config');
5
+ const {
6
+ emptyProjectList,
7
+ getProjectListSnapshot,
8
+ invalidateProjectSnapshots
9
+ } = require('../services/project-snapshots');
10
+ const { invalidateSessionSnapshots } = require('../services/session-snapshots');
11
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
5
12
 
6
13
  module.exports = (config) => {
7
14
  /**
8
15
  * GET /api/gemini/projects
9
16
  * 获取所有 Gemini 项目列表
10
17
  */
11
- router.get('/', (req, res) => {
18
+ router.get('/', async (req, res) => {
12
19
  try {
13
20
  // 检查 Gemini 是否安装
14
21
  if (!isGeminiInstalled()) {
15
22
  return res.json({
16
23
  projects: [],
17
24
  currentProject: null,
18
- error: 'Gemini CLI not installed or not found'
25
+ error: 'Gemini CLI not installed or not found',
26
+ meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
19
27
  });
20
28
  }
21
29
 
22
- const projects = getProjects();
23
-
24
- res.json({
25
- projects,
26
- currentProject: projects[0] ? projects[0].name : null
30
+ const force = req.query?.fresh === '1';
31
+ const snapshot = await getProjectListSnapshot('gemini', {
32
+ fallbackValue: emptyProjectList(null),
33
+ force,
34
+ refresh: () => runDashboardSnapshotWorker('projects', 'gemini', config, { force })
27
35
  });
36
+ res.json({ ...snapshot.value, meta: snapshot.meta });
28
37
  } catch (err) {
29
38
  console.error('[Gemini API] Failed to get projects:', err);
30
39
 
@@ -59,6 +68,7 @@ module.exports = (config) => {
59
68
  }
60
69
 
61
70
  saveProjectOrder(order);
71
+ invalidateProjectSnapshots('gemini');
62
72
 
63
73
  res.json({ success: true });
64
74
  } catch (err) {
@@ -79,6 +89,8 @@ module.exports = (config) => {
79
89
 
80
90
  const { projectHash } = req.params;
81
91
  const result = deleteProject(projectHash);
92
+ invalidateProjectSnapshots('gemini');
93
+ invalidateSessionSnapshots('gemini', projectHash);
82
94
 
83
95
  res.json(result);
84
96
  } catch (err) {
@@ -13,6 +13,14 @@ const {
13
13
  } = require('../services/gemini-sessions');
14
14
  const { isGeminiInstalled } = require('../services/gemini-config');
15
15
  const { loadAliases } = require('../services/alias');
16
+ const {
17
+ defaultProjectInfo,
18
+ emptySessionList,
19
+ getSessionListSnapshot,
20
+ invalidateSessionSnapshots,
21
+ runSessionSnapshotWorker
22
+ } = require('../services/session-snapshots');
23
+ const { invalidateProjectSnapshots } = require('../services/project-snapshots');
16
24
 
17
25
  function normalizeForkOptions(body = {}) {
18
26
  const alias = typeof body.alias === 'string' ? body.alias.trim() : '';
@@ -90,39 +98,23 @@ module.exports = (config) => {
90
98
  * GET /api/gemini/sessions/:projectHash
91
99
  * 获取项目的所有会话
92
100
  */
93
- router.get('/:projectHash', (req, res) => {
101
+ router.get('/:projectHash', async (req, res) => {
94
102
  try {
95
103
  if (!isGeminiInstalled()) {
96
104
  return res.status(404).json({ error: 'Gemini CLI not installed' });
97
105
  }
98
106
 
99
107
  const { projectHash } = req.params;
100
- const sessions = getProjectSessions(projectHash);
101
-
102
- // 计算总大小
103
- const totalSize = sessions.reduce((sum, session) => {
104
- return sum + (session.size || 0);
105
- }, 0);
106
-
107
- // 获取别名
108
- const aliases = loadAliases();
109
-
110
- // 使用彩虹表解析真实路径
111
- const realPath = getProjectPath(projectHash);
112
- const path = require('path');
113
- const displayName = realPath ? path.basename(realPath) : `Project ${projectHash.substring(0, 8)}`;
114
-
115
- res.json({
116
- sessions,
117
- totalSize,
118
- aliases,
119
- projectInfo: {
120
- name: projectHash,
121
- fullPath: realPath || projectHash,
122
- path: realPath || projectHash,
123
- displayName
124
- }
108
+ const force = req.query?.fresh === '1';
109
+ const snapshot = await getSessionListSnapshot('gemini', projectHash, {
110
+ fallbackValue: emptySessionList(projectHash, {
111
+ aliases: loadAliases(),
112
+ projectInfo: defaultProjectInfo(projectHash)
113
+ }),
114
+ force,
115
+ refresh: () => runSessionSnapshotWorker('gemini', projectHash, {}, { force })
125
116
  });
117
+ res.json({ ...snapshot.value, meta: snapshot.meta });
126
118
  } catch (err) {
127
119
  console.error('[Gemini API] Failed to get sessions:', err);
128
120
  res.status(500).json({ error: err.message });
@@ -328,6 +320,8 @@ module.exports = (config) => {
328
320
 
329
321
  const { sessionId } = req.params;
330
322
  const result = deleteSession(sessionId);
323
+ invalidateSessionSnapshots('gemini', req.params.projectHash);
324
+ invalidateProjectSnapshots('gemini');
331
325
 
332
326
  res.json(result);
333
327
  } catch (err) {
@@ -377,6 +371,11 @@ module.exports = (config) => {
377
371
  }
378
372
  });
379
373
 
374
+ if (deletedSessionIds.length > 0) {
375
+ invalidateSessionSnapshots('gemini', req.params.projectHash);
376
+ invalidateProjectSnapshots('gemini');
377
+ }
378
+
380
379
  res.json({
381
380
  success: failed.length === 0,
382
381
  requestedCount: uniqueSessionIds.length,
@@ -402,6 +401,8 @@ module.exports = (config) => {
402
401
 
403
402
  const { sessionId } = req.params;
404
403
  const result = forkSession(sessionId, normalizeForkOptions(req.body));
404
+ invalidateSessionSnapshots('gemini', req.params.projectHash);
405
+ invalidateProjectSnapshots('gemini');
405
406
 
406
407
  res.json(result);
407
408
  } catch (err) {
@@ -428,6 +429,7 @@ module.exports = (config) => {
428
429
  }
429
430
 
430
431
  saveSessionOrder(projectHash, order);
432
+ invalidateSessionSnapshots('gemini', projectHash);
431
433
 
432
434
  res.json({ success: true });
433
435
  } catch (err) {
@@ -1,32 +1,41 @@
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
  isOmpInstalled
8
7
  } = require('../services/omp-sessions');
8
+ const {
9
+ emptyProjectList,
10
+ getProjectListSnapshot,
11
+ invalidateProjectSnapshots
12
+ } = require('../services/project-snapshots');
13
+ const { invalidateSessionSnapshots } = require('../services/session-snapshots');
14
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
9
15
 
10
16
  function isNotFoundError(error) {
11
17
  return !!(error && error.message === 'Project not found');
12
18
  }
13
19
 
14
20
  module.exports = () => {
15
- router.get('/', (req, res) => {
21
+ router.get('/', async (req, res) => {
16
22
  try {
17
23
  if (!isOmpInstalled()) {
18
24
  return res.json({
19
25
  projects: [],
20
26
  currentProject: null,
21
- error: 'OMP CLI not installed or not found'
27
+ error: 'OMP CLI not installed or not found',
28
+ meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
22
29
  });
23
30
  }
24
31
 
25
- const projects = getProjects();
26
- res.json({
27
- projects,
28
- currentProject: projects[0] ? projects[0].name : null
32
+ const force = req.query?.fresh === '1';
33
+ const snapshot = await getProjectListSnapshot('omp', {
34
+ fallbackValue: emptyProjectList(null),
35
+ force,
36
+ refresh: () => runDashboardSnapshotWorker('projects', 'omp', {}, { force })
29
37
  });
38
+ res.json({ ...snapshot.value, meta: snapshot.meta });
30
39
  } catch (err) {
31
40
  console.error('[OMP API] Failed to get projects:', err);
32
41
  if (err.code === 'ENOENT') {
@@ -49,6 +58,7 @@ module.exports = () => {
49
58
  return res.status(400).json({ error: 'order must be an array' });
50
59
  }
51
60
  saveProjectOrder(order);
61
+ invalidateProjectSnapshots('omp');
52
62
  res.json({ success: true });
53
63
  } catch (err) {
54
64
  console.error('[OMP API] Failed to save project order:', err);
@@ -62,6 +72,8 @@ module.exports = () => {
62
72
  return res.status(404).json({ error: 'OMP CLI not installed' });
63
73
  }
64
74
  const result = deleteProject(req.params.projectName);
75
+ invalidateProjectSnapshots('omp');
76
+ invalidateSessionSnapshots('omp', req.params.projectName);
65
77
  res.json(result);
66
78
  } catch (err) {
67
79
  if (isNotFoundError(err)) {
@@ -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);