coding-tool-x 3.8.0 → 3.8.2
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.
- package/CHANGELOG.md +2 -0
- package/README.md +4 -6
- package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
- package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
- package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
- package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
- package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
- package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
- package/dist/web/assets/index-D5hyHPj7.js +2 -0
- package/dist/web/index.html +1 -1
- package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
- package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
- package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
- package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
- package/package.json +2 -2
- package/src/commands/update.js +1 -1
- package/src/config/paths.js +1 -0
- package/src/server/api/codex-sessions.js +14 -14
- package/src/server/api/gemini-sessions.js +52 -14
- package/src/server/api/omp-projects.js +2 -2
- package/src/server/api/omp-sessions.js +61 -23
- package/src/server/api/sessions.js +44 -2
- package/src/server/opencode-proxy-server.js +55 -1037
- package/src/server/services/claude-wire.js +694 -0
- package/src/server/services/codex-sessions.js +41 -233
- package/src/server/services/codex-wire.js +219 -0
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/gemini-wire.js +759 -0
- package/src/server/services/model-detector.js +49 -131
- package/src/server/services/omp-channels.js +23 -4
- package/src/server/services/omp-gateway.js +29 -1
- package/src/server/services/omp-sessions.js +102 -105
- package/src/server/services/omp-settings-manager.js +27 -3
- package/src/server/services/opencode-gateway-adapters.js +22 -1147
- package/src/server/services/opencode-normalization.js +546 -0
- package/src/server/services/opencode-sessions.js +339 -546
- package/src/server/services/session-converter.js +4 -4
- package/src/server/services/session-history-adapters/claude.js +235 -0
- package/src/server/services/session-history-adapters/codex.js +158 -0
- package/src/server/services/session-history-adapters/gemini.js +292 -0
- package/src/server/services/session-history-adapters/index.js +20 -0
- package/src/server/services/session-history-adapters/omp.js +256 -0
- package/src/server/services/session-history-index.js +930 -0
- package/src/server/services/session-history-worker.js +99 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/speed-test.js +85 -303
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
- package/dist/web/assets/index-DIWJmyL6.js +0 -2
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
getProjectPath,
|
|
12
12
|
getAllSessions
|
|
13
13
|
} = require('../services/gemini-sessions');
|
|
14
|
+
const { getSessionStatus, getSessionOutline, getMessagePage } = require('../services/session-history-index');
|
|
14
15
|
const { isGeminiInstalled } = require('../services/gemini-config');
|
|
15
16
|
const { loadAliases } = require('../services/alias');
|
|
16
17
|
const {
|
|
@@ -45,7 +46,7 @@ module.exports = (config) => {
|
|
|
45
46
|
* GET /api/gemini/sessions/search/global?keyword=xxx
|
|
46
47
|
* 全局搜索
|
|
47
48
|
*/
|
|
48
|
-
router.get('/search/global', (req, res) => {
|
|
49
|
+
router.get('/search/global', async (req, res) => {
|
|
49
50
|
try {
|
|
50
51
|
if (!isGeminiInstalled()) {
|
|
51
52
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -57,7 +58,7 @@ module.exports = (config) => {
|
|
|
57
58
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const results = searchSessions(keyword);
|
|
61
|
+
const results = await searchSessions(keyword);
|
|
61
62
|
|
|
62
63
|
res.json({
|
|
63
64
|
keyword,
|
|
@@ -75,14 +76,14 @@ module.exports = (config) => {
|
|
|
75
76
|
* GET /api/gemini/sessions/recent/list?limit=10
|
|
76
77
|
* 获取最近会话
|
|
77
78
|
*/
|
|
78
|
-
router.get('/recent/list', (req, res) => {
|
|
79
|
+
router.get('/recent/list', async (req, res) => {
|
|
79
80
|
try {
|
|
80
81
|
if (!isGeminiInstalled()) {
|
|
81
82
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
const limit = parseInt(req.query.limit) || 5;
|
|
85
|
-
const sessions = getRecentSessions(limit);
|
|
86
|
+
const sessions = await getRecentSessions(limit);
|
|
86
87
|
|
|
87
88
|
res.json({
|
|
88
89
|
sessions,
|
|
@@ -125,7 +126,7 @@ module.exports = (config) => {
|
|
|
125
126
|
* GET /api/gemini/sessions/:projectHash/search
|
|
126
127
|
* 搜索项目内会话内容
|
|
127
128
|
*/
|
|
128
|
-
router.get('/:projectHash/search', (req, res) => {
|
|
129
|
+
router.get('/:projectHash/search', async (req, res) => {
|
|
129
130
|
try {
|
|
130
131
|
if (!isGeminiInstalled()) {
|
|
131
132
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -141,7 +142,7 @@ module.exports = (config) => {
|
|
|
141
142
|
const contextLength = context ? parseInt(context) : 35;
|
|
142
143
|
|
|
143
144
|
// 搜索所有会话,然后过滤该项目的会话
|
|
144
|
-
const allResults = searchSessions(keyword, contextLength);
|
|
145
|
+
const allResults = await searchSessions(keyword, contextLength);
|
|
145
146
|
const results = allResults.filter(r => r.projectHash === projectHash);
|
|
146
147
|
|
|
147
148
|
res.json({
|
|
@@ -155,14 +156,26 @@ module.exports = (config) => {
|
|
|
155
156
|
}
|
|
156
157
|
});
|
|
157
158
|
|
|
158
|
-
router.get('/:projectHash/:sessionId/status', (req, res) => {
|
|
159
|
+
router.get('/:projectHash/:sessionId/status', async (req, res) => {
|
|
160
|
+
// Try session-history-index first for fast lookup
|
|
161
|
+
try {
|
|
162
|
+
const idxStatus = await getSessionStatus('gemini', req.params.sessionId, { consistency: 'stale-ok' });
|
|
163
|
+
if (idxStatus) {
|
|
164
|
+
return res.json({
|
|
165
|
+
sessionId: idxStatus.sessionId,
|
|
166
|
+
lastModified: new Date(idxStatus.lastModified).toISOString(),
|
|
167
|
+
size: idxStatus.size
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (_) { /* fall through to direct lookup */ }
|
|
171
|
+
|
|
159
172
|
try {
|
|
160
173
|
if (!isGeminiInstalled()) {
|
|
161
174
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
const { sessionId } = req.params;
|
|
165
|
-
const session = getSessionById(sessionId);
|
|
178
|
+
const session = await getSessionById(sessionId);
|
|
166
179
|
if (!session) {
|
|
167
180
|
return res.status(404).json({ error: 'Session not found' });
|
|
168
181
|
}
|
|
@@ -178,14 +191,25 @@ module.exports = (config) => {
|
|
|
178
191
|
}
|
|
179
192
|
});
|
|
180
193
|
|
|
181
|
-
router.get('/:projectHash/:sessionId/outline', (req, res) => {
|
|
194
|
+
router.get('/:projectHash/:sessionId/outline', async (req, res) => {
|
|
195
|
+
// Try session-history-index first for fast lookup
|
|
196
|
+
try {
|
|
197
|
+
const idxOutline = await getSessionOutline('gemini', req.params.sessionId, { consistency: 'stale-ok' });
|
|
198
|
+
if (idxOutline) {
|
|
199
|
+
return res.json({
|
|
200
|
+
sessionId: idxOutline.sessionId,
|
|
201
|
+
items: idxOutline.items
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} catch (_) { /* fall through to direct read */ }
|
|
205
|
+
|
|
182
206
|
try {
|
|
183
207
|
if (!isGeminiInstalled()) {
|
|
184
208
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
185
209
|
}
|
|
186
210
|
|
|
187
211
|
const { sessionId } = req.params;
|
|
188
|
-
const session = getSessionById(sessionId);
|
|
212
|
+
const session = await getSessionById(sessionId);
|
|
189
213
|
if (!session) {
|
|
190
214
|
return res.status(404).json({ error: 'Session not found' });
|
|
191
215
|
}
|
|
@@ -218,7 +242,21 @@ module.exports = (config) => {
|
|
|
218
242
|
* GET /api/gemini/sessions/:projectHash/:sessionId/messages
|
|
219
243
|
* 获取会话的消息列表
|
|
220
244
|
*/
|
|
221
|
-
router.get('/:projectHash/:sessionId/messages', (req, res) => {
|
|
245
|
+
router.get('/:projectHash/:sessionId/messages', async (req, res) => {
|
|
246
|
+
const startMs = Date.now();
|
|
247
|
+
// Try session-history-index first for fast lookup
|
|
248
|
+
try {
|
|
249
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
250
|
+
const pageNum = parseInt(page) || 1;
|
|
251
|
+
const limitNum = parseInt(limit) || 20;
|
|
252
|
+
const idxPage = await getMessagePage('gemini', req.params.sessionId, {
|
|
253
|
+
page: pageNum, limit: limitNum, order
|
|
254
|
+
});
|
|
255
|
+
if (idxPage) {
|
|
256
|
+
return res.json(idxPage);
|
|
257
|
+
}
|
|
258
|
+
} catch (_) { /* fall through to direct file read */ }
|
|
259
|
+
|
|
222
260
|
try {
|
|
223
261
|
if (!isGeminiInstalled()) {
|
|
224
262
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -227,7 +265,7 @@ module.exports = (config) => {
|
|
|
227
265
|
const { sessionId } = req.params;
|
|
228
266
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
229
267
|
|
|
230
|
-
const session = getSessionById(sessionId);
|
|
268
|
+
const session = await getSessionById(sessionId);
|
|
231
269
|
|
|
232
270
|
if (!session) {
|
|
233
271
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -442,7 +480,7 @@ module.exports = (config) => {
|
|
|
442
480
|
* POST /api/gemini/sessions/:projectHash/:sessionId/launch
|
|
443
481
|
* 获取会话启动命令(用于复制)
|
|
444
482
|
*/
|
|
445
|
-
router.post('/:projectHash/:sessionId/launch', (req, res) => {
|
|
483
|
+
router.post('/:projectHash/:sessionId/launch', async (req, res) => {
|
|
446
484
|
try {
|
|
447
485
|
if (!isGeminiInstalled()) {
|
|
448
486
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -451,7 +489,7 @@ module.exports = (config) => {
|
|
|
451
489
|
const { projectHash, sessionId } = req.params;
|
|
452
490
|
|
|
453
491
|
// 获取会话详情
|
|
454
|
-
const session = getSessionById(sessionId);
|
|
492
|
+
const session = await getSessionById(sessionId);
|
|
455
493
|
|
|
456
494
|
if (!session) {
|
|
457
495
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -66,12 +66,12 @@ module.exports = () => {
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
router.delete('/:projectName', (req, res) => {
|
|
69
|
+
router.delete('/:projectName', async (req, res) => {
|
|
70
70
|
try {
|
|
71
71
|
if (!isOmpInstalled()) {
|
|
72
72
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
73
73
|
}
|
|
74
|
-
const result = deleteProject(req.params.projectName);
|
|
74
|
+
const result = await deleteProject(req.params.projectName);
|
|
75
75
|
invalidateProjectSnapshots('omp');
|
|
76
76
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
77
77
|
res.json(result);
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
isOmpInstalled,
|
|
15
15
|
HOME_DIR
|
|
16
16
|
} = require('../services/omp-sessions');
|
|
17
|
+
const { getSessionStatus, getSessionOutline, getMessagePage } = require('../services/session-history-index');
|
|
17
18
|
const { loadAliases } = require('../services/alias');
|
|
18
19
|
const { broadcastLog } = require('../websocket-server');
|
|
19
20
|
const {
|
|
@@ -35,7 +36,7 @@ function quoteForShell(value) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
module.exports = () => {
|
|
38
|
-
router.get('/search/global', (req, res) => {
|
|
39
|
+
router.get('/search/global', async (req, res) => {
|
|
39
40
|
try {
|
|
40
41
|
if (!isOmpInstalled()) {
|
|
41
42
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
@@ -46,7 +47,7 @@ module.exports = () => {
|
|
|
46
47
|
if (!keyword) {
|
|
47
48
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
48
49
|
}
|
|
49
|
-
const sessions = searchSessions(keyword, contextLength);
|
|
50
|
+
const sessions = await searchSessions(keyword, contextLength);
|
|
50
51
|
const totalMatches = sessions.reduce((sum, session) => sum + (session.matchCount || 0), 0);
|
|
51
52
|
res.json({ keyword, totalMatches, sessions, source: 'omp' });
|
|
52
53
|
} catch (err) {
|
|
@@ -55,20 +56,20 @@ module.exports = () => {
|
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
router.get('/recent/list', (req, res) => {
|
|
59
|
+
router.get('/recent/list', async (req, res) => {
|
|
59
60
|
try {
|
|
60
61
|
if (!isOmpInstalled()) {
|
|
61
62
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
62
63
|
}
|
|
63
64
|
const limit = parseInt(req.query.limit, 10) || 5;
|
|
64
|
-
res.json({ sessions: getRecentSessions(limit), source: 'omp' });
|
|
65
|
+
res.json({ sessions: await getRecentSessions(limit), source: 'omp' });
|
|
65
66
|
} catch (err) {
|
|
66
67
|
console.error('[OMP API] Failed to get recent sessions:', err);
|
|
67
68
|
res.status(500).json({ error: err.message });
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
router.get('/:projectName/search', (req, res) => {
|
|
72
|
+
router.get('/:projectName/search', async (req, res) => {
|
|
72
73
|
try {
|
|
73
74
|
if (!isOmpInstalled()) {
|
|
74
75
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
@@ -80,7 +81,7 @@ module.exports = () => {
|
|
|
80
81
|
if (!keyword) {
|
|
81
82
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
82
83
|
}
|
|
83
|
-
const sessions = searchSessions(keyword, contextLength, projectName);
|
|
84
|
+
const sessions = await searchSessions(keyword, contextLength, projectName);
|
|
84
85
|
const totalMatches = sessions.reduce((sum, session) => sum + (session.matchCount || 0), 0);
|
|
85
86
|
res.json({ keyword, totalMatches, sessions });
|
|
86
87
|
} catch (err) {
|
|
@@ -111,12 +112,24 @@ module.exports = () => {
|
|
|
111
112
|
}
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
router.get('/:projectName/:sessionId/status', (req, res) => {
|
|
115
|
+
router.get('/:projectName/:sessionId/status', async (req, res) => {
|
|
116
|
+
// Try session-history-index first for fast lookup
|
|
117
|
+
try {
|
|
118
|
+
const idxStatus = await getSessionStatus('omp', req.params.sessionId, { consistency: 'stale-ok' });
|
|
119
|
+
if (idxStatus) {
|
|
120
|
+
return res.json({
|
|
121
|
+
sessionId: idxStatus.sessionId,
|
|
122
|
+
lastModified: new Date(idxStatus.lastModified).toISOString(),
|
|
123
|
+
size: idxStatus.size
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (_) { /* fall through to direct lookup */ }
|
|
127
|
+
|
|
115
128
|
try {
|
|
116
129
|
if (!isOmpInstalled()) {
|
|
117
130
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
118
131
|
}
|
|
119
|
-
const session = getSessionById(req.params.sessionId);
|
|
132
|
+
const session = await getSessionById(req.params.sessionId);
|
|
120
133
|
if (!session) {
|
|
121
134
|
return res.status(404).json({ error: 'Session not found' });
|
|
122
135
|
}
|
|
@@ -131,12 +144,23 @@ module.exports = () => {
|
|
|
131
144
|
}
|
|
132
145
|
});
|
|
133
146
|
|
|
134
|
-
router.get('/:projectName/:sessionId/outline', (req, res) => {
|
|
147
|
+
router.get('/:projectName/:sessionId/outline', async (req, res) => {
|
|
148
|
+
// Try session-history-index first for fast lookup
|
|
149
|
+
try {
|
|
150
|
+
const idxOutline = await getSessionOutline('omp', req.params.sessionId, { consistency: 'stale-ok' });
|
|
151
|
+
if (idxOutline) {
|
|
152
|
+
return res.json({
|
|
153
|
+
sessionId: idxOutline.sessionId,
|
|
154
|
+
items: idxOutline.items
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
} catch (_) { /* fall through to direct read */ }
|
|
158
|
+
|
|
135
159
|
try {
|
|
136
160
|
if (!isOmpInstalled()) {
|
|
137
161
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
138
162
|
}
|
|
139
|
-
const session = getSessionById(req.params.sessionId);
|
|
163
|
+
const session = await getSessionById(req.params.sessionId);
|
|
140
164
|
if (!session) {
|
|
141
165
|
return res.status(404).json({ error: 'Session not found' });
|
|
142
166
|
}
|
|
@@ -160,14 +184,28 @@ module.exports = () => {
|
|
|
160
184
|
}
|
|
161
185
|
});
|
|
162
186
|
|
|
163
|
-
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
187
|
+
router.get('/:projectName/:sessionId/messages', async (req, res) => {
|
|
188
|
+
const startMs = Date.now();
|
|
189
|
+
// Try session-history-index first for fast lookup
|
|
190
|
+
try {
|
|
191
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
192
|
+
const pageNum = parseInt(page) || 1;
|
|
193
|
+
const limitNum = parseInt(limit) || 20;
|
|
194
|
+
const idxPage = await getMessagePage('omp', req.params.sessionId, {
|
|
195
|
+
page: pageNum, limit: limitNum, order
|
|
196
|
+
});
|
|
197
|
+
if (idxPage) {
|
|
198
|
+
return res.json(idxPage);
|
|
199
|
+
}
|
|
200
|
+
} catch (_) { /* fall through to direct file read */ }
|
|
201
|
+
|
|
164
202
|
try {
|
|
165
203
|
if (!isOmpInstalled()) {
|
|
166
204
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
167
205
|
}
|
|
168
206
|
const { sessionId } = req.params;
|
|
169
207
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
170
|
-
const session = getSessionById(sessionId);
|
|
208
|
+
const session = await getSessionById(sessionId);
|
|
171
209
|
if (!session) {
|
|
172
210
|
return res.status(404).json({ error: 'Session not found' });
|
|
173
211
|
}
|
|
@@ -204,12 +242,12 @@ module.exports = () => {
|
|
|
204
242
|
}
|
|
205
243
|
});
|
|
206
244
|
|
|
207
|
-
router.delete('/:projectName/:sessionId', (req, res) => {
|
|
245
|
+
router.delete('/:projectName/:sessionId', async (req, res) => {
|
|
208
246
|
try {
|
|
209
247
|
if (!isOmpInstalled()) {
|
|
210
248
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
211
249
|
}
|
|
212
|
-
const result = deleteSession(req.params.sessionId);
|
|
250
|
+
const result = await deleteSession(req.params.sessionId);
|
|
213
251
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
214
252
|
invalidateProjectSnapshots('omp');
|
|
215
253
|
res.json(result);
|
|
@@ -222,7 +260,7 @@ module.exports = () => {
|
|
|
222
260
|
}
|
|
223
261
|
});
|
|
224
262
|
|
|
225
|
-
router.post('/:projectName/batch-delete', (req, res) => {
|
|
263
|
+
router.post('/:projectName/batch-delete', async (req, res) => {
|
|
226
264
|
try {
|
|
227
265
|
if (!isOmpInstalled()) {
|
|
228
266
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
@@ -242,14 +280,14 @@ module.exports = () => {
|
|
|
242
280
|
}
|
|
243
281
|
const deletedSessionIds = [];
|
|
244
282
|
const failed = [];
|
|
245
|
-
|
|
283
|
+
for (const sessionId of uniqueSessionIds) {
|
|
246
284
|
try {
|
|
247
|
-
deleteSession(sessionId);
|
|
285
|
+
await deleteSession(sessionId);
|
|
248
286
|
deletedSessionIds.push(sessionId);
|
|
249
287
|
} catch (err) {
|
|
250
288
|
failed.push({ sessionId, error: err.message });
|
|
251
289
|
}
|
|
252
|
-
}
|
|
290
|
+
}
|
|
253
291
|
if (deletedSessionIds.length > 0) {
|
|
254
292
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
255
293
|
invalidateProjectSnapshots('omp');
|
|
@@ -267,12 +305,12 @@ module.exports = () => {
|
|
|
267
305
|
}
|
|
268
306
|
});
|
|
269
307
|
|
|
270
|
-
router.post('/:projectName/:sessionId/fork', (req, res) => {
|
|
308
|
+
router.post('/:projectName/:sessionId/fork', async (req, res) => {
|
|
271
309
|
try {
|
|
272
310
|
if (!isOmpInstalled()) {
|
|
273
311
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
274
312
|
}
|
|
275
|
-
const result = forkSession(req.params.sessionId, req.body || {});
|
|
313
|
+
const result = await forkSession(req.params.sessionId, req.body || {});
|
|
276
314
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
277
315
|
invalidateProjectSnapshots('omp');
|
|
278
316
|
res.json(result);
|
|
@@ -303,17 +341,17 @@ module.exports = () => {
|
|
|
303
341
|
}
|
|
304
342
|
});
|
|
305
343
|
|
|
306
|
-
router.post('/:projectName/:sessionId/launch', (req, res) => {
|
|
344
|
+
router.post('/:projectName/:sessionId/launch', async (req, res) => {
|
|
307
345
|
try {
|
|
308
346
|
if (!isOmpInstalled()) {
|
|
309
347
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
310
348
|
}
|
|
311
349
|
const { projectName, sessionId } = req.params;
|
|
312
|
-
const session = getSessionById(sessionId);
|
|
350
|
+
const session = await getSessionById(sessionId);
|
|
313
351
|
if (!session) {
|
|
314
352
|
return res.status(404).json({ error: 'Session not found' });
|
|
315
353
|
}
|
|
316
|
-
const project = getProjects().find(p => p.name === projectName);
|
|
354
|
+
const project = (await getProjects()).find(p => p.name === projectName);
|
|
317
355
|
const cwd = session.directory || project?.fullPath || HOME_DIR;
|
|
318
356
|
const command = buildLaunchCommand(sessionId, cwd, req.body || {});
|
|
319
357
|
const copyCommand = `cd ${quoteForShell(cwd)} && ${command}`;
|
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
|
|
7
|
+
const { getSessionStatus, getSessionOutline, getMessagePage } = require('../services/session-history-index');
|
|
7
8
|
const { loadAliases } = require('../services/alias');
|
|
8
9
|
const { broadcastLog } = require('../websocket-server');
|
|
9
10
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
@@ -376,7 +377,7 @@ module.exports = (config) => {
|
|
|
376
377
|
});
|
|
377
378
|
|
|
378
379
|
// GET /api/sessions/:projectName/search - Search sessions content
|
|
379
|
-
router.get('/:projectName/search', (req, res) => {
|
|
380
|
+
router.get('/:projectName/search', async (req, res) => {
|
|
380
381
|
try {
|
|
381
382
|
const { projectName } = req.params;
|
|
382
383
|
const { keyword, context } = req.query;
|
|
@@ -386,7 +387,7 @@ module.exports = (config) => {
|
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
const contextLength = context ? parseInt(context) : 15;
|
|
389
|
-
const results = searchSessions(config, projectName, keyword, contextLength);
|
|
390
|
+
const results = await searchSessions(config, projectName, keyword, contextLength);
|
|
390
391
|
|
|
391
392
|
res.json({
|
|
392
393
|
keyword,
|
|
@@ -402,6 +403,19 @@ module.exports = (config) => {
|
|
|
402
403
|
// GET /api/sessions/:projectName/:sessionId/status - Lightweight status for live sync
|
|
403
404
|
router.get('/:projectName/:sessionId/status', async (req, res) => {
|
|
404
405
|
try {
|
|
406
|
+
|
|
407
|
+
// Try session-history-index first for fast lookup
|
|
408
|
+
try {
|
|
409
|
+
const idxStatus = await getSessionStatus('claude', req.params.sessionId, { consistency: 'stale-ok' });
|
|
410
|
+
if (idxStatus) {
|
|
411
|
+
return res.json({
|
|
412
|
+
sessionId: idxStatus.sessionId,
|
|
413
|
+
lastModified: new Date(idxStatus.lastModified).toISOString(),
|
|
414
|
+
size: idxStatus.size
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
} catch (_) { /* fall through to direct stat */ }
|
|
418
|
+
|
|
405
419
|
const { projectName, sessionId } = req.params;
|
|
406
420
|
const { fullPath } = parseRealProjectPath(projectName);
|
|
407
421
|
const { sessionFile, triedPaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
|
|
@@ -428,6 +442,18 @@ module.exports = (config) => {
|
|
|
428
442
|
// GET /api/sessions/:projectName/:sessionId/outline - Lightweight user outline for TOC and forking
|
|
429
443
|
router.get('/:projectName/:sessionId/outline', async (req, res) => {
|
|
430
444
|
try {
|
|
445
|
+
|
|
446
|
+
// Try session-history-index first for fast lookup
|
|
447
|
+
try {
|
|
448
|
+
const idxOutline = await getSessionOutline('claude', req.params.sessionId, { consistency: 'stale-ok' });
|
|
449
|
+
if (idxOutline) {
|
|
450
|
+
return res.json({
|
|
451
|
+
sessionId: idxOutline.sessionId,
|
|
452
|
+
items: idxOutline.items
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
} catch (_) { /* fall through to direct read */ }
|
|
456
|
+
|
|
431
457
|
const { projectName, sessionId } = req.params;
|
|
432
458
|
const { fullPath } = parseRealProjectPath(projectName);
|
|
433
459
|
const { sessionFile, triedPaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
|
|
@@ -453,6 +479,22 @@ module.exports = (config) => {
|
|
|
453
479
|
// GET /api/sessions/:projectName/:sessionId/messages - Get session messages with pagination
|
|
454
480
|
router.get('/:projectName/:sessionId/messages', async (req, res) => {
|
|
455
481
|
try {
|
|
482
|
+
|
|
483
|
+
// Try session-history-index first for fast lookup
|
|
484
|
+
try {
|
|
485
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
486
|
+
const pageNum = parseInt(page) || 1;
|
|
487
|
+
const limitNum = parseInt(limit) || 20;
|
|
488
|
+
const idxPage = await getMessagePage('claude', req.params.sessionId, {
|
|
489
|
+
page: pageNum,
|
|
490
|
+
limit: limitNum,
|
|
491
|
+
order
|
|
492
|
+
});
|
|
493
|
+
if (idxPage) {
|
|
494
|
+
return res.json(idxPage);
|
|
495
|
+
}
|
|
496
|
+
} catch (_) { /* fall through to direct file read */ }
|
|
497
|
+
|
|
456
498
|
const { projectName, sessionId } = req.params;
|
|
457
499
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
458
500
|
|