coding-tool-x 3.7.3 → 3.7.4

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.
@@ -5,7 +5,7 @@
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-BovYCFsd.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-BjdNYwSs.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">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.7.3",
3
+ "version": "3.7.4",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -432,6 +432,8 @@ const PATHS = {
432
432
  },
433
433
 
434
434
  // 运行时、缓存、备份
435
+ cache: CACHE_DIR,
436
+ snapshotCache: path.join(CACHE_DIR, 'snapshots'),
435
437
  sessionCache: path.join(CACHE_DIR, 'session-cache.json'),
436
438
  sessionHasCache: path.join(CACHE_DIR, 'session-has-cache.json'),
437
439
  channelModels: path.join(CACHE_DIR, 'channel-models.json'),
@@ -2,6 +2,11 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { getProjects, saveProjectOrder, deleteProject } = require('../services/codex-sessions');
4
4
  const { isCodexInstalled } = require('../services/codex-config');
5
+ const {
6
+ emptyProjectList,
7
+ getProjectListSnapshot,
8
+ invalidateProjectSnapshots
9
+ } = require('../services/project-snapshots');
5
10
 
6
11
  const DEBUG_CODEX_PERF = process.env.DEBUG_CODEX_PERF === '1';
7
12
 
@@ -13,11 +18,19 @@ function logPerf(route, startMs, detail = '') {
13
18
  }
14
19
 
15
20
  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
+
16
29
  /**
17
30
  * GET /api/codex/projects
18
31
  * 获取所有 Codex 项目列表
19
32
  */
