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
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>coding-tool-x · AI 编程工作台</title>
8
- <script type="module" crossorigin src="/assets/index-BjdNYwSs.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-ClMNA9Hf.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-CVyvdmg1.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/icons-nGdSKQn2.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-kQCC5dlu.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-B_nFqixU.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.7.4",
3
+ "version": "3.7.6",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,12 +1,14 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
- const { getProjects, saveProjectOrder, deleteProject } = require('../services/codex-sessions');
3
+ const { saveProjectOrder, deleteProject } = require('../services/codex-sessions');
4
4
  const { isCodexInstalled } = require('../services/codex-config');
5
5
  const {
6
6
  emptyProjectList,
7
7
  getProjectListSnapshot,
8
8
  invalidateProjectSnapshots
9
9
  } = require('../services/project-snapshots');
10
+ const { invalidateSessionSnapshots } = require('../services/session-snapshots');
11
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
10
12
 
11
13
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
12
14
 
@@ -18,14 +20,6 @@ function logPerf(route, startMs, detail = '') {
18
20
  }
19
21
 
20
22
  module.exports = (config) => {
21
- function buildProjectsPayload() {
22
- const projects = getProjects();
23
- return {
24
- projects,
25
- currentProject: projects[0] ? projects[0].name : null
26
- };
27
- }
28
-
29
23
  /**
30
24
  * GET /api/codex/projects
31
25
  * 获取所有 Codex 项目列表
@@ -44,10 +38,11 @@ module.exports = (config) => {
44
38
  });
45
39
  }
46
40
 
41
+ const force = req.query?.fresh === '1';
47
42
  const snapshot = await getProjectListSnapshot('codex', {
48
43
  fallbackValue: emptyProjectList(null),
49
- force: req.query?.fresh === '1',
50
- refresh: buildProjectsPayload
44
+ force,
45
+ refresh: () => runDashboardSnapshotWorker('projects', 'codex', config, { force })
51
46
  });
52
47
  logPerf('GET /api/codex/projects', startMs, `projects=${snapshot.value.projects.length} stale=${snapshot.meta.stale}`);
53
48
 
@@ -109,6 +104,7 @@ module.exports = (config) => {
109
104
  const { projectName } = req.params;
110
105
  const result = deleteProject(projectName);
111
106
  invalidateProjectSnapshots('codex');
107
+ invalidateSessionSnapshots('codex', projectName);
112
108
 
113
109
  res.json(result);
114
110
  } catch (err) {
@@ -11,6 +11,14 @@ const {
11
11
  } = require('../services/codex-sessions');
12
12
  const { isCodexInstalled } = require('../services/codex-config');
13
13
  const { loadAliases } = require('../services/alias');
14
+ const {
15
+ defaultProjectInfo,
16
+ emptySessionList,
17
+ getSessionListSnapshot,
18
+ invalidateSessionSnapshots,
19
+ runSessionSnapshotWorker
20
+ } = require('../services/session-snapshots');
21
+ const { invalidateProjectSnapshots } = require('../services/project-snapshots');
14
22
 
15
23
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
16
24
 
@@ -151,7 +159,7 @@ module.exports = (config) => {
151
159
  * GET /api/codex/sessions/:projectName
152
160
  * 获取项目的所有会话
153
161
  */
154
- router.get('/:projectName', (req, res) => {
162
+ router.get('/:projectName', async (req, res) => {
155
163
  const startMs = Date.now();
156
164
  try {
157
165
  if (!isCodexInstalled()) {
@@ -160,27 +168,18 @@ module.exports = (config) => {
160
168
  }
161
169
 
162
170
  const { projectName } = req.params;
163
- const sessions = getSessionsByProject(projectName);
164
-
165
- // 计算总大小
166
- const totalSize = sessions.reduce((sum, session) => {
167
- return sum + (session.size || 0);
168
- }, 0);
169
-
170
- // 获取别名
171
- const aliases = loadAliases();
172
- logPerf('GET /api/codex/sessions/:projectName', startMs, `project=${projectName}, sessions=${sessions.length}`);
173
-
174
- res.json({
175
- sessions,
176
- totalSize,
177
- aliases, // 返回所有别名
178
- projectInfo: {
179
- name: projectName,
180
- fullPath: projectName,
181
- displayName: projectName
182
- }
171
+ const force = req.query?.fresh === '1';
172
+ const snapshot = await getSessionListSnapshot('codex', projectName, {
173
+ fallbackValue: emptySessionList(projectName, {
174
+ aliases: loadAliases(),
175
+ projectInfo: defaultProjectInfo(projectName)
176
+ }),
177
+ force,
178
+ refresh: () => runSessionSnapshotWorker('codex', projectName, {}, { force })
183
179
  });
180
+ logPerf('GET /api/codex/sessions/:projectName', startMs, `project=${projectName}, sessions=${snapshot.value.sessions.length}, stale=${snapshot.meta.stale}`);
181
+
182
+ res.json({ ...snapshot.value, meta: snapshot.meta });
184
183
  } catch (err) {
185
184
  console.error('[Codex API] Failed to get sessions:', err);
186
185
  logPerf('GET /api/codex/sessions/:projectName', startMs, `error=${err.message}`);
@@ -453,6 +452,8 @@ module.exports = (config) => {
453
452
 
454
453
  const { sessionId } = req.params;
455
454
  const result = deleteSession(sessionId);
455
+ invalidateSessionSnapshots('codex', req.params.projectName);
456
+ invalidateProjectSnapshots('codex');
456
457
 
457
458
  res.json(result);
458
459
  } catch (err) {
@@ -502,6 +503,11 @@ module.exports = (config) => {
502
503
  }
503
504
  });
504
505
 
506
+ if (deletedSessionIds.length > 0) {
507
+ invalidateSessionSnapshots('codex', req.params.projectName);
508
+ invalidateProjectSnapshots('codex');
509
+ }
510
+
505
511
  res.json({
506
512
  success: failed.length === 0,
507
513
  requestedCount: uniqueSessionIds.length,
@@ -527,6 +533,8 @@ module.exports = (config) => {
527
533
 
528
534
  const { sessionId } = req.params;
529
535
  const result = forkSession(sessionId, normalizeForkOptions(req.body));
536
+ invalidateSessionSnapshots('codex', req.params.projectName);
537
+ invalidateProjectSnapshots('codex');
530
538
 
531
539
  res.json(result);
532
540
  } catch (err) {
@@ -553,6 +561,7 @@ module.exports = (config) => {
553
561
  }
554
562
 
555
563
  saveSessionOrder(projectName, order);
564
+ invalidateSessionSnapshots('codex', projectName);
556
565
 
557
566
  res.json({ success: true });
558
567
  } catch (err) {
@@ -5,34 +5,17 @@ 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');
32
13
  const { getSnapshot } = require('../services/snapshot-cache');
33
14
  const { projectCountKey } = require('../services/project-snapshots');
15
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
34
16
 
35
17
  const SNAPSHOT_TTL_MS = 60 * 1000;
18
+ const DASHBOARD_SNAPSHOT_DEFER_MS = 0;
36
19
  const EMPTY_COUNTS = { projectCount: 0, sessionCount: 0 };
37
20
  const EMPTY_STATS = { requests: 0, tokens: 0, cost: 0, byModel: {}, byChannel: {} };
38
21
 
@@ -90,11 +73,15 @@ function formatStats(stats) {
90
73
  return { ...EMPTY_STATS };
91
74
  }
92
75
 
93
- async function readDashboardSnapshot(key, fallbackValue, refresh) {
76
+ async function readDashboardSnapshot(kind, source, key, fallbackValue, config = {}, options = {}) {
77
+ const force = options.force === true;
94
78
  return getSnapshot(key, {
95
79
  ttlMs: SNAPSHOT_TTL_MS,
96
80
  fallbackValue,
97
- refresh
81
+ force,
82
+ staleWhileForce: force,
83
+ deferMs: DASHBOARD_SNAPSHOT_DEFER_MS,
84
+ refresh: () => runDashboardSnapshotWorker(kind, source, config, { force })
98
85
  });
99
86
  }
100
87
 
@@ -139,6 +126,7 @@ function dashboardMeta(parts) {
139
126
  router.get('/init', async (req, res) => {
140
127
  try {
141
128
  const config = loadConfig();
129
+ const force = req.query?.fresh === '1' || req.query?.force === '1';
142
130
 
143
131
  const uiConfig = safeRead('uiConfig', () => loadUIConfig(), {});
144
132
  const favorites = safeRead('favorites', () => loadFavorites(), {});
@@ -167,21 +155,21 @@ router.get('/init', async (req, res) => {
167
155
  opencodeCounts,
168
156
  ompCounts
169
157
  ] = await Promise.all([
170
- readDashboardSnapshot(channelSnapshotKey('claude'), [], () => getAllChannels()),
171
- readDashboardSnapshot(channelSnapshotKey('codex'), { channels: [] }, () => getCodexChannels()),
172
- readDashboardSnapshot(channelSnapshotKey('gemini'), { channels: [] }, () => getGeminiChannels()),
173
- readDashboardSnapshot(channelSnapshotKey('opencode'), { channels: [] }, () => getOpenCodeChannels()),
174
- readDashboardSnapshot(channelSnapshotKey('omp'), { channels: [] }, () => getOmpChannels()),
175
- readDashboardSnapshot(todayStatsSnapshotKey('claude'), null, () => getClaudeTodayStatistics()),
176
- readDashboardSnapshot(todayStatsSnapshotKey('codex'), null, () => getCodexTodayStatistics()),
177
- readDashboardSnapshot(todayStatsSnapshotKey('gemini'), null, () => getGeminiTodayStatistics()),
178
- readDashboardSnapshot(todayStatsSnapshotKey('opencode'), null, () => getOpenCodeTodayStatistics()),
179
- readDashboardSnapshot(todayStatsSnapshotKey('omp'), null, () => getOmpTodayStatistics()),
180
- readDashboardSnapshot(projectCountKey('claude'), EMPTY_COUNTS, () => getClaudeCounts(config)),
181
- readDashboardSnapshot(projectCountKey('codex'), EMPTY_COUNTS, () => getCodexCounts()),
182
- readDashboardSnapshot(projectCountKey('gemini'), EMPTY_COUNTS, () => getGeminiCounts()),
183
- readDashboardSnapshot(projectCountKey('opencode'), EMPTY_COUNTS, () => getOpenCodeCounts()),
184
- readDashboardSnapshot(projectCountKey('omp'), EMPTY_COUNTS, () => 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 })
185
173
  ]);
186
174
 
187
175
  const parts = {
@@ -1,22 +1,16 @@
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
5
  const {
6
6
  emptyProjectList,
7
7
  getProjectListSnapshot,
8
8
  invalidateProjectSnapshots
9
9
  } = require('../services/project-snapshots');
10
+ const { invalidateSessionSnapshots } = require('../services/session-snapshots');
11
+ const { runDashboardSnapshotWorker } = require('../services/dashboard-snapshot-worker');
10
12
 
11
13
  module.exports = (config) => {
12
- function buildProjectsPayload() {
13
- const projects = getProjects();
14
- return {
15
- projects,
16
- currentProject: projects[0] ? projects[0].name : null
17
- };
18
- }
19
-
20
14
  /**
21
15
  * GET /api/gemini/projects
22
16
  * 获取所有 Gemini 项目列表
@@ -33,10 +27,11 @@ module.exports = (config) => {
33
27
  });
34
28
  }
35
29
 
30
+ const force = req.query?.fresh === '1';
36
31
  const snapshot = await getProjectListSnapshot('gemini', {
37
32
  fallbackValue: emptyProjectList(null),
38
- force: req.query?.fresh === '1',
39
- refresh: buildProjectsPayload
33
+ force,
34
+ refresh: () => runDashboardSnapshotWorker('projects', 'gemini', config, { force })
40
35
  });
41
36
  res.json({ ...snapshot.value, meta: snapshot.meta });
42
37
  } catch (err) {
@@ -95,6 +90,7 @@ module.exports = (config) => {
95
90
  const { projectHash } = req.params;
96
91
  const result = deleteProject(projectHash);
97
92
  invalidateProjectSnapshots('gemini');
93
+ invalidateSessionSnapshots('gemini', projectHash);
98
94
 
99
95
  res.json(result);
100
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) {
@@ -23,9 +23,13 @@ const {
23
23
  } = require('../services/model-detector');
24
24
  const {
25
25
  findAuthProviderForKey,
26
- getOmpAuthProviderSnapshot
26
+ getCachedOmpAuthProviderSnapshot,
27
+ getOmpAuthProviderCacheMeta,
28
+ warmOmpAuthProviderSnapshot
27
29
  } = require('../services/omp-auth-providers');
28
30
 
31
+ const CHANNEL_LIST_AUTH_OPTIONS = { accountCheck: false, includeStatus: false };
32
+
29
33
  function uniqueModels(models = []) {
30
34
  const seen = new Set();
31
35
  const result = [];
@@ -136,19 +140,53 @@ function attachAuthProvider(channel, snapshot) {
136
140
  };
137
141
  }
138
142
 
143
+ function getAuthSnapshotForChannelList() {
144
+ const snapshot = getCachedOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
145
+ const shouldWarm = !snapshot || snapshot.stale;
146
+ if (shouldWarm) {
147
+ warmOmpAuthProviderSnapshot(CHANNEL_LIST_AUTH_OPTIONS);
148
+ }
149
+ const meta = getOmpAuthProviderCacheMeta(CHANNEL_LIST_AUTH_OPTIONS);
150
+ return {
151
+ snapshot,
152
+ meta: {
153
+ ...meta,
154
+ stale: !snapshot || Boolean(snapshot.stale),
155
+ refreshing: shouldWarm || meta.refreshing,
156
+ fallback: !snapshot
157
+ }
158
+ };
159
+ }
160
+
161
+ function buildUnavailableAuthProviderMeta(error) {
162
+ return {
163
+ cached: false,
164
+ stale: false,
165
+ refreshing: false,
166
+ fallback: true,
167
+ checkedAt: new Date().toISOString(),
168
+ error: error || 'omp-not-available'
169
+ };
170
+ }
171
+
139
172
  module.exports = () => {
140
173
  router.get('/', (req, res) => {
141
174
  try {
142
175
  if (!isOmpInstalled()) {
143
- return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
176
+ return res.json({
177
+ channels: [],
178
+ installed: false,
179
+ error: 'OMP CLI not installed',
180
+ authProviderMeta: buildUnavailableAuthProviderMeta('OMP CLI not installed')
181
+ });
144
182
  }
145
183
  const data = getChannels();
146
- const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
184
+ const { snapshot: authSnapshot, meta: authProviderMeta } = getAuthSnapshotForChannelList();
147
185
  const channels = (data.channels || []).map(ch => ({
148
186
  ...attachAuthProvider(ch, authSnapshot),
149
187
  health: getChannelHealthStatus(ch.id, 'omp')
150
188
  }));
151
- res.json({ channels, installed: true });
189
+ res.json({ channels, installed: true, authProviderMeta });
152
190
  } catch (err) {
153
191
  console.error('[OMP Channels API] Failed to get channels:', err);
154
192
  res.status(500).json({ error: err.message });
@@ -158,14 +196,19 @@ module.exports = () => {
158
196
  router.get('/enabled', (req, res) => {
159
197
  try {
160
198
  if (!isOmpInstalled()) {
161
- return res.json({ channels: [], installed: false, error: 'OMP CLI not installed' });
199
+ return res.json({
200
+ channels: [],
201
+ installed: false,
202
+ error: 'OMP CLI not installed',
203
+ authProviderMeta: buildUnavailableAuthProviderMeta('OMP CLI not installed')
204
+ });
162
205
  }
163
206
  const data = getChannels();
164
- const authSnapshot = getOmpAuthProviderSnapshot({ accountCheck: false, includeStatus: false });
207
+ const { snapshot: authSnapshot, meta: authProviderMeta } = getAuthSnapshotForChannelList();
165
208
  const channels = (data.channels || [])
166
209
  .filter(ch => ch.enabled !== false)
167
210
  .map(ch => ({ ...attachAuthProvider(ch, authSnapshot), health: getChannelHealthStatus(ch.id, 'omp') }));
168
- res.json({ channels, installed: true });
211
+ res.json({ channels, installed: true, authProviderMeta });
169
212
  } catch (err) {
170
213
  console.error('[OMP Channels API] Failed to get enabled channels:', err);
171
214
  res.status(500).json({ error: err.message });
@@ -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
  isOmpInstalled
@@ -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 = () => {
20
- function buildProjectsPayload() {
21
- const projects = getProjects();
22
- return {
23
- projects,
24
- currentProject: projects[0] ? projects[0].name : null
25
- };
26
- }
27
-
28
21
  router.get('/', async (req, res) => {
29
22
  try {
30
23
  if (!isOmpInstalled()) {
@@ -36,10 +29,11 @@ module.exports = () => {
36
29
  });
37
30
  }
38
31
 
32
+ const force = req.query?.fresh === '1';
39
33
  const snapshot = await getProjectListSnapshot('omp', {
40
34
  fallbackValue: emptyProjectList(null),
41
- force: req.query?.fresh === '1',
42
- refresh: buildProjectsPayload
35
+ force,
36
+ refresh: () => runDashboardSnapshotWorker('projects', 'omp', {}, { force })
43
37
  });
44
38
  res.json({ ...snapshot.value, meta: snapshot.meta });
45
39
  } catch (err) {
@@ -79,6 +73,7 @@ module.exports = () => {
79
73
  }
80
74
  const result = deleteProject(req.params.projectName);
81
75
  invalidateProjectSnapshots('omp');
76
+ invalidateSessionSnapshots('omp', req.params.projectName);
82
77
  res.json(result);
83
78
  } catch (err) {
84
79
  if (isNotFoundError(err)) {