coding-tool-x 3.7.4 → 3.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/web/assets/{Analytics-BmPIW7oK.js → Analytics-wCRLv26m.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CG4j4ua4.js → ConfigTemplates-aa1blEHh.js} +1 -1
  3. package/dist/web/assets/Home-CqnBXFxj.css +1 -0
  4. package/dist/web/assets/Home-eIxO50-k.js +1 -0
  5. package/dist/web/assets/{PluginManager-CCJ7mo35.js → PluginManager-BAwGFNjN.js} +1 -1
  6. package/dist/web/assets/ProjectList-CRiAEixW.js +1 -0
  7. package/dist/web/assets/ProjectList-Dw2omgOH.css +1 -0
  8. package/dist/web/assets/SessionList-BQ0FW6Gd.js +1 -0
  9. package/dist/web/assets/SessionList-kYJ6-Ies.css +1 -0
  10. package/dist/web/assets/{SkillManager-DmOTHcQ7.js → SkillManager-Bhzs1O5S.js} +1 -1
  11. package/dist/web/assets/{WorkspaceManager-DLVj0zlu.js → WorkspaceManager-CLdX-pTT.js} +1 -1
  12. package/dist/web/assets/index-B_nFqixU.css +1 -0
  13. package/dist/web/assets/index-ClMNA9Hf.js +2 -0
  14. package/dist/web/index.html +2 -2
  15. package/package.json +1 -1
  16. package/src/server/api/codex-projects.js +7 -11
  17. package/src/server/api/codex-sessions.js +30 -21
  18. package/src/server/api/dashboard.js +24 -36
  19. package/src/server/api/gemini-projects.js +7 -11
  20. package/src/server/api/gemini-sessions.js +28 -26
  21. package/src/server/api/omp-channels.js +50 -7
  22. package/src/server/api/omp-projects.js +6 -11
  23. package/src/server/api/omp-sessions.js +31 -17
  24. package/src/server/api/opencode-projects.js +6 -11
  25. package/src/server/api/opencode-sessions.js +28 -21
  26. package/src/server/api/projects.js +7 -28
  27. package/src/server/api/sessions.js +32 -16
  28. package/src/server/services/channel-balance.js +2 -1
  29. package/src/server/services/codex-sessions.js +23 -10
  30. package/src/server/services/dashboard-snapshot-worker.js +216 -0
  31. package/src/server/services/gemini-sessions.js +15 -3
  32. package/src/server/services/native-oauth-adapters.js +2 -1
  33. package/src/server/services/omp-auth-provider-worker.js +29 -0
  34. package/src/server/services/omp-auth-providers.js +137 -11
  35. package/src/server/services/omp-config.js +7 -4
  36. package/src/server/services/omp-sessions.js +38 -14
  37. package/src/server/services/opencode-sessions.js +9 -6
  38. package/src/server/services/plugins-service.js +2 -1
  39. package/src/server/services/project-snapshots.js +8 -1
  40. package/src/server/services/session-snapshot-worker.js +158 -0
  41. package/src/server/services/session-snapshots.js +145 -0
  42. package/src/server/services/sessions.js +6 -4
  43. package/src/server/services/snapshot-cache.js +123 -13
  44. package/dist/web/assets/Home-B9yAAZrD.js +0 -1
  45. package/dist/web/assets/Home-DSTUSxrT.css +0 -1
  46. package/dist/web/assets/ProjectList-D3a0oRum.css +0 -1
  47. package/dist/web/assets/ProjectList-DVUs3o8x.js +0 -1
  48. package/dist/web/assets/SessionList-D12muXvx.css +0 -1
  49. package/dist/web/assets/SessionList-DwssBydw.js +0 -1
  50. package/dist/web/assets/index-BjdNYwSs.js +0 -2
  51. package/dist/web/assets/index-kQCC5dlu.css +0 -1