20
- router.get('/', (req, res) => {
33
+ router.get('/', async (req, res) => {
21
34
  const startMs = Date.now();
22
35
  try {
23
36
  // 检查 Codex 是否安装
@@ -26,17 +39,19 @@ module.exports = (config) => {
26
39
  return res.json({
27
40
  projects: [],
28
41
  currentProject: null,
29
- error: 'Codex CLI not installed or not found'
42
+ error: 'Codex CLI not installed or not found',
43
+ meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
30
44
  });
31
45
  }
32
46
 
33
- const projects = getProjects();
34
- logPerf('GET /api/codex/projects', startMs, `projects=${projects.length}`);
35
-
36
- res.json({
37
- projects,
38
- currentProject: projects[0] ? projects[0].name : null
47
+ const snapshot = await getProjectListSnapshot('codex', {
48
+ fallbackValue: emptyProjectList(null),
49
+ force: req.query?.fresh === '1',
50
+ refresh: buildProjectsPayload
39
51
  });
52
+ logPerf('GET /api/codex/projects', startMs, `projects=${snapshot.value.projects.length} stale=${snapshot.meta.stale}`);
53
+
54
+ res.json({ ...snapshot.value, meta: snapshot.meta });
40
55
  } catch (err) {
41
56
  console.error('[Codex API] Failed to get projects:', err);
42
57
  logPerf('GET /api/codex/projects', startMs, `error=${err.message}`);
@@ -72,6 +87,7 @@ module.exports = (config) => {
72
87
  }
73
88
 
74
89
  saveProjectOrder(order);
90
+ invalidateProjectSnapshots('codex');
75
91
 
76
92
  res.json({ success: true });
77
93
  } catch (err) {
@@ -92,6 +108,7 @@ module.exports = (config) => {
92
108
 
93
109
  const { projectName } = req.params;
94
110
  const result = deleteProject(projectName);
111
+ invalidateProjectSnapshots('codex');
95
112
 
96
113
  res.json(result);
97
114
  } catch (err) {
@@ -29,6 +29,108 @@ const { getTodayStatistics: getCodexTodayStatistics } = require('../services/cod
29
29
  const { getTodayStatistics: getGeminiTodayStatistics } = require('../services/gemini-statistics-service');
30
30
  const { getTodayStatistics: getOpenCodeTodayStatistics } = require('../services/opencode-statistics-service');
31
31
  const { getTodayStatistics: getOmpTodayStatistics } = require('../services/omp-statistics-service');
32
+ const { getSnapshot } = require('../services/snapshot-cache');
33
+ const { projectCountKey } = require('../services/project-snapshots');
34
+
35
+ const SNAPSHOT_TTL_MS = 60 * 1000;
36
+ const EMPTY_COUNTS = { projectCount: 0, sessionCount: 0 };
37
+ const EMPTY_STATS = { requests: 0, tokens: 0, cost: 0, byModel: {}, byChannel: {} };
38
+
39
+ function safeRead(label, reader, fallback) {
40
+ try {
41
+ return reader();
42
+ } catch (error) {
43
+ console.warn(`[Dashboard] ${label} failed:`, error.message);
44
+ return fallback;
45
+ }
46
+ }
47
+
48
+ function channelSnapshotKey(source) {
49
+ return `dashboard:channels:${source}`;
50
+ }
51
+
52
+ function todayStatsSnapshotKey(source) {
53
+ return `dashboard:today-stats:${source}`;
54
+ }
55
+
56
+ function normalizeChannelTokens(tokens) {
57
+ if (tokens && typeof tokens === 'object') {
58
+ return Number(tokens.total ?? tokens.input ?? 0) || 0;
59
+ }
60
+ return Number(tokens) || 0;
61
+ }
62
+
63
+ function normalizeByChannel(byChannel = {}) {
64
+ if (!byChannel || typeof byChannel !== 'object') {
65
+ return {};
66
+ }
67
+
68
+ return Object.fromEntries(Object.entries(byChannel).map(([channelId, item = {}]) => [
69
+ channelId,
70
+ {
71
+ ...item,
72
+ requests: Number(item.requests) || 0,
73
+ tokens: normalizeChannelTokens(item.tokens),
74
+ cost: Number(item.cost) || 0
75
+ }
76
+ ]));
77
+ }
78
+
79
+ // 格式化统计数据:取 summary、byModel 和 byChannel 中的数据
80
+ function formatStats(stats) {
81
+ if (stats && stats.summary) {
82
+ return {
83
+ requests: stats.summary.requests || 0,
84
+ tokens: normalizeChannelTokens(stats.summary.tokens),
85
+ cost: stats.summary.cost || 0,
86
+ byModel: stats.byModel || {},
87
+ byChannel: normalizeByChannel(stats.byChannel || {})
88
+ };
89
+ }
90
+ return { ...EMPTY_STATS };
91
+ }
92
+
93
+ async function readDashboardSnapshot(key, fallbackValue, refresh) {
94
+ return getSnapshot(key, {
95
+ ttlMs: SNAPSHOT_TTL_MS,
96
+ fallbackValue,
97
+ refresh
98
+ });
99
+ }
100
+
101
+ function summarizePart(partSnapshots = {}) {
102
+ const items = {};
103
+ let stale = false;
104
+ let refreshing = false;
105
+ let generatedAt = null;
106
+
107
+ Object.entries(partSnapshots).forEach(([source, snapshot]) => {
108
+ const meta = snapshot.meta || {};
109
+ items[source] = meta;
110
+ stale = stale || Boolean(meta.stale);
111
+ refreshing = refreshing || Boolean(meta.refreshing);
112
+ if (meta.generatedAt && (!generatedAt || meta.generatedAt > generatedAt)) {
113
+ generatedAt = meta.generatedAt;
114
+ }
115
+ });
116
+
117
+ return {
118
+ generatedAt,
119
+ stale,
120
+ refreshing,
121
+ items
122
+ };
123
+ }
124
+
125
+ function dashboardMeta(parts) {
126
+ const partValues = Object.values(parts);
127
+ return {
128
+ generatedAt: new Date().toISOString(),
129
+ stale: partValues.some(part => part.stale),
130
+ refreshing: partValues.some(part => part.refreshing),
131
+ parts
132
+ };
133
+ }
32
134
 
33
135
  /**
34
136
  * GET /api/dashboard/init
@@ -38,20 +140,22 @@ router.get('/init', async (req, res) => {
38
140
  try {
39
141
  const config = loadConfig();
40
142
 
41
- // 并行获取所有数据
143
+ const uiConfig = safeRead('uiConfig', () => loadUIConfig(), {});
144
+ const favorites = safeRead('favorites', () => loadFavorites(), {});
145
+ const proxyStatus = {
146
+ claude: safeRead('claude proxy status', () => getProxyStatus(), {}),
147
+ codex: safeRead('codex proxy status', () => getCodexProxyStatus(), {}),
148
+ gemini: safeRead('gemini proxy status', () => getGeminiProxyStatus(), {}),
149
+ opencode: safeRead('opencode proxy status', () => getOpenCodeProxyStatus(), {}),
150
+ omp: safeRead('omp proxy status', () => getOmpProxyStatus(), {})
151
+ };
152
+
42
153
  const [
43
- uiConfig,
44
- favorites,
45
154
  claudeChannels,
46
155
  codexChannels,
47
156
  geminiChannels,
48
157
  opencodeChannels,
49
158
  ompChannels,
50
- claudeProxyStatus,
51
- codexProxyStatus,
52
- geminiProxyStatus,
53
- opencodeProxyStatus,
54
- ompProxyStatus,
55
159
  claudeTodayStats,
56
160
  codexTodayStats,
57
161
  geminiTodayStats,
@@ -63,52 +167,45 @@ router.get('/init', async (req, res) => {
63
167
  opencodeCounts,
64
168
  ompCounts
65
169
  ] = 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())
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())
99
185
  ]);
100
186
 
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: {} };
187
+ const parts = {
188
+ counts: summarizePart({
189
+ claude: claudeCounts,
190
+ codex: codexCounts,
191
+ gemini: geminiCounts,
192
+ opencode: opencodeCounts,
193
+ omp: ompCounts
194
+ }),
195
+ todayStats: summarizePart({
196
+ claude: claudeTodayStats,
197
+ codex: codexTodayStats,
198
+ gemini: geminiTodayStats,
199
+ opencode: opencodeTodayStats,
200
+ omp: ompTodayStats
201
+ }),
202
+ channels: summarizePart({
203
+ claude: claudeChannels,
204
+ codex: codexChannels,
205
+ gemini: geminiChannels,
206
+ opencode: opencodeChannels,
207
+ omp: ompChannels
208
+ })
112
209
  };
113
210
 
114
211
  res.json({
@@ -117,33 +214,28 @@ router.get('/init', async (req, res) => {
117
214
  uiConfig,
118
215
  favorites,
119
216
  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
217
+ claude: claudeChannels.value,
218
+ codex: codexChannels.value,
219
+ gemini: geminiChannels.value,
220
+ opencode: opencodeChannels.value.channels || [],
221
+ omp: ompChannels.value.channels || []
132
222
  },
223
+ proxyStatus,
133
224
  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 }
225
+ claude: claudeCounts.value || EMPTY_COUNTS,
226
+ codex: codexCounts.value || EMPTY_COUNTS,
227
+ gemini: geminiCounts.value || EMPTY_COUNTS,
228
+ opencode: opencodeCounts.value || EMPTY_COUNTS,
229
+ omp: ompCounts.value || EMPTY_COUNTS
139
230
  },
140
231
  todayStats: {
141
- claude: formatStats(claudeTodayStats),
142
- codex: formatStats(codexTodayStats),
143
- gemini: formatStats(geminiTodayStats),
144
- opencode: formatStats(opencodeTodayStats),
145
- omp: formatStats(ompTodayStats)
146
- }
232
+ claude: formatStats(claudeTodayStats.value),
233
+ codex: formatStats(codexTodayStats.value),
234
+ gemini: formatStats(geminiTodayStats.value),
235
+ opencode: formatStats(opencodeTodayStats.value),
236
+ omp: formatStats(ompTodayStats.value)
237
+ },
238
+ meta: dashboardMeta(parts)
147
239
  }
148
240
  });
149
241
  } catch (error) {
@@ -2,29 +2,43 @@ const express = require('express');
2
2
  const router = express.Router();
3
3
  const { getProjects, 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');
5
10
 
6
11
  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
+
7
20
  /**
8
21
  * GET /api/gemini/projects
9
22
  * 获取所有 Gemini 项目列表
10
23
  */
11
- router.get('/', (req, res) => {
24
+ router.get('/', async (req, res) => {
12
25
  try {
13
26
  // 检查 Gemini 是否安装
14
27
  if (!isGeminiInstalled()) {
15
28
  return res.json({
16
29
  projects: [],
17
30
  currentProject: null,
18
- error: 'Gemini CLI not installed or not found'
31
+ error: 'Gemini CLI not installed or not found',
32
+ meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
19
33
  });
20
34
  }
21
35
 
22
- const projects = getProjects();
23
-
24
- res.json({
25
- projects,
26
- currentProject: projects[0] ? projects[0].name : null
36
+ const snapshot = await getProjectListSnapshot('gemini', {
37
+ fallbackValue: emptyProjectList(null),
38
+ force: req.query?.fresh === '1',
39
+ refresh: buildProjectsPayload
27
40
  });
41
+ res.json({ ...snapshot.value, meta: snapshot.meta });
28
42
  } catch (err) {
29
43
  console.error('[Gemini API] Failed to get projects:', err);
30
44
 
@@ -59,6 +73,7 @@ module.exports = (config) => {
59
73
  }
60
74
 
61
75
  saveProjectOrder(order);
76
+ invalidateProjectSnapshots('gemini');
62
77
 
63
78
  res.json({ success: true });
64
79
  } catch (err) {
@@ -79,6 +94,7 @@ module.exports = (config) => {
79
94
 
80
95
  const { projectHash } = req.params;
81
96
  const result = deleteProject(projectHash);
97
+ invalidateProjectSnapshots('gemini');
82
98
 
83
99
  res.json(result);
84
100
  } catch (err) {
@@ -6,27 +6,42 @@ const {
6
6
  deleteProject,
7
7
  isOmpInstalled
8
8
  } = require('../services/omp-sessions');
9
+ const {
10
+ emptyProjectList,
11
+ getProjectListSnapshot,
12
+ invalidateProjectSnapshots
13
+ } = require('../services/project-snapshots');
9
14
 
10
15
  function isNotFoundError(error) {
11
16
  return !!(error && error.message === 'Project not found');
12
17
  }
13
18
 
14
19
  module.exports = () => {
15
- router.get('/', (req, res) => {
20
+ function buildProjectsPayload() {
21
+ const projects = getProjects();
22
+ return {
23
+ projects,
24
+ currentProject: projects[0] ? projects[0].name : null
25
+ };
26
+ }
27
+
28
+ router.get('/', async (req, res) => {
16
29
  try {
17
30
  if (!isOmpInstalled()) {
18
31
  return res.json({
19
32
  projects: [],
20
33
  currentProject: null,
21
- error: 'OMP CLI not installed or not found'
34
+ error: 'OMP CLI not installed or not found',
35
+ meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
22
36
  });
23
37
  }
24
38
 
25
- const projects = getProjects();
26
- res.json({
27
- projects,
28
- currentProject: projects[0] ? projects[0].name : null
39
+ const snapshot = await getProjectListSnapshot('omp', {
40
+ fallbackValue: emptyProjectList(null),
41
+ force: req.query?.fresh === '1',
42
+ refresh: buildProjectsPayload
29
43
  });
44
+ res.json({ ...snapshot.value, meta: snapshot.meta });
30
45
  } catch (err) {
31
46
  console.error('[OMP API] Failed to get projects:', err);
32
47
  if (err.code === 'ENOENT') {
@@ -49,6 +64,7 @@ module.exports = () => {
49
64
  return res.status(400).json({ error: 'order must be an array' });
50
65
  }
51
66
  saveProjectOrder(order);
67
+ invalidateProjectSnapshots('omp');
52
68
  res.json({ success: true });
53
69
  } catch (err) {
54
70
  console.error('[OMP API] Failed to save project order:', err);
@@ -62,6 +78,7 @@ module.exports = () => {
62
78
  return res.status(404).json({ error: 'OMP CLI not installed' });
63
79
  }
64
80
  const result = deleteProject(req.params.projectName);
81
+ invalidateProjectSnapshots('omp');
65
82
  res.json(result);
66
83
  } catch (err) {
67
84
  if (isNotFoundError(err)) {
@@ -6,32 +6,46 @@ const {
6
6
  deleteProject,
7
7
  isOpenCodeInstalled
8
8
  } = require('../services/opencode-sessions');
9
+ const {
10
+ emptyProjectList,
11
+ getProjectListSnapshot,
12
+ invalidateProjectSnapshots
13
+ } = require('../services/project-snapshots');
9
14
 
10
15
  function isNotFoundError(error) {
11
16
  return !!(error && error.message === 'Project not found');
12
17
  }
13
18
 
14
19
  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
+
15
28
  /**
16
29
  * GET /api/opencode/projects
17
30
  * 获取所有 OpenCode 项目列表
18
31
  */
19
- router.get('/', (req, res) => {
32
+ router.get('/', async (req, res) => {
20
33
  try {
21
34
  if (!isOpenCodeInstalled()) {
22
35
  return res.json({
23
36
  projects: [],
24
37
  currentProject: null,
25
- error: 'OpenCode CLI not installed or not found'
38
+ error: 'OpenCode CLI not installed or not found',
39
+ meta: { generatedAt: new Date().toISOString(), stale: false, refreshing: false, fallback: true, error: null }
26
40
  });
27
41
  }
28
42
 
29
- const projects = getProjects();
30
-
31
- res.json({
32
- projects,
33
- currentProject: projects[0] ? projects[0].name : null
43
+ const snapshot = await getProjectListSnapshot('opencode', {
44
+ fallbackValue: emptyProjectList(null),
45
+ force: req.query?.fresh === '1',
46
+ refresh: buildProjectsPayload
34
47
  });
48
+ res.json({ ...snapshot.value, meta: snapshot.meta });
35
49
  } catch (err) {
36
50
  console.error('[OpenCode API] Failed to get projects:', err);
37
51
 
@@ -65,6 +79,7 @@ module.exports = (config) => {
65
79
  }
66
80
 
67
81
  saveProjectOrder(order);
82
+ invalidateProjectSnapshots('opencode');
68
83
  res.json({ success: true });
69
84
  } catch (err) {
70
85
  console.error('[OpenCode API] Failed to save project order:', err);
@@ -84,6 +99,7 @@ module.exports = (config) => {
84
99
 
85
100
  const { projectName } = req.params;
86
101
  const result = deleteProject(projectName);
102
+ invalidateProjectSnapshots('opencode');
87
103
  res.json(result);
88
104
  } catch (err) {
89
105
  if (isNotFoundError(err)) {
@@ -1,33 +1,47 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
3
  const { getProjectsWithStats, saveProjectOrder, getProjectOrder, deleteProject } = require('../services/sessions');
4
+ const {
5
+ emptyProjectList,
6
+ getProjectListSnapshot,
7
+ invalidateProjectSnapshots
8
+ } = require('../services/project-snapshots');
4
9
 
5
10
  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
+
6
36
  // GET /api/projects - Get all projects with stats
7
37
  router.get('/', async (req, res) => {
8
38
  try {
9
- const projects = await getProjectsWithStats(config);
10
- const order = getProjectOrder(config);
11
-
12
- // Sort projects by saved order
13
- let sortedProjects = projects;
14
- if (order && order.length > 0) {
15
- const orderMap = new Map(order.map((name, idx) => [name, idx]));
16
- sortedProjects = [...projects].sort((a, b) => {
17
- const aIdx = orderMap.has(a.name) ? orderMap.get(a.name) : 999999;
18
- const bIdx = orderMap.has(b.name) ? orderMap.get(b.name) : 999999;
19
- if (aIdx === bIdx) {
20
- // Both are new, sort by lastUsed
21
- return (b.lastUsed || 0) - (a.lastUsed || 0);
22
- }
23
- return aIdx - bIdx;
24
- });
25
- }
26
-
27
- res.json({
28
- projects: sortedProjects,
29
- currentProject: config.currentProject || (sortedProjects[0] ? sortedProjects[0].name : null)
39
+ const snapshot = await getProjectListSnapshot('claude', {
40
+ fallbackValue: emptyProjectList(config.currentProject || null),
41
+ force: req.query?.fresh === '1',
42
+ refresh: buildProjectsPayload
30
43
  });
44
+ res.json({ ...snapshot.value, meta: snapshot.meta });
31
45
  } catch (error) {
32
46
  console.error('Error fetching projects:', error);
33
47
  res.status(500).json({ error: error.message });
@@ -42,6 +56,7 @@ module.exports = (config) => {
42
56
  return res.status(400).json({ error: 'Order must be an array' });
43
57
  }
44
58
  saveProjectOrder(config, order);
59
+ invalidateProjectSnapshots('claude');
45
60
  res.json({ success: true });
46
61
  } catch (error) {
47
62
  console.error('Error saving project order:', error);
@@ -96,6 +111,7 @@ module.exports = (config) => {
96
111
  'utf8'
97
112
  );
98
113
 
114
+ invalidateProjectSnapshots('claude');
99
115
  res.json({
100
116
  success: true,
101
117
  projectName,
@@ -113,6 +129,7 @@ module.exports = (config) => {
113
129
  try {
114
130
  const { projectName } = req.params;
115
131
  deleteProject(config, projectName);
132
+ invalidateProjectSnapshots('claude');
116
133
  res.json({ success: true });
117
134
  } catch (error) {
118
135
  console.error('Error deleting project:', error);