coding-tool-x 3.8.7 → 3.8.9

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 (143) hide show
  1. package/README.md +2 -2
  2. package/dist/web/assets/{AgentsDrawer-C7ePByZx.js → AgentsDrawer-Cj5CJ4d9.js} +1 -1
  3. package/dist/web/assets/Analytics-1doS-8uC.js +1 -0
  4. package/dist/web/assets/AssetPathField-C9TOPl7o.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-Ccl4rhdz.js → ChatHistoryDrawer-BTmQ1aQr.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-MxG_UD4o.js → CollapseItem-CwqJk3bE.js} +1 -1
  7. package/dist/web/assets/{CommandsDrawer-DWbaxHRZ.js → CommandsDrawer-CaZWDI_G.js} +1 -1
  8. package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +1 -0
  9. package/dist/web/assets/{ConfigTemplates-AiRPv2cd.js → ConfigTemplates-gyERg9iC.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesDrawer-CKMFsfEF.js → ConfigTemplatesDrawer-B17RqFi4.js} +1 -1
  11. package/dist/web/assets/{ConfigTemplatesPanel-Cvbnkvpt.js → ConfigTemplatesPanel-DKIC_FqG.js} +1 -1
  12. package/dist/web/assets/{Divider-D28g1gwj.js → Divider-DsdbkHrG.js} +1 -1
  13. package/dist/web/assets/{FavoritesDrawer-DA-dR_vP.js → FavoritesDrawer-DaziQmBp.js} +1 -1
  14. package/dist/web/assets/Forward-DLCL7aMY.js +1 -0
  15. package/dist/web/assets/{GatewayConvertDrawer-402_raGb.js → GatewayConvertDrawer-OMgWUUSc.js} +1 -1
  16. package/dist/web/assets/Home-DOSUTfrE.js +1 -0
  17. package/dist/web/assets/{MarkdownEditor-BvkDECwH.js → MarkdownEditor-9TTwTS-U.js} +1 -1
  18. package/dist/web/assets/McpDrawer-I8rl8lGH.js +1 -0
  19. package/dist/web/assets/{McpFormDrawer-DPMnyt5L.js → McpFormDrawer-DEP3C7ut.js} +1 -1
  20. package/dist/web/assets/{PluginManager-FKVeugr4.js → PluginManager-BuRcWuBj.js} +1 -1
  21. package/dist/web/assets/{PluginsDrawer-MvM1R55d.js → PluginsDrawer-_zZENaQn.js} +1 -1
  22. package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +1 -0
  23. package/dist/web/assets/PluginsPanel-CzGMK-Ap.css +1 -0
  24. package/dist/web/assets/ProjectList-BSxuL2td.js +1 -0
  25. package/dist/web/assets/{ProjectList-677RLZ8O.css → ProjectList-VbIYSHbH.css} +1 -1
  26. package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +1 -0
  27. package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +1 -0
  28. package/dist/web/assets/RecentSessionsDrawer-yrAD4p1A.css +1 -0
  29. package/dist/web/assets/SessionCard-BL_x0d3t.js +1 -0
  30. package/dist/web/assets/SessionList-DhRYZ6vl.js +1 -0
  31. package/dist/web/assets/SessionList-qG_XpPBJ.css +1 -0
  32. package/dist/web/assets/{SettingsDrawer-lMjttbkD.js → SettingsDrawer-fYb0DU7X.js} +1 -1
  33. package/dist/web/assets/SkillManager-S698Hns8.js +1 -0
  34. package/dist/web/assets/{SkillsDrawer-DF6JsSyj.js → SkillsDrawer-Ms4lPCow.js} +1 -1
  35. package/dist/web/assets/{SkillsPanel-B3Nm5j32.css → SkillsPanel-C4ke56nF.css} +1 -1
  36. package/dist/web/assets/SkillsPanel-rqnqAOGL.js +1 -0
  37. package/dist/web/assets/{SpeedTestDrawer-BaZkZqPP.js → SpeedTestDrawer-Bbn38SfV.js} +1 -1
  38. package/dist/web/assets/Upload-CdyVHxCI.js +1 -0
  39. package/dist/web/assets/{WorkspaceDrawer-BFSwZ5yU.js → WorkspaceDrawer-L7HJRc6u.js} +1 -1
  40. package/dist/web/assets/{WorkspaceManager-BHL1uYct.js → WorkspaceManager-DsoNg2z8.js} +1 -1
  41. package/dist/web/assets/{WorkspacePanel-B5PooBtg.js → WorkspacePanel-Be8PVZah.js} +1 -1
  42. package/dist/web/assets/{headers-cf-QapUq.js → headers-B5JWGn3v.js} +1 -1
  43. package/dist/web/assets/{icons-B2Vg-IOC.js → icons-DmFe4b5j.js} +1 -1
  44. package/dist/web/assets/{index-BohDDAy_.css → index-3Lp6z8TQ.css} +1 -1
  45. package/dist/web/assets/index-BKlAWTH-.js +2 -0
  46. package/dist/web/assets/{light-Cp0t7v32.js → light-0eavxNvt.js} +1 -1
  47. package/dist/web/assets/platforms-BmzzwnSw.js +1 -0
  48. package/dist/web/assets/platforms-DWXgmy2w.js +1 -0
  49. package/dist/web/assets/session-visibility-BAmxh3cK.js +1 -0
  50. package/dist/web/assets/sessions-CC0j-KH_.js +1 -0
  51. package/dist/web/assets/sessions-D4yQtfKb.js +1 -0
  52. package/dist/web/assets/use-message-DqCwefG6.js +1 -0
  53. package/dist/web/assets/{useResponsiveDrawer-D-DDpbnI.js → useResponsiveDrawer-V_onl9NV.js} +1 -1
  54. package/dist/web/index.html +7 -7
  55. package/package.json +1 -1
  56. package/src/commands/daemon.js +27 -4
  57. package/src/commands/search.js +17 -2
  58. package/src/config/paths.js +2 -0
  59. package/src/platforms/channel-auth-registry.js +36 -6
  60. package/src/platforms/channel-auth-service.js +12 -2
  61. package/src/platforms/driver-registry.js +9 -1
  62. package/src/platforms/drivers/claude/session-history-adapter.js +72 -1
  63. package/src/platforms/drivers/claude/sessions-implementation.js +49 -7
  64. package/src/platforms/drivers/claude/sessions.js +30 -6
  65. package/src/platforms/drivers/codex/parser.js +57 -1
  66. package/src/platforms/drivers/codex/session-history-adapter.js +8 -2
  67. package/src/platforms/drivers/codex/sessions-implementation.js +25 -3
  68. package/src/platforms/drivers/codex/sessions.js +18 -1
  69. package/src/platforms/drivers/dsh/api-operations.js +396 -0
  70. package/src/platforms/drivers/dsh/channels-implementation.js +458 -0
  71. package/src/platforms/drivers/dsh/channels.js +191 -0
  72. package/src/platforms/drivers/dsh/common.js +571 -0
  73. package/src/platforms/drivers/dsh/native-config.js +93 -0
  74. package/src/platforms/drivers/dsh/native-logs.js +116 -0
  75. package/src/platforms/drivers/dsh/plugin-manager.js +117 -0
  76. package/src/platforms/drivers/dsh/projects.js +23 -0
  77. package/src/platforms/drivers/dsh/proxy-implementation.js +260 -0
  78. package/src/platforms/drivers/dsh/proxy.js +23 -0
  79. package/src/platforms/drivers/dsh/resources.js +346 -0
  80. package/src/platforms/drivers/dsh/sessions.js +587 -0
  81. package/src/platforms/drivers/dsh/statistics-implementation.js +238 -0
  82. package/src/platforms/drivers/dsh/statistics.js +24 -0
  83. package/src/platforms/drivers/gemini/session-history-adapter.js +49 -1
  84. package/src/platforms/drivers/gemini/sessions-implementation.js +46 -1
  85. package/src/platforms/drivers/gemini/sessions.js +2 -1
  86. package/src/platforms/drivers/omp/prompts.js +58 -2
  87. package/src/platforms/drivers/omp/proxy-implementation.js +25 -1
  88. package/src/platforms/drivers/omp/session-history-adapter.js +42 -1
  89. package/src/platforms/drivers/omp/sessions-implementation.js +51 -3
  90. package/src/platforms/drivers/omp/sessions.js +33 -2
  91. package/src/platforms/drivers/opencode/channels-implementation.js +25 -22
  92. package/src/platforms/drivers/opencode/channels.js +6 -1
  93. package/src/platforms/drivers/opencode/sessions-implementation.js +89 -9
  94. package/src/platforms/drivers/opencode/sessions.js +37 -1
  95. package/src/platforms/manifest-schema.js +43 -3
  96. package/src/platforms/manifests/dsh.json +542 -0
  97. package/src/platforms/manifests/omp.json +2 -1
  98. package/src/platforms/manifests/opencode.json +28 -0
  99. package/src/platforms/native-oauth-adapters.js +318 -18
  100. package/src/platforms/oauth-credentials-service.js +344 -13
  101. package/src/platforms/path-resolver.js +2 -0
  102. package/src/platforms/platform-path-context.js +12 -2
  103. package/src/platforms/registry.js +16 -1
  104. package/src/platforms/runtime.js +5 -0
  105. package/src/platforms/session-history-adapters.js +1 -0
  106. package/src/server/api/platform-api-config.js +23 -2
  107. package/src/server/api/platform-route-factory.js +9 -2
  108. package/src/server/api/platforms.js +19 -1
  109. package/src/server/api/plugins.js +108 -13
  110. package/src/server/index.js +35 -1
  111. package/src/server/services/config-registry-service.js +10 -1
  112. package/src/server/services/config-templates-service.js +1 -0
  113. package/src/server/services/plugins-service.js +474 -103
  114. package/src/server/services/session-history-index.js +900 -140
  115. package/src/server/services/session-history-worker.js +3 -1
  116. package/src/server/services/skill-service.js +214 -7
  117. package/src/server/services/usage-log-utils.js +1 -0
  118. package/src/server/services/workspace-service.js +39 -10
  119. package/src/shared/driver-factories/api.js +22 -6
  120. package/src/shared/driver-factories/projects.js +8 -0
  121. package/dist/web/assets/Analytics-BVG6ehFv.js +0 -1
  122. package/dist/web/assets/AssetPathField-BZI9NvEr.js +0 -1
  123. package/dist/web/assets/ConfigExportDrawer-DrawKwxE.js +0 -1
  124. package/dist/web/assets/Home-CTF8ZWKW.js +0 -1
  125. package/dist/web/assets/McpDrawer-DZGHkuDm.js +0 -1
  126. package/dist/web/assets/PluginsPanel-CKuBs38B.js +0 -1
  127. package/dist/web/assets/PluginsPanel-D6ZqITcR.css +0 -1
  128. package/dist/web/assets/ProjectList-tsjwjXV1.js +0 -1
  129. package/dist/web/assets/PromptsDrawer-B-X9ci8n.js +0 -1
  130. package/dist/web/assets/RecentSessionsDrawer-CKFsOYDM.css +0 -1
  131. package/dist/web/assets/RecentSessionsDrawer-wLktMF_c.js +0 -1
  132. package/dist/web/assets/SessionCard-DkJPy0TY.js +0 -1
  133. package/dist/web/assets/SessionList-BpYRz1cX.css +0 -1
  134. package/dist/web/assets/SessionList-Gzhh4FOZ.js +0 -1
  135. package/dist/web/assets/SkillManager-CzFxWxx-.js +0 -1
  136. package/dist/web/assets/SkillsPanel--lLEts9y.js +0 -1
  137. package/dist/web/assets/Upload-BEQ-3Kqq.js +0 -1
  138. package/dist/web/assets/index-BFInlZGI.js +0 -2
  139. package/dist/web/assets/platforms-CBkB45JP.js +0 -1
  140. package/dist/web/assets/platforms-FTpjxAT6.js +0 -1
  141. package/dist/web/assets/sessions-Cmy31slD.js +0 -1
  142. package/dist/web/assets/sessions-yC_oVVh8.js +0 -1
  143. package/dist/web/assets/use-message-DoLM8C1C.js +0 -1
@@ -53,6 +53,7 @@ const LEGACY_STATS_DIR = path.join(LEGACY_DIR, 'stats');
53
53
  const CONTROL_MANIFEST_PATH = path.join(CONFIG_DIR, 'effective-control.json');
54
54
  const SKILL_ARTIFACTS_DIR = path.join(STORAGE_DIR, 'skill-artifacts');
55
55
  const SKILL_REFRESH_TASKS_PATH = path.join(RUNTIME_DIR, 'skill-refresh-tasks.json');
56
+ const PLUGIN_REFRESH_TASKS_PATH = path.join(RUNTIME_DIR, 'plugin-refresh-tasks.json');
56
57
 
57
58
  // 旧目录(升级时自动合并到 ~/.cc-tool)
58
59
  const LEGACY_BASE_DIRS = [
@@ -416,6 +417,7 @@ const PATHS = {
416
417
  effectiveControlManifest: CONTROL_MANIFEST_PATH,
417
418
  skillArtifacts: SKILL_ARTIFACTS_DIR,
418
419
  skillRefreshTasks: SKILL_REFRESH_TASKS_PATH,
420
+ pluginRefreshTasks: PLUGIN_REFRESH_TASKS_PATH,
419
421
 
420
422
  // 渠道配置
421
423
  channelsDir: CHANNELS_DIR,
@@ -58,10 +58,32 @@ function scanNative(adapter) {
58
58
  }
59
59
 
60
60
  function scanOmp() {
61
- const snapshot = getOmpAuthProviderSnapshot({ forceRefresh: true, accountCheck: true });
61
+ const snapshot = getOmpAuthProviderSnapshot({ forceRefresh: true, accountCheck: true }) || {};
62
62
  const hasAccounts = (snapshot.providers || []).some(provider => (
63
63
  provider.loggedIn === true && Array.isArray(provider.accounts) && provider.accounts.length > 0
64
64
  ));
65
+ if (!hasAccounts) {
66
+ const native = readAllNativeOAuth('omp') || [];
67
+ let synced = null;
68
+ if (native.length) {
69
+ try {
70
+ synced = oauthStore.syncLocalCredential('omp');
71
+ } catch {
72
+ synced = null;
73
+ }
74
+ }
75
+ const credentials = synced?.credentials || [];
76
+ const candidates = native.map((metadata, index) => nativeCandidate('omp', metadata, credentials[index]));
77
+ return {
78
+ candidates,
79
+ nativeState: {
80
+ available: snapshot.available === true || candidates.length > 0,
81
+ providers: snapshot.providers || [],
82
+ checkedAt: snapshot.checkedAt
83
+ },
84
+ warnings: candidates.length ? [] : [snapshot.reason || 'omp: unavailable: no logged-in local provider']
85
+ };
86
+ }
65
87
  let synced = null;
66
88
  if (hasAccounts) {
67
89
  try {
@@ -81,13 +103,14 @@ function scanOmp() {
81
103
  const credential = credentials.find(item => (
82
104
  item.providerId === provider.id
83
105
  && (item.accountId === String(accountId || '') || item.accountEmail === String(accountEmail || ''))
84
- ));
106
+ )) || credentials
107
+ .filter(item => item.providerId === provider.id)[Number(accountId) - 1] || null;
85
108
  const ref = safeRef({
86
109
  credentialId: credential?.id,
87
110
  providerId: provider.id,
88
- accountId,
89
- identityKey,
90
- accountEmail
111
+ accountId: credential?.accountId || accountId,
112
+ identityKey: credential?.identityKey || identityKey,
113
+ accountEmail: credential?.accountEmail || accountEmail
91
114
  });
92
115
  if (!ref.accountId && !ref.identityKey && (provider.accounts || []).length > 1) continue;
93
116
  candidates.push({
@@ -132,8 +155,15 @@ const adapters = Object.freeze({
132
155
  },
133
156
  omp: {
134
157
  scan: scanOmp,
135
- quota: async () => ({ status: 'unavailable', error: 'OMP native OAuth quota is unavailable' }),
158
+ usage: (ref) => oauthStore.fetchCredentialUsage('omp', ref.credentialId),
159
+ quota: (ref) => oauthStore.fetchCredentialUsage('omp', ref.credentialId),
136
160
  channelServicePath: './drivers/omp/channels-implementation'
161
+ },
162
+ opencode: {
163
+ scan: () => scanNative('opencode'),
164
+ usage: (ref) => oauthStore.fetchCredentialUsage('opencode', ref.credentialId),
165
+ quota: (ref) => oauthStore.fetchCredentialUsage('opencode', ref.credentialId),
166
+ channelServicePath: './drivers/opencode/channels-implementation'
137
167
  }
138
168
  });
139
169
 
@@ -37,11 +37,21 @@ function resolveChannelAuthRef(adapter, channel) {
37
37
  const matched = candidates.find(candidate => matchChannel(channel, candidate));
38
38
  if (matched) return safeRef(matched.authRef);
39
39
 
40
+ // A channel may retain an old account ID after the local OAuth store rotates
41
+ // credentials. When there is exactly one account for the same provider, it
42
+ // is safe to rebind the stale reference for quota lookup.
43
+ const soleCandidate = candidates.length === 1 ? candidates[0] : null;
44
+ const soleRef = safeRef(soleCandidate?.authRef);
45
+ const providerId = current.providerId || String(channel?.oauthProviderId || '').trim();
46
+ if (soleCandidate && providerId && soleRef.providerId === providerId) {
47
+ return soleRef;
48
+ }
49
+
40
50
  // A channel created before OAuth credentials were synced can have no stable
41
51
  // reference. Only auto-select when the local scan found exactly one account.
42
52
  if (!current.credentialId && !current.providerId && !current.accountId
43
- && !current.identityKey && !current.accountEmail && candidates.length === 1) {
44
- return safeRef(candidates[0].authRef);
53
+ && !current.identityKey && !current.accountEmail && soleCandidate) {
54
+ return soleRef;
45
55
  }
46
56
 
47
57
  return current;
@@ -48,7 +48,15 @@ function getDriverRegistry() {
48
48
  'generic-mcp': createGenericMcpDriver,
49
49
  'generic-prompt': createGenericPromptDriver,
50
50
  'opencode-conversion': require('./drivers/opencode/conversion').createDriver,
51
- 'claude-health': require('./drivers/claude/health').createDriver
51
+ 'claude-health': require('./drivers/claude/health').createDriver,
52
+ 'dsh-api': require('./drivers/dsh/api-operations').createDriver,
53
+ 'dsh-projects': require('./drivers/dsh/projects').createDriver,
54
+ 'dsh-sessions': require('./drivers/dsh/sessions').createDriver,
55
+ 'dsh-native-config': require('./drivers/dsh/native-config').createDriver,
56
+ 'dsh-channels': require('./drivers/dsh/channels').createDriver,
57
+ 'dsh-proxy': require('./drivers/dsh/proxy').createDriver,
58
+ 'dsh-statistics': require('./drivers/dsh/statistics').createDriver,
59
+ 'dsh-native-logs': require('./drivers/dsh/native-logs').createDriver
52
60
  }
53
61
  });
54
62
  registerLegacyDrivers(defaultRegistry);
@@ -150,6 +150,77 @@ async function inventory({ projectsDir } = {}) {
150
150
  return descriptors;
151
151
  }
152
152
 
153
+ /** Read a bounded prefix for project/session list summaries. */
154
+ function readSessionSummaryHead(filePath, maxBytes = 64 * 1024) {
155
+ let fd;
156
+ try {
157
+ fd = fs.openSync(filePath, 'r');
158
+ const buffer = Buffer.alloc(maxBytes);
159
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
160
+ return buffer.subarray(0, bytesRead).toString('utf8')
161
+ .split(/\r?\n/)
162
+ .filter(line => line.trim())
163
+ .map(line => {
164
+ try { return JSON.parse(line); } catch (_) { return null; }
165
+ })
166
+ .filter(Boolean);
167
+ } catch (_) {
168
+ return [];
169
+ } finally {
170
+ if (fd !== undefined) {
171
+ try { fs.closeSync(fd); } catch (_) {}
172
+ }
173
+ }
174
+ }
175
+
176
+ async function summarize(descriptor) {
177
+ const lines = readSessionSummaryHead(descriptor.filePath);
178
+ const { fullPath, projectName: displayName } = parseRealProjectPath(
179
+ descriptor.projectHint,
180
+ { projectsDir: descriptor.projectsDir }
181
+ );
182
+ let firstMessage = null;
183
+ let gitBranch = null;
184
+ let provider = null;
185
+ let model = null;
186
+ let startedAt = null;
187
+ for (const json of lines) {
188
+ const payload = json.message && typeof json.message === 'object' && !Array.isArray(json.message)
189
+ ? json.message
190
+ : {};
191
+ const timestamp = json.timestamp || payload.timestamp || null;
192
+ const timestampMs = timestamp ? new Date(timestamp).getTime() : null;
193
+ if (timestampMs && startedAt === null) startedAt = timestampMs;
194
+ const metadata = json.type === 'metadata' || json.type === 'summary';
195
+ if (metadata) {
196
+ gitBranch = json.gitBranch || payload.gitBranch || gitBranch;
197
+ provider = json.provider || payload.provider || provider;
198
+ model = json.model || payload.model || model;
199
+ }
200
+ const role = json.role || payload.role
201
+ || (json.type === 'user' || json.type === 'human' ? 'user' : null);
202
+ const rawContent = json.content !== undefined ? json.content : payload.content;
203
+ if (!firstMessage && role === 'user') firstMessage = getClaudeUserText(rawContent) || null;
204
+ if (json.model || payload.model) model = json.model || payload.model;
205
+ if (json.provider || payload.provider) provider = json.provider || payload.provider;
206
+ }
207
+ return {
208
+ session: {
209
+ sessionId: descriptor.sessionId,
210
+ projectName: descriptor.projectHint || 'unknown',
211
+ projectDisplayName: displayName || descriptor.projectHint || 'unknown',
212
+ projectFullPath: fullPath || null,
213
+ firstMessage,
214
+ gitBranch,
215
+ provider,
216
+ model,
217
+ startedAt,
218
+ updatedAt: descriptor.mtimeMs,
219
+ extraJson: JSON.stringify({ cwd: fullPath || null })
220
+ }
221
+ };
222
+ }
223
+
153
224
  /**
154
225
  * Parse a Claude session file into normalized session + messages.
155
226
  * @param {{filePath: string, size: number, mtimeMs: number, sessionId: string, projectHint: string}} descriptor
@@ -310,4 +381,4 @@ async function parse(descriptor) {
310
381
  };
311
382
  }
312
383
 
313
- module.exports = { configure, inventory, parse, getClaudeUserText };
384
+ module.exports = { configure, inventory, summarize, parse, getClaudeUserText };
@@ -208,6 +208,10 @@ async function getProjectsWithStats(config = {}, options = {}) {
208
208
  return getSessionHistoryIndex(config).listProjects('claude', { ...options, config });
209
209
  }
210
210
 
211
+ async function getProjectsPage(config = {}, options = {}) {
212
+ return getSessionHistoryIndex(config).listProjectsPage('claude', { ...options, config });
213
+ }
214
+
211
215
  // ===== Path resolution helpers (kept for mutation/conversion paths) =====
212
216
 
213
217
  function parseRealProjectPath(encodedName, config = {}) {
@@ -327,7 +331,7 @@ function extractCwdFromSessionHeader(sessionFile) {
327
331
  // ===== Counts: delegates to index =====
328
332
 
329
333
  async function getProjectAndSessionCounts(config = {}, options = {}) {
330
- const projects = await getSessionHistoryIndex().listProjects('claude', { ...options, config });
334
+ const projects = await getSessionHistoryIndex().getAllProjects('claude', { ...options, config });
331
335
  let sessionCount = 0;
332
336
  for (const project of projects) sessionCount += project.sessionCount || 0;
333
337
  return { projectCount: projects.length, sessionCount };
@@ -362,6 +366,23 @@ async function getSessionsForProject(config, projectName, options = {}) {
362
366
  const totalSize = sessions.reduce((sum, s) => sum + (s.size || 0), 0);
363
367
  return { sessions, totalSize };
364
368
  }
369
+
370
+ async function getSessionsPage(config, projectName, options = {}) {
371
+ const payload = await getSessionHistoryIndex(config).listSessionsPage('claude', projectName, { ...options, config });
372
+ const forkRelations = getForkRelations();
373
+ return {
374
+ ...payload,
375
+ sessions: payload.sessions.map(session => ({
376
+ sessionId: session.sessionId,
377
+ mtime: session.mtime,
378
+ size: session.size,
379
+ filePath: session.filePath,
380
+ gitBranch: session.gitBranch || null,
381
+ firstMessage: session.firstMessage || null,
382
+ forkedFrom: forkRelations[session.sessionId] || null
383
+ }))
384
+ };
385
+ }
365
386
  // ===== Mutations (sync) =====
366
387
 
367
388
  function deleteSession(config, projectName, sessionId) {
@@ -439,6 +460,7 @@ async function searchSessions(config, projectName, keyword, contextLength = 15,
439
460
  const results = await getSessionHistoryIndex().searchSessions('claude', keyword, { projectName, contextLength, ...options, config });
440
461
  const aliases = loadAliases();
441
462
  return results.map(r => ({
463
+ ...r,
442
464
  sessionId: r.sessionId,
443
465
  alias: aliases[r.sessionId] || null,
444
466
  matchCount: r.matchCount,
@@ -465,18 +487,25 @@ async function getRecentSessions(config, limit = 5, options = {}) {
465
487
  }));
466
488
  }
467
489
 
468
- async function searchSessionsAcrossProjects(config, keyword, contextLength = 35) {
490
+ async function searchSessionsAcrossProjects(config, keyword, limit = 35, options = {}) {
469
491
  const allResults = [];
492
+ const searchOptions = {
493
+ ...options,
494
+ contextLength: Number(options.contextLength) || 35,
495
+ limit: Math.max(1, Math.min(200, Number(limit) || 35)),
496
+ config,
497
+ projectName: options.projectName || null
498
+ };
470
499
 
471
500
  try {
472
- const claudeResults = await getSessionHistoryIndex().searchSessions('claude', keyword, { contextLength });
501
+ const claudeResults = await getSessionHistoryIndex().searchSessions('claude', keyword, searchOptions);
473
502
  for (const r of claudeResults) allResults.push({ ...r, channel: 'claude' });
474
503
  } catch (_) {}
475
504
 
476
505
  try {
477
506
  if (fs.existsSync(CODEX_PROJECTS_DIR)) {
478
507
  const { searchSessions: codexSearch } = require('../codex/sessions-implementation');
479
- const codexResults = await codexSearch(keyword);
508
+ const codexResults = await codexSearch(keyword, searchOptions);
480
509
  for (const r of codexResults) allResults.push({ ...r, channel: 'codex' });
481
510
  }
482
511
  } catch (_) {}
@@ -484,13 +513,24 @@ async function searchSessionsAcrossProjects(config, keyword, contextLength = 35)
484
513
  try {
485
514
  if (fs.existsSync(GEMINI_PROJECTS_DIR)) {
486
515
  const { searchSessions: geminiSearch } = require('../gemini/sessions-implementation');
487
- const geminiResults = await geminiSearch(keyword, contextLength);
516
+ const geminiResults = await geminiSearch(keyword, searchOptions.contextLength, searchOptions);
488
517
  for (const r of geminiResults) allResults.push({ ...r, channel: 'gemini' });
489
518
  }
490
519
  } catch (_) {}
491
520
 
492
- allResults.sort((a, b) => (b.matchCount || 0) - (a.matchCount || 0));
493
- return allResults;
521
+ allResults.sort((a, b) => {
522
+ const updatedA = sessionTimestamp(a.updatedAt ?? a.lastUpdated ?? a.mtimeMs ?? a.mtime);
523
+ const updatedB = sessionTimestamp(b.updatedAt ?? b.lastUpdated ?? b.mtimeMs ?? b.mtime);
524
+ return updatedB - updatedA || String(a.sessionId || '').localeCompare(String(b.sessionId || ''));
525
+ });
526
+ return allResults.slice(0, searchOptions.limit);
527
+ }
528
+
529
+ function sessionTimestamp(value) {
530
+ const numeric = Number(value);
531
+ if (Number.isFinite(numeric) && numeric > 0) return numeric;
532
+ const parsed = Date.parse(String(value || ''));
533
+ return Number.isFinite(parsed) ? parsed : 0;
494
534
  }
495
535
  function hasActualMessages(sessionFile) {
496
536
  if (!sessionFile || !fs.existsSync(sessionFile)) return false;
@@ -513,7 +553,9 @@ module.exports = {
513
553
  configure,
514
554
  getProjects,
515
555
  getProjectsWithStats,
556
+ getProjectsPage,
516
557
  getSessionsForProject,
558
+ getSessionsPage,
517
559
  deleteSession,
518
560
  forkSession,
519
561
  launch,
@@ -11,7 +11,7 @@ function createDriver(context = {}) {
11
11
  servicePath: './claude/sessions-implementation',
12
12
  localServicePath: '../platforms/drivers/claude/sessions-implementation',
13
13
  adapterLocalPath: '../platforms/drivers/claude/session-history-adapter',
14
- adapterMethods: { inventory: 'inventory', parse: 'parse' },
14
+ adapterMethods: { inventory: 'inventory', summarize: 'summarize', parse: 'parse' },
15
15
  methods: {
16
16
  launch: 'launch',
17
17
  saveSessionOrder: 'saveSessionOrder',
@@ -44,6 +44,14 @@ function createDriver(context = {}) {
44
44
  request
45
45
  );
46
46
  },
47
+ listSessionsPage: (service, projectNameOrRequest, options = {}) => {
48
+ const descriptorRequest = projectNameOrRequest && typeof projectNameOrRequest === 'object';
49
+ const request = descriptorRequest ? projectNameOrRequest : options;
50
+ const projectName = descriptorRequest
51
+ ? request.params?.projectName
52
+ : projectNameOrRequest;
53
+ return service.getSessionsPage(request.config || {}, projectName, request);
54
+ },
47
55
  recent: (service, limitOrRequest, options = {}) => {
48
56
  const descriptorRequest = limitOrRequest && typeof limitOrRequest === 'object';
49
57
  const request = descriptorRequest ? limitOrRequest : options;
@@ -59,11 +67,27 @@ function createDriver(context = {}) {
59
67
  contextLength,
60
68
  options
61
69
  ),
62
- searchAcrossProjects: (service, keyword, limit, options = {}) => service.searchSessionsAcrossProjects(
63
- options.config || {},
64
- keyword,
65
- limit
66
- )
70
+ searchAcrossProjects: (service, requestOrKeyword, limit, options = {}) => {
71
+ if (requestOrKeyword && typeof requestOrKeyword === 'object' && !Array.isArray(requestOrKeyword)) {
72
+ const request = requestOrKeyword;
73
+ const query = request.query || {};
74
+ const requestLimit = Number.parseInt(query.limit, 10) || 35;
75
+ return service.searchSessionsAcrossProjects(
76
+ request.config || options.config || {},
77
+ query.keyword || query.q || query.query || '',
78
+ requestLimit,
79
+ {
80
+ ...request,
81
+ ...options,
82
+ config: request.config || options.config || {},
83
+ projectName: query.projectName || query.project || null,
84
+ limit: requestLimit,
85
+ contextLength: Number(query.context) || 35
86
+ }
87
+ );
88
+ }
89
+ return service.searchSessionsAcrossProjects(options.config || {}, requestOrKeyword, limit, options);
90
+ }
67
91
  },
68
92
  onSuccess: operation => {
69
93
  if (['delete', 'fork', 'saveSessionOrder'].includes(operation)) {
@@ -339,11 +339,67 @@ function parseSession(filePath) {
339
339
  };
340
340
  }
341
341
 
342
+ /**
343
+ * Read only the beginning of a rollout for list views. Codex rollouts can be
344
+ * hundreds of megabytes; this helper must never call readJSONL/parseSession.
345
+ */
346
+ function parseSessionSummary(filePath, { sessionId = null, mtimeMs = null } = {}) {
347
+ let fd;
348
+ try {
349
+ fd = fs.openSync(filePath, 'r');
350
+ const buffer = Buffer.alloc(64 * 1024);
351
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
352
+ const lines = buffer.subarray(0, bytesRead).toString('utf8')
353
+ .split(/\r?\n/)
354
+ .filter(line => line.trim())
355
+ .map(line => {
356
+ try { return JSON.parse(line); } catch (_) { return null; }
357
+ })
358
+ .filter(Boolean);
359
+ const meta = extractSessionMeta(lines);
360
+ if (!meta) return null;
361
+ const firstUser = lines.find(line => {
362
+ const payload = line.type === 'response_item' ? line.payload : null;
363
+ return payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content);
364
+ });
365
+ const content = firstUser?.payload?.content
366
+ ?.map(block => block?.text || block?.input_text || '')
367
+ .join('\n')
368
+ .trim() || null;
369
+ const nativeSessionId = typeof meta.sessionId === 'string' && meta.sessionId.trim()
370
+ ? meta.sessionId.trim()
371
+ : sessionId;
372
+ const projectName = extractCodexProjectName(meta);
373
+ return {
374
+ session: {
375
+ sessionId: nativeSessionId,
376
+ projectName,
377
+ projectDisplayName: projectName,
378
+ projectFullPath: typeof meta.cwd === 'string' && meta.cwd.trim() ? meta.cwd.trim() : null,
379
+ firstMessage: content,
380
+ gitBranch: meta.git?.branch || null,
381
+ provider: meta.provider || null,
382
+ model: null,
383
+ startedAt: meta.timestamp ? new Date(meta.timestamp).getTime() : null,
384
+ updatedAt: mtimeMs,
385
+ extraJson: JSON.stringify({ cwd: meta.cwd || null })
386
+ }
387
+ };
388
+ } catch (_) {
389
+ return null;
390
+ } finally {
391
+ if (fd !== undefined) {
392
+ try { fs.closeSync(fd); } catch (_) {}
393
+ }
394
+ }
395
+ }
396
+
342
397
  module.exports = {
343
398
  readJSONL,
344
399
  extractSessionMeta,
345
400
  extractMessages,
346
401
  extractTokenUsage,
347
402
  parseSession,
348
- parseSessionMeta
403
+ parseSessionMeta,
404
+ parseSessionSummary
349
405
  };
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { getCodexDir } = require('./config');
6
- const { parseSession } = require('./parser');
6
+ const { parseSession, parseSessionSummary } = require('./parser');
7
7
 
8
8
  function configure({ pathContext } = {}) {
9
9
  require('./config').configure?.({ pathContext });
@@ -90,6 +90,12 @@ async function inventory({ projectsDir } = {}) {
90
90
  }));
91
91
  }
92
92
 
93
+ async function summarize(descriptor) {
94
+ const parsed = parseSessionSummary(descriptor.filePath, descriptor);
95
+ if (!parsed?.session) return { sessionId: descriptor.sessionId, projectName: descriptor.projectHint || 'unknown' };
96
+ return parsed;
97
+ }
98
+
93
99
  /**
94
100
  * Parse a Codex session file.
95
101
  */
@@ -169,4 +175,4 @@ async function parse(descriptor) {
169
175
  return { session, messages: normalizedMessages };
170
176
  }
171
177
 
172
- module.exports = { configure, inventory, parse, extractCodexProjectName };
178
+ module.exports = { configure, inventory, summarize, parse, extractCodexProjectName };
@@ -250,7 +250,7 @@ function normalizeSession(codexSession) {
250
250
  */
251
251
  async function getProjects(options = {}) {
252
252
  if (!fs.existsSync(getSessionsDir())) return [];
253
- const projects = await getSessionHistoryIndex({ ...options, projectsDir: getSessionsDir() }).listProjects('codex', options);
253
+ const projects = await getSessionHistoryIndex({ ...options, projectsDir: getSessionsDir() }).getAllProjects('codex', options);
254
254
  const savedOrder = getProjectOrder();
255
255
  if (savedOrder.length === 0) return projects;
256
256
 
@@ -260,6 +260,11 @@ async function getProjects(options = {}) {
260
260
  return [...ordered, ...byName.values()];
261
261
  }
262
262
 
263
+ async function getProjectsPage(options = {}) {
264
+ return getSessionHistoryIndex({ ...options, projectsDir: getSessionsDir() })
265
+ .listProjectsPage('codex', options);
266
+ }
267
+
263
268
  /**
264
269
  * 根据项目名获取会话列表(归一化格式)
265
270
  * @param {string} projectName - 项目名称
@@ -284,6 +289,21 @@ async function getSessionsByProject(projectName, options = {}) {
284
289
  return [...byId.values(), ...ordered];
285
290
  }
286
291
 
292
+ async function getSessionsPage(projectName, options = {}) {
293
+ const payload = await getSessionHistoryIndex().listSessionsPage('codex', projectName, options);
294
+ const forkRelations = require('../claude/sessions-implementation').getForkRelations();
295
+ const aliases = require('../../../server/services/alias').loadAliases();
296
+ return {
297
+ ...payload,
298
+ sessions: payload.sessions.map(session => ({
299
+ ...session,
300
+ mtime: new Date(session.mtime).toISOString(),
301
+ forkedFrom: forkRelations[session.sessionId] || null,
302
+ alias: aliases[session.sessionId] || null
303
+ }))
304
+ };
305
+ }
306
+
287
307
  /**
288
308
  * 根据 sessionId 获取会话(归一化格式)
289
309
  * @param {string} sessionId - 会话 ID
@@ -302,8 +322,8 @@ async function getSessionById(sessionId) {
302
322
  * @param {string} keyword - 搜索关键词
303
323
  * @returns {Array} 搜索结果
304
324
  */
305
- async function searchSessions(keyword) {
306
- return getSessionHistoryIndex().searchSessions('codex', keyword);
325
+ async function searchSessions(keyword, options = {}) {
326
+ return getSessionHistoryIndex().searchSessions('codex', keyword, options);
307
327
  }
308
328
 
309
329
  /**
@@ -825,7 +845,9 @@ module.exports = {
825
845
  scanSessionFiles,
826
846
  getAllSessions,
827
847
  getProjects,
848
+ getProjectsPage,
828
849
  getSessionsByProject,
850
+ getSessionsPage,
829
851
  getSessionById,
830
852
  searchSessions,
831
853
  normalizeSession,
@@ -11,9 +11,10 @@ function createDriver(context = {}) {
11
11
  servicePath: './codex/sessions-implementation',
12
12
  localServicePath: '../platforms/drivers/codex/sessions-implementation',
13
13
  adapterLocalPath: '../platforms/drivers/codex/session-history-adapter',
14
- adapterMethods: { inventory: 'inventory', parse: 'parse' },
14
+ adapterMethods: { inventory: 'inventory', summarize: 'summarize', parse: 'parse' },
15
15
  methods: {
16
16
  listSessions: 'getSessionsByProject',
17
+ listSessionsPage: 'getSessionsPage',
17
18
  recent: 'getRecentSessions',
18
19
  search: 'searchSessions',
19
20
  getSessionById: 'getSessionById',
@@ -25,6 +26,22 @@ function createDriver(context = {}) {
25
26
  status: 'getSessionStatus',
26
27
  messages: 'getSessionMessages'
27
28
  },
29
+ customMethods: {
30
+ searchAcrossProjects: (service, requestOrKeyword, limit, options = {}) => {
31
+ if (requestOrKeyword && typeof requestOrKeyword === 'object' && !Array.isArray(requestOrKeyword)) {
32
+ const request = requestOrKeyword;
33
+ const query = request.query || {};
34
+ return service.searchSessions(query.keyword || query.q || '', {
35
+ ...request,
36
+ ...options,
37
+ projectName: query.projectName || query.project || null,
38
+ limit: Number.parseInt(query.limit, 10) || 35,
39
+ contextLength: Number(query.context) || 35
40
+ });
41
+ }
42
+ return service.searchSessions(requestOrKeyword, { ...options, limit });
43
+ }
44
+ },
28
45
  onSuccess: operation => {
29
46
  if (['delete', 'fork', 'saveSessionOrder'].includes(operation)) {
30
47
  context.sessionHistoryIndex?.invalidateSource('codex');