@@ -615,7 +615,11 @@ function normalizeSession(geminiSession) {
615
615
  * @param {string} projectHash - 项目 hash
616
616
  * @returns {string|null} 项目路径
617
617
  */
618
- function getProjectPath(projectHash) {
618
+ function getProjectPath(projectHash, options = {}) {
619
+ if (options.force) {
620
+ pathMappingCache = null;
621
+ pathMappingCacheTime = 0;
622
+ }
619
623
  const pathMapping = buildPathMapping();
620
624
  return pathMapping.get(projectHash) || null;
621
625
  }
@@ -624,7 +628,11 @@ function getProjectPath(projectHash) {
624
628
  * 聚合项目列表
625
629
  * @returns {Array} 项目对象数组
626
630
  */
627
- function getProjects() {
631
+ function getProjects(options = {}) {
632
+ if (options.force) {
633
+ pathMappingCache = null;
634
+ pathMappingCacheTime = 0;
635
+ }
628
636
  const sessions = getAllSessions();
629
637
  const projectMap = new Map();
630
638
 
@@ -680,7 +688,11 @@ function getProjects() {
680
688
  * @param {string} projectHash - 项目 hash
681
689
  * @returns {Array} 会话对象数组
682
690
  */
683
- function getProjectSessions(projectHash) {
691
+ function getProjectSessions(projectHash, options = {}) {
692
+ if (options.force) {
693
+ pathMappingCache = null;
694
+ pathMappingCacheTime = 0;
695
+ }
684
696
  const allSessions = getAllSessions();
685
697
  return allSessions
686
698
  .filter(session => session.projectHash === projectHash)
@@ -1029,7 +1029,8 @@ function runOmpCommand(args = [], options = {}) {
1029
1029
  ...process.env,
1030
1030
  ...(options.env || {})
1031
1031
  },
1032
- timeout: options.timeout || 10000
1032
+ timeout: options.timeout || 10000,
1033
+ windowsHide: true
1033
1034
  });
1034
1035
  const status = result?.status === undefined || result?.status === null ? 0 : result.status;
1035
1036
  return {
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const { getOmpAuthProviderSnapshot } = require('./omp-auth-providers');
4
+
5
+ function attachWorkerHandler() {
6
+ process.on('message', (options = {}) => {
7
+ try {
8
+ const value = getOmpAuthProviderSnapshot(options);
9
+ if (process.send) {
10
+ process.send({ ok: true, value }, () => process.exit(0));
11
+ return;
12
+ }
13
+ } catch (error) {
14
+ if (process.send) {
15
+ process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
16
+ return;
17
+ }
18
+ }
19
+ process.exit(1);
20
+ });
21
+ }
22
+
23
+ if (process.env.CC_TOOL_OMP_AUTH_PROVIDER_WORKER === '1' || require.main === module) {
24
+ attachWorkerHandler();
25
+ }
26
+
27
+ module.exports = {
28
+ attachWorkerHandler
29
+ };
@@ -1,9 +1,11 @@
1
- const { spawnSync } = require('child_process');
1
+ const path = require('path');
2
+ const { fork, spawnSync } = require('child_process');
2
3
  const ompConfig = require('./omp-config');
3
4
 
4
5
  const COMMAND_TIMEOUT_MS = 5000;
5
6
  const TOKEN_LIST_TIMEOUT_MS = 3000;
6
7
  const CACHE_TTL_MS = 15000;
8
+ const WARMUP_WORKER_TIMEOUT_MS = COMMAND_TIMEOUT_MS + 1000;
7
9
 
8
10
  const LOGIN_PROVIDER_IDS = new Set([
9
11
  'openai-codex',
@@ -50,6 +52,20 @@ const PROVIDER_ALIASES = {
50
52
  };
51
53
 
52
54
  let snapshotCache = {};
55
+ const warmupTimers = new Map();
56
+ const warmupErrors = new Map();
57
+
58
+ function getSnapshotCacheKey(options = {}) {
59
+ return options.accountCheck === false
60
+ ? (options.includeStatus === false ? 'metadata' : 'metadata-status')
61
+ : 'full';
62
+ }
63
+
64
+ function writeSnapshotCache(cacheKey, loadedAt, value) {
65
+ snapshotCache[cacheKey] = { loadedAt, value };
66
+ warmupErrors.delete(cacheKey);
67
+ return value;
68
+ }
53
69
 
54
70
  function normalizeProviderId(value = '') {
55
71
  return String(value || '')
@@ -95,7 +111,8 @@ function runCommand(command, args = [], options = {}) {
95
111
  ...process.env,
96
112
  ...(options.env || {})
97
113
  },
98
- timeout: options.timeout || COMMAND_TIMEOUT_MS
114
+ timeout: options.timeout || COMMAND_TIMEOUT_MS,
115
+ windowsHide: true
99
116
  });
100
117
  if (typeof result === 'string' || Buffer.isBuffer(result)) {
101
118
  return { status: 0, stdout: normalizeCommandOutput(result), stderr: '' };
@@ -205,9 +222,7 @@ function buildEmptySnapshot(runtime, reason) {
205
222
 
206
223
  function getOmpAuthProviderSnapshot(options = {}) {
207
224
  const now = Date.now();
208
- const cacheKey = options.accountCheck === false
209
- ? (options.includeStatus === false ? 'metadata' : 'metadata-status')
210
- : 'full';
225
+ const cacheKey = getSnapshotCacheKey(options);
211
226
  const cached = snapshotCache[cacheKey];
212
227
  if (!options.forceRefresh && cached && now - cached.loadedAt < CACHE_TTL_MS) {
213
228
  return cached.value;
@@ -217,16 +232,14 @@ function getOmpAuthProviderSnapshot(options = {}) {
217
232
  const runtime = options.runtime || ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {});
218
233
  if (!runtime || runtime.runtime !== 'omp' || !runtime.installed) {
219
234
  const value = buildEmptySnapshot(runtime || null, 'omp-not-available');
220
- snapshotCache[cacheKey] = { loadedAt: now, value };
221
- return value;
235
+ return writeSnapshotCache(cacheKey, now, value);
222
236
  }
223
237
 
224
238
  const command = runtime.command;
225
239
  const listResult = parseJsonCommand(command, ['auth-broker', 'list', '--json'], options);
226
240
  if (!listResult.ok || !Array.isArray(listResult.value)) {
227
241
  const value = buildEmptySnapshot(runtime, listResult.error || listResult.stderr || 'auth-provider-list-failed');
228
- snapshotCache[cacheKey] = { loadedAt: now, value };
229
- return value;
242
+ return writeSnapshotCache(cacheKey, now, value);
230
243
  }
231
244
 
232
245
  const supportedProviders = listResult.value
@@ -268,8 +281,112 @@ function getOmpAuthProviderSnapshot(options = {}) {
268
281
  aliases: { ...PROVIDER_ALIASES },
269
282
  checkedAt: new Date().toISOString()
270
283
  };
271
- snapshotCache[cacheKey] = { loadedAt: now, value };
272
- return value;
284
+ return writeSnapshotCache(cacheKey, now, value);
285
+ }
286
+
287
+ function getCachedOmpAuthProviderSnapshot(options = {}) {
288
+ const cacheKey = getSnapshotCacheKey(options);
289
+ const cached = snapshotCache[cacheKey];
290
+ if (!cached) return null;
291
+ return {
292
+ ...cached.value,
293
+ stale: Date.now() - cached.loadedAt >= CACHE_TTL_MS
294
+ };
295
+ }
296
+
297
+ function getOmpAuthProviderCacheMeta(options = {}) {
298
+ const cacheKey = getSnapshotCacheKey(options);
299
+ const cached = snapshotCache[cacheKey];
300
+ const stale = !cached || Date.now() - cached.loadedAt >= CACHE_TTL_MS;
301
+ const cachedReason = cached?.value?.available === false ? cached.value.reason || null : null;
302
+ return {
303
+ cached: Boolean(cached),
304
+ stale,
305
+ refreshing: warmupTimers.has(cacheKey),
306
+ fallback: !cached,
307
+ checkedAt: cached?.value?.checkedAt || null,
308
+ error: warmupErrors.get(cacheKey) || cachedReason || null
309
+ };
310
+ }
311
+
312
+ function warmOmpAuthProviderSnapshot(options = {}) {
313
+ const cacheKey = getSnapshotCacheKey(options);
314
+ if (warmupTimers.has(cacheKey)) return;
315
+
316
+ const finishWarmup = (task, error = null, value = null) => {
317
+ if (!warmupTimers.has(cacheKey)) return;
318
+ if (task.timeout) clearTimeout(task.timeout);
319
+ if (task.child) {
320
+ task.child.removeAllListeners();
321
+ if (!task.child.killed) task.child.kill();
322
+ }
323
+ warmupTimers.delete(cacheKey);
324
+ if (error) {
325
+ warmupErrors.set(cacheKey, sanitizeIdentity(error?.message || String(error || 'auth-provider-warmup-failed')));
326
+ return;
327
+ }
328
+ if (value) {
329
+ writeSnapshotCache(cacheKey, Date.now(), value);
330
+ }
331
+ };
332
+
333
+ // Tests and callers that supply a function-based command runner cannot cross
334
+ // the process boundary. Production channel-list warmups use the worker path.
335
+ if (process.env.NODE_ENV === 'test' || options.commandRunner) {
336
+ const task = { timeout: null, child: null };
337
+ const timer = setTimeout(() => {
338
+ try {
339
+ finishWarmup(task, null, getOmpAuthProviderSnapshot(options));
340
+ } catch (error) {
341
+ finishWarmup(task, error);
342
+ }
343
+ }, 0);
344
+ task.timeout = timer;
345
+ warmupTimers.set(cacheKey, task);
346
+ return;
347
+ }
348
+
349
+ const workerPath = path.join(__dirname, 'omp-auth-provider-worker.js');
350
+ const task = { timeout: null, child: null };
351
+ try {
352
+ const child = fork(workerPath, [], {
353
+ stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
354
+ env: {
355
+ ...process.env,
356
+ CC_TOOL_OMP_AUTH_PROVIDER_WORKER: '1'
357
+ }
358
+ });
359
+ task.child = child;
360
+ task.timeout = setTimeout(() => {
361
+ finishWarmup(task, new Error('auth-provider-warmup-timed-out'));
362
+ }, WARMUP_WORKER_TIMEOUT_MS);
363
+ warmupTimers.set(cacheKey, task);
364
+
365
+ child.on('message', (message) => {
366
+ if (message?.ok) {
367
+ finishWarmup(task, null, message.value);
368
+ } else {
369
+ finishWarmup(task, new Error(message?.error || 'auth-provider-warmup-failed'));
370
+ }
371
+ });
372
+ child.on('error', (error) => finishWarmup(task, error));
373
+ child.on('exit', (code, signal) => {
374
+ if (warmupTimers.get(cacheKey) === task) {
375
+ finishWarmup(task, new Error(`auth-provider-warmup-worker-exited-${code || signal || 'unknown'}`));
376
+ }
377
+ });
378
+ child.send({
379
+ accountCheck: options.accountCheck,
380
+ includeStatus: options.includeStatus,
381
+ forceRefresh: options.forceRefresh === true,
382
+ runtime: options.runtime,
383
+ runtimeOptions: options.runtimeOptions
384
+ });
385
+ } catch (error) {
386
+ // `fork` itself can fail before the task is registered. Preserve the
387
+ // failure for the non-blocking status endpoint instead of dropping it.
388
+ warmupErrors.set(cacheKey, sanitizeIdentity(error?.message || String(error || 'auth-provider-warmup-failed')));
389
+ }
273
390
  }
274
391
 
275
392
  function findAuthProviderForKey(providerKey, snapshot) {
@@ -280,6 +397,12 @@ function findAuthProviderForKey(providerKey, snapshot) {
280
397
 
281
398
  function clearOmpAuthProviderCache() {
282
399
  snapshotCache = {};
400
+ for (const task of warmupTimers.values()) {
401
+ if (task.timeout) clearTimeout(task.timeout);
402
+ if (task.child && !task.child.killed) task.child.kill();
403
+ }
404
+ warmupTimers.clear();
405
+ warmupErrors.clear();
283
406
  }
284
407
 
285
408
  module.exports = {
@@ -287,7 +410,10 @@ module.exports = {
287
410
  ACCOUNT_CHECK_PROVIDER_IDS,
288
411
  clearOmpAuthProviderCache,
289
412
  findAuthProviderForKey,
413
+ getCachedOmpAuthProviderSnapshot,
414
+ getOmpAuthProviderCacheMeta,
290
415
  getOmpAuthProviderSnapshot,
416
+ warmOmpAuthProviderSnapshot,
291
417
  normalizeProviderId,
292
418
  resolveProviderAlias,
293
419
  sanitizeIdentity
@@ -61,7 +61,10 @@ function runOmpCommand(command, args = [], env = process.env, options = {}, stdi
61
61
  encoding: 'utf8',
62
62
  env: buildCommandEnv(env),
63
63
  stdio,
64
- timeout: options.timeout || 3000
64
+ timeout: options.timeout || 3000,
65
+ // On Windows, an npm/PowerShell command shim can otherwise create a visible
66
+ // console every time we probe OMP or resolve its configuration directory.
67
+ windowsHide: true
65
68
  });
66
69
  }
67
70
 
@@ -76,8 +79,8 @@ function readOmpAgentDirFromCommand(command, env = process.env, options = {}) {
76
79
  }
77
80
 
78
81
  function getOmpAgentDir(env = process.env, options = {}) {
79
- const runtime = options.runtime || resolveOmpRuntime(env, options);
80
- if (runtime.runtime === 'omp' && runtime.installed) {
82
+ const runtime = options.runtime || (options.resolveRuntime === false ? null : resolveOmpRuntime(env, options));
83
+ if (runtime?.runtime === 'omp' && runtime.installed) {
81
84
  const commandAgentDir = readOmpAgentDirFromCommand(runtime.command, env, options);
82
85
  if (commandAgentDir) {
83
86
  return commandAgentDir;
@@ -87,7 +90,7 @@ function getOmpAgentDir(env = process.env, options = {}) {
87
90
  }
88
91
 
89
92
  function getOmpPaths(env = process.env, options = {}) {
90
- const runtime = options.runtime || resolveOmpRuntime(env, options);
93
+ const runtime = options.runtime || (options.resolveRuntime === false ? null : resolveOmpRuntime(env, options));
91
94
  const agentDir = getOmpAgentDir(env, { ...options, runtime });
92
95
  return {
93
96
  agentDir,
@@ -1,11 +1,12 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execFileSync } = require('child_process');
4
3
  const { PATHS, HOME_DIR } = require('../../config/paths');
5
- const { getOmpCommand, getOmpPaths, isOmpInstalled, resolveOmpRuntime } = require('./omp-config');
4
+ const { getOmpCommand, getOmpPaths, resolveOmpRuntime } = require('./omp-config');
6
5
 
7
6
  const PROJECT_ORDER_FILE = PATHS.ompProjectOrder;
8
7
  const SESSION_ORDER_FILE = PATHS.ompSessionOrder;
8
+ const OMP_INSTALL_CACHE_TTL_MS = 5 * 60 * 1000;
9
+ let ompInstallCache = { expiresAt: 0, value: false };
9
10
 
10
11
  function ensureDir(dirPath) {
11
12
  if (!fs.existsSync(dirPath)) {
@@ -211,7 +212,20 @@ function parseSessionFile(filePath) {
211
212
  };
212
213
  }
213
214
 
214
- function scanSessionFiles(rootDir = getOmpPaths().sessions) {
215
+ function getOmpSessionPaths() {
216
+ // Reading historical sessions normally only needs OMP's native directory
217
+ // convention. Avoid starting the CLI here: project/count snapshot workers
218
+ // invoke this path independently, which made Windows repeatedly open
219
+ // PowerShell shims. Keep the CLI lookup as a fallback for non-standard OMP
220
+ // locations that cannot be derived from the environment.
221
+ const nativePaths = getOmpPaths(process.env, { resolveRuntime: false });
222
+ if (fs.existsSync(nativePaths.agentDir) || fs.existsSync(nativePaths.sessions)) {
223
+ return nativePaths;
224
+ }
225
+ return getOmpPaths();
226
+ }
227
+
228
+ function scanSessionFiles(rootDir = getOmpSessionPaths().sessions) {
215
229
  if (!fs.existsSync(rootDir)) {
216
230
  return [];
217
231
  }
@@ -259,7 +273,7 @@ function loadSessionOrder() {
259
273
  return safeReadJson(SESSION_ORDER_FILE, {});
260
274
  }
261
275
 
262
- function getProjects() {
276
+ function getProjects(_options = {}) {
263
277
  const sessions = getAllSessions();
264
278
  const projectMap = new Map();
265
279
  sessions.forEach((session) => {
@@ -299,7 +313,7 @@ function getProjects() {
299
313
  return projects;
300
314
  }
301
315
 
302
- function getSessionsByProject(projectName) {
316
+ function getSessionsByProject(projectName, _options = {}) {
303
317
  const orderMap = loadSessionOrder();
304
318
  const order = Array.isArray(orderMap[projectName]) ? orderMap[projectName] : [];
305
319
  const sessions = getAllSessions().filter(session => session.projectName === projectName);
@@ -425,7 +439,7 @@ function saveSessionOrder(projectName, order) {
425
439
  safeWriteJson(SESSION_ORDER_FILE, data);
426
440
  }
427
441
 
428
- function getProjectAndSessionCounts() {
442
+ function getProjectAndSessionCounts(_options = {}) {
429
443
  const sessions = getAllSessions();
430
444
  return {
431
445
  projectCount: new Set(sessions.map(session => session.projectName)).size,
@@ -454,15 +468,25 @@ function shellQuote(value) {
454
468
  }
455
469
 
456
470
  function isOmpCliInstalled() {
457
- if (isOmpInstalled()) return true;
458
- const runtime = resolveOmpRuntime();
459
- if (runtime.installed) return true;
460
- try {
461
- execFileSync(getOmpCommand(), ['--version'], { stdio: 'ignore', timeout: 3000 });
462
- return true;
463
- } catch {
464
- return false;
471
+ const now = Date.now();
472
+ if (ompInstallCache.expiresAt > now) {
473
+ return ompInstallCache.value;
465
474
  }
475
+
476
+ let installed = false;
477
+ const nativePaths = getOmpSessionPaths();
478
+ if (fs.existsSync(nativePaths.agentDir)) {
479
+ installed = true;
480
+ } else {
481
+ const runtime = resolveOmpRuntime();
482
+ installed = Boolean(runtime && runtime.installed);
483
+ }
484
+
485
+ ompInstallCache = {
486
+ value: installed,
487
+ expiresAt: now + OMP_INSTALL_CACHE_TTL_MS
488
+ };
489
+ return installed;
466
490
  }
467
491
 
468
492
  module.exports = {
@@ -493,7 +493,7 @@ function getProjectDisplayName(project) {
493
493
  }
494
494
 
495
495
  // 获取所有项目
496
- function getProjects() {
496
+ function getProjects(_options = {}) {
497
497
  const projects = getProjectRows().map((project) => ({
498
498
  name: project.id,
499
499
  displayName: getProjectDisplayName(project),
@@ -512,7 +512,7 @@ function getProjects() {
512
512
  }
513
513
 
514
514
  // 根据项目ID获取会话列表
515
- function getSessionsByProjectId(projectId) {
515
+ function getSessionsByProjectId(projectId, _options = {}) {
516
516
  const sessions = getSessionRowsByProjectId(projectId).map(session => normalizeSession(session, projectId));
517
517
  const order = getSessionOrder(projectId);
518
518
 
@@ -536,8 +536,8 @@ function getSessionsByProjectId(projectId) {
536
536
  }
537
537
 
538
538
  // 根据项目名获取会话列表
539
- function getSessionsByProject(projectName) {
540
- return getSessionsByProjectId(projectName);
539
+ function getSessionsByProject(projectName, options = {}) {
540
+ return getSessionsByProjectId(projectName, options);
541
541
  }
542
542
 
543
543
  function getSessionLocation(sessionId) {
@@ -628,9 +628,12 @@ function getSessionMessages(sessionId) {
628
628
  }
629
629
 
630
630
  // 获取项目和会话数量统计
631
- function getProjectAndSessionCounts() {
631
+ function getProjectAndSessionCounts(options = {}) {
632
632
  const now = Date.now();
633
- if (countsCache.expiresAt > now) {
633
+ if (options.force) {
634
+ countsCache.expiresAt = 0;
635
+ }
636
+ if (!options.force && countsCache.expiresAt > now) {
634
637
  return countsCache.value;
635
638
  }
636
639
 
@@ -833,7 +833,8 @@ class PluginsService {
833
833
  cwd: process.cwd(),
834
834
  encoding: 'utf8',
835
835
  stdio: 'pipe',
836
- timeout: 120000
836
+ timeout: 120000,
837
+ windowsHide: true
837
838
  });
838
839
  }
839
840
 
@@ -6,6 +6,8 @@ const {
6
6
  } = require('./snapshot-cache');
7
7
 
8
8
  const PROJECT_SNAPSHOT_TTL_MS = 60 * 1000;
9
+ const PROJECT_SNAPSHOT_WAIT_ON_MISS_MS = process.env.NODE_ENV === 'test' ? 0 : 1500;
10
+ const PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS = process.env.NODE_ENV === 'test' ? 0 : 1500;
9
11
 
10
12
  function projectListKey(source) {
11
13
  return `projects:list:${source}`;
@@ -33,7 +35,10 @@ async function getProjectListSnapshot(source, {
33
35
  fallbackValue,
34
36
  refresh,
35
37
  force,
36
- backgroundOnMiss: !force
38
+ backgroundOnMiss: !force,
39
+ staleWhileForce: true,
40
+ waitOnMissMs: PROJECT_SNAPSHOT_WAIT_ON_MISS_MS,
41
+ waitOnForceMs: PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS
37
42
  });
38
43
  }
39
44
 
@@ -44,6 +49,8 @@ function invalidateProjectSnapshots(source) {
44
49
 
45
50
  module.exports = {
46
51
  PROJECT_SNAPSHOT_TTL_MS,
52
+ PROJECT_SNAPSHOT_WAIT_ON_MISS_MS,
53
+ PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS,
47
54
  projectListKey,
48
55
  projectCountKey,
49
56
  emptyProjectList,
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ function totalSizeOf(sessions) {
6
+ return sessions.reduce((sum, session) => sum + (Number(session.size) || 0), 0);
7
+ }
8
+
9
+ function getAliases() {
10
+ const { loadAliases } = require('./alias');
11
+ return loadAliases();
12
+ }
13
+
14
+ async function buildClaudePayload(projectName, config, options = {}) {
15
+ const {
16
+ getSessionsForProject,
17
+ parseRealProjectPath
18
+ } = require('./sessions');
19
+
20
+ const result = await getSessionsForProject(config, projectName, { force: options.force === true });
21
+ const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
22
+
23
+ return {
24
+ sessions: result.sessions,
25
+ totalSize: result.totalSize,
26
+ aliases: getAliases(),
27
+ projectInfo: {
28
+ name: projectName,
29
+ displayName,
30
+ fullPath
31
+ }
32
+ };
33
+ }
34
+
35
+ function buildCodexPayload(projectName, options = {}) {
36
+ const { getSessionsByProject } = require('./codex-sessions');
37
+ const sessions = getSessionsByProject(projectName, { force: options.force === true });
38
+
39
+ return {
40
+ sessions,
41
+ totalSize: totalSizeOf(sessions),
42
+ aliases: getAliases(),
43
+ projectInfo: {
44
+ name: projectName,
45
+ fullPath: projectName,
46
+ path: projectName,
47
+ displayName: projectName
48
+ }
49
+ };
50
+ }
51
+
52
+ function buildGeminiPayload(projectHash, options = {}) {
53
+ const { getProjectSessions, getProjectPath } = require('./gemini-sessions');
54
+ const sessions = getProjectSessions(projectHash, { force: options.force === true });
55
+ const realPath = getProjectPath(projectHash, { force: options.force === true });
56
+ const displayName = realPath ? path.basename(realPath) : `Project ${projectHash.substring(0, 8)}`;
57
+
58
+ return {
59
+ sessions,
60
+ totalSize: totalSizeOf(sessions),
61
+ aliases: getAliases(),
62
+ projectInfo: {
63
+ name: projectHash,
64
+ fullPath: realPath || projectHash,
65
+ path: realPath || projectHash,
66
+ displayName
67
+ }
68
+ };
69
+ }
70
+
71
+ function buildOpenCodePayload(projectName, options = {}) {
72
+ const { getSessionsByProject, getProjects } = require('./opencode-sessions');
73
+ const sessions = getSessionsByProject(projectName, { force: options.force === true });
74
+ const firstDirectory = sessions.find(session => session.directory)?.directory;
75
+ const project = firstDirectory ? null : getProjects({ force: options.force === true }).find(p => p.name === projectName);
76
+ const fullPath = firstDirectory || project?.fullPath || projectName;
77
+
78
+ return {
79
+ sessions,
80
+ totalSize: totalSizeOf(sessions),
81
+ aliases: getAliases(),
82
+ projectInfo: {
83
+ name: projectName,
84
+ fullPath,
85
+ path: project?.path || fullPath,
86
+ displayName: project?.displayName || (fullPath ? path.basename(fullPath) : projectName)
87
+ }
88
+ };
89
+ }
90
+
91
+ function buildOmpPayload(projectName, options = {}) {
92
+ const { getProjects, getSessionsByProject } = require('./omp-sessions');
93
+ const sessions = getSessionsByProject(projectName, { force: options.force === true });
94
+ const firstDirectory = sessions.find(session => session.directory)?.directory;
95
+ let project = null;
96
+ try {
97
+ project = getProjects({ force: options.force === true }).find(p => p.name === projectName) || null;
98
+ } catch {
99
+ project = null;
100
+ }
101
+ const fullPath = project?.fullPath || project?.path || firstDirectory || projectName;
102
+
103
+ return {
104
+ sessions,
105
+ totalSize: totalSizeOf(sessions),
106
+ aliases: getAliases(),
107
+ projectInfo: {
108
+ name: projectName,
109
+ fullPath,
110
+ path: project?.path || fullPath,
111
+ displayName: project?.displayName || (fullPath ? path.basename(fullPath) : projectName)
112
+ }
113
+ };
114
+ }
115
+
116
+ async function buildPayload({ source, projectName, config, options }) {
117
+ const snapshotOptions = options || {};
118
+ switch (source) {
119
+ case 'claude':
120
+ return buildClaudePayload(projectName, config || {}, snapshotOptions);
121
+ case 'codex':
122
+ return buildCodexPayload(projectName, snapshotOptions);
123
+ case 'gemini':
124
+ return buildGeminiPayload(projectName, snapshotOptions);
125
+ case 'opencode':
126
+ return buildOpenCodePayload(projectName, snapshotOptions);
127
+ case 'omp':
128
+ return buildOmpPayload(projectName, snapshotOptions);
129
+ default:
130
+ throw new Error(`Unsupported session snapshot source: ${source}`);
131
+ }
132
+ }
133
+
134
+ function attachWorkerHandler() {
135
+ process.on('message', async (message) => {
136
+ try {
137
+ const value = await buildPayload(message || {});
138
+ if (process.send) {
139
+ process.send({ ok: true, value }, () => process.exit(0));
140
+ return;
141
+ }
142
+ } catch (error) {
143
+ if (process.send) {
144
+ process.send({ ok: false, error: error?.message || String(error) }, () => process.exit(1));
145
+ return;
146
+ }
147
+ }
148
+ process.exit(0);
149
+ });
150
+ }
151
+
152
+ if (process.env.CC_TOOL_SESSION_SNAPSHOT_WORKER === '1' || require.main === module) {
153
+ attachWorkerHandler();
154
+ }
155
+
156
+ module.exports = {
157
+ buildPayload
158
+ };