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.
Files changed (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +4 -6
  3. package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
  5. package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
  6. package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
  7. package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
  8. package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
  9. package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
  11. package/dist/web/assets/index-D5hyHPj7.js +2 -0
  12. package/dist/web/index.html +1 -1
  13. package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
  14. package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
  15. package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
  16. package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
  17. package/package.json +2 -2
  18. package/src/commands/update.js +1 -1
  19. package/src/config/paths.js +1 -0
  20. package/src/server/api/codex-sessions.js +14 -14
  21. package/src/server/api/gemini-sessions.js +52 -14
  22. package/src/server/api/omp-projects.js +2 -2
  23. package/src/server/api/omp-sessions.js +61 -23
  24. package/src/server/api/sessions.js +44 -2
  25. package/src/server/opencode-proxy-server.js +55 -1037
  26. package/src/server/services/claude-wire.js +694 -0
  27. package/src/server/services/codex-sessions.js +41 -233
  28. package/src/server/services/codex-wire.js +219 -0
  29. package/src/server/services/dashboard-snapshot-worker.js +5 -5
  30. package/src/server/services/gemini-sessions.js +67 -157
  31. package/src/server/services/gemini-wire.js +759 -0
  32. package/src/server/services/model-detector.js +49 -131
  33. package/src/server/services/omp-channels.js +23 -4
  34. package/src/server/services/omp-gateway.js +29 -1
  35. package/src/server/services/omp-sessions.js +102 -105
  36. package/src/server/services/omp-settings-manager.js +27 -3
  37. package/src/server/services/opencode-gateway-adapters.js +22 -1147
  38. package/src/server/services/opencode-normalization.js +546 -0
  39. package/src/server/services/opencode-sessions.js +339 -546
  40. package/src/server/services/session-converter.js +4 -4
  41. package/src/server/services/session-history-adapters/claude.js +235 -0
  42. package/src/server/services/session-history-adapters/codex.js +158 -0
  43. package/src/server/services/session-history-adapters/gemini.js +292 -0
  44. package/src/server/services/session-history-adapters/index.js +20 -0
  45. package/src/server/services/session-history-adapters/omp.js +256 -0
  46. package/src/server/services/session-history-index.js +930 -0
  47. package/src/server/services/session-history-worker.js +99 -0
  48. package/src/server/services/session-snapshot-worker.js +7 -7
  49. package/src/server/services/sessions.js +167 -775
  50. package/src/server/services/speed-test.js +85 -303
  51. package/src/server/services/sqlite-connection.js +78 -0
  52. package/src/server/services/workspace-service.js +3 -3
  53. 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
- uniqueSessionIds.forEach((sessionId) => {
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