coding-tool-x 3.7.6 → 3.7.8

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 (44) hide show
  1. package/dist/web/assets/{Analytics-wCRLv26m.js → Analytics-BE5H9JGG.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-aa1blEHh.js → ConfigTemplates-z118k1BE.js} +1 -1
  3. package/dist/web/assets/{Home-eIxO50-k.js → Home-CBhtmOIN.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BAwGFNjN.js → PluginManager-CGhfZ3t8.js} +1 -1
  5. package/dist/web/assets/ProjectList-BMPLJtgQ.js +1 -0
  6. package/dist/web/assets/ProjectList-VeMNXfgO.css +1 -0
  7. package/dist/web/assets/SessionList-C4chCPxK.css +1 -0
  8. package/dist/web/assets/SessionList-D9e7wGF1.js +1 -0
  9. package/dist/web/assets/{SkillManager-Bhzs1O5S.js → SkillManager-DJpdUmnk.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-CLdX-pTT.js → WorkspaceManager-C0Ud_V7v.js} +1 -1
  11. package/dist/web/assets/index-CeTaG4IX.js +2 -0
  12. package/dist/web/assets/index-S8wrU2Nf.css +1 -0
  13. package/dist/web/index.html +2 -2
  14. package/package.json +1 -1
  15. package/src/commands/daemon.js +3 -1
  16. package/src/commands/port-config.js +0 -2
  17. package/src/commands/switch.js +1 -5
  18. package/src/commands/ui.js +4 -3
  19. package/src/commands/workspace.js +35 -3
  20. package/src/config/loader.js +76 -5
  21. package/src/index.js +11 -0
  22. package/src/server/api/config.js +0 -3
  23. package/src/server/api/omp-channels.js +29 -9
  24. package/src/server/api/omp-config.js +6 -8
  25. package/src/server/api/settings.js +0 -2
  26. package/src/server/api/workspaces.js +39 -1
  27. package/src/server/services/dashboard-snapshot-worker.js +1 -0
  28. package/src/server/services/https-cert.js +3 -2
  29. package/src/server/services/model-detector.js +106 -26
  30. package/src/server/services/omp-auth-providers.js +1 -0
  31. package/src/server/services/omp-config.js +22 -0
  32. package/src/server/services/omp-settings-manager.js +31 -16
  33. package/src/server/services/session-snapshots.js +1 -0
  34. package/src/server/services/sessions.js +35 -19
  35. package/src/server/services/web-build.js +2 -1
  36. package/src/server/services/workspace-service.js +204 -98
  37. package/src/utils/cli-flags.js +7 -0
  38. package/src/utils/session.js +10 -4
  39. package/dist/web/assets/ProjectList-CRiAEixW.js +0 -1
  40. package/dist/web/assets/ProjectList-Dw2omgOH.css +0 -1
  41. package/dist/web/assets/SessionList-BQ0FW6Gd.js +0 -1
  42. package/dist/web/assets/SessionList-kYJ6-Ies.css +0 -1
  43. package/dist/web/assets/index-B_nFqixU.css +0 -1
  44. package/dist/web/assets/index-ClMNA9Hf.js +0 -2
@@ -233,6 +233,9 @@ const MODEL_ALIASES = {
233
233
  };
234
234
 
235
235
  const TEST_TIMEOUT_MS = 10000; // 10 seconds per model test
236
+ const MODEL_LIST_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
237
+ const MODEL_LIST_FAILURE_TTL_MS = 5 * 60 * 1000;
238
+ const MODEL_LIST_AUTH_FAILURE_TTL_MS = 60 * 60 * 1000;
236
239
  const CLAUDE_CODE_BETA_HEADER = 'claude-code-20250219,interleaved-thinking-2025-05-14';
237
240
 
238
241
  const MODEL_UNAVAILABLE_HINTS = [
@@ -797,6 +800,84 @@ function isSignatureCacheValid(cacheEntry, signatureKey, expectedSignature) {
797
800
  return cacheEntry[signatureKey] === expectedSignature;
798
801
  }
799
802
 
803
+ function getCacheAgeMs(timestamp, now = Date.now()) {
804
+ const checkedAt = Date.parse(timestamp || '');
805
+ return Number.isFinite(checkedAt) ? Math.max(0, now - checkedAt) : Infinity;
806
+ }
807
+
808
+ function isCacheFresh(timestamp, ttlMs, now = Date.now()) {
809
+ return getCacheAgeMs(timestamp, now) < ttlMs;
810
+ }
811
+
812
+ function getModelListFailureTtl(statusCode) {
813
+ return [401, 403, 404, 429].includes(Number(statusCode))
814
+ ? MODEL_LIST_AUTH_FAILURE_TTL_MS
815
+ : MODEL_LIST_FAILURE_TTL_MS;
816
+ }
817
+
818
+ function buildCachedModelListResult(entry, listSignature, forceRefresh) {
819
+ if (forceRefresh || !isSignatureCacheValid(entry, 'listSignature', listSignature)) {
820
+ return null;
821
+ }
822
+
823
+ const failure = entry.modelListFailure;
824
+ if (failure && Date.parse(failure.retryAfter || '') > Date.now()) {
825
+ return {
826
+ models: Array.isArray(entry.fetchedModels) ? entry.fetchedModels : [],
827
+ supported: true,
828
+ cached: true,
829
+ stale: true,
830
+ backoff: true,
831
+ fallbackUsed: false,
832
+ error: failure.error || '模型列表暂时不可用',
833
+ errorHint: failure.errorHint || '请稍后重试,或手动填写模型名称',
834
+ statusCode: failure.statusCode,
835
+ lastChecked: entry.lastChecked || failure.failedAt || null,
836
+ retryAfter: failure.retryAfter
837
+ };
838
+ }
839
+
840
+ if (!Array.isArray(entry.fetchedModels)) {
841
+ return null;
842
+ }
843
+
844
+ const stale = !isCacheFresh(entry.lastChecked, MODEL_LIST_CACHE_TTL_MS);
845
+ const result = {
846
+ models: entry.fetchedModels,
847
+ supported: true,
848
+ cached: true,
849
+ fallbackUsed: false,
850
+ error: null,
851
+ lastChecked: entry.lastChecked || null
852
+ };
853
+ if (stale) {
854
+ result.stale = true;
855
+ result.errorHint = '模型目录缓存已过期;可点击“刷新可选模型”更新。';
856
+ }
857
+ return result;
858
+ }
859
+
860
+ function saveModelListFailure(cache, cacheKey, listSignature, result = {}) {
861
+ const now = new Date();
862
+ const ttlMs = getModelListFailureTtl(result.statusCode);
863
+ const previous = cache[cacheKey] || {};
864
+ const failure = {
865
+ error: result.error || '模型列表暂时不可用',
866
+ errorHint: result.errorHint || '请稍后重试,或手动填写模型名称',
867
+ statusCode: result.statusCode || null,
868
+ failedAt: now.toISOString(),
869
+ retryAfter: new Date(now.getTime() + ttlMs).toISOString()
870
+ };
871
+ cache[cacheKey] = {
872
+ ...previous,
873
+ fetchedModels: Array.isArray(previous.fetchedModels) ? previous.fetchedModels : [],
874
+ listSignature,
875
+ modelListFailure: failure
876
+ };
877
+ saveModelCache(cache);
878
+ return failure;
879
+ }
880
+
800
881
  /**
801
882
  * Test if a specific model is available for a channel
802
883
  * @param {Object} channel - Channel configuration
@@ -869,8 +950,10 @@ async function probeModelAvailability(channel, channelType, options = {}) {
869
950
  });
870
951
  const cacheKey = getChannelCacheKey(channel, probeSignature);
871
952
 
872
- // Return cached result if channel and probe options are unchanged
873
- if (!forceRefresh && isSignatureCacheValid(cache[cacheKey], 'probeSignature', probeSignature)) {
953
+ // Return cached result only while the matching probe result is still fresh.
954
+ if (!forceRefresh
955
+ && isSignatureCacheValid(cache[cacheKey], 'probeSignature', probeSignature)
956
+ && isCacheFresh(cache[cacheKey].lastChecked, MODEL_LIST_CACHE_TTL_MS)) {
874
957
  return {
875
958
  availableModels: cache[cacheKey].availableModels || [],
876
959
  preferredTestModel: cache[cacheKey].preferredTestModel || null,
@@ -1026,21 +1109,17 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1026
1109
  });
1027
1110
  const cacheKey = getChannelCacheKey(channel, listSignature);
1028
1111
 
1029
- // Check cache first, and only reuse when channel/list context is unchanged
1030
- if (!forceRefresh
1031
- && isSignatureCacheValid(cache[cacheKey], 'listSignature', listSignature)
1032
- && Array.isArray(cache[cacheKey].fetchedModels)) {
1033
- return {
1034
- models: cache[cacheKey].fetchedModels || [],
1035
- supported: true,
1036
- cached: true,
1037
- fallbackUsed: false,
1038
- error: null,
1039
- lastChecked: cache[cacheKey].lastChecked
1040
- };
1041
- }
1112
+ // Reuse a fresh catalog immediately. Expired catalogs are also returned as
1113
+ // stale data so opening the editor never blocks on the provider; the user
1114
+ // can explicitly request a refresh.
1115
+ const cachedResult = buildCachedModelListResult(cache[cacheKey], listSignature, forceRefresh);
1116
+ if (cachedResult) return cachedResult;
1042
1117
 
1043
1118
  return new Promise((resolve) => {
1119
+ const resolveFailure = (result) => {
1120
+ saveModelListFailure(cache, cacheKey, listSignature, result);
1121
+ resolve(result);
1122
+ };
1044
1123
  try {
1045
1124
  const baseUrl = channel.baseUrl.trim().replace(/\/+$/, '');
1046
1125
  const endpoint = capability.modelListEndpoint; // e.g. '/v1/models'
@@ -1095,7 +1174,8 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1095
1174
  availableModels: cache[cacheKey]?.availableModels || [],
1096
1175
  preferredTestModel: cache[cacheKey]?.preferredTestModel || null,
1097
1176
  probeSignature: cache[cacheKey]?.probeSignature || null,
1098
- listSignature
1177
+ listSignature,
1178
+ modelListFailure: null
1099
1179
  };
1100
1180
 
1101
1181
  cache[cacheKey] = cacheEntry;
@@ -1113,7 +1193,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1113
1193
  });
1114
1194
  } catch (parseError) {
1115
1195
  console.error(`[ModelDetector] Failed to parse models response: ${parseError.message}`);
1116
- resolve({
1196
+ resolveFailure({
1117
1197
  models: [],
1118
1198
  supported: true,
1119
1199
  cached: false,
@@ -1133,7 +1213,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1133
1213
  errorMessage = 'Cloudflare 防护拦截,无法自动获取模型列表';
1134
1214
  errorHint = '该 API 端点受 Cloudflare 保护,请手动填写模型名称';
1135
1215
  console.warn(`[ModelDetector] Cloudflare protection detected for ${channel.name}, no fallback models injected`);
1136
- resolve({
1216
+ resolveFailure({
1137
1217
  models: [],
1138
1218
  supported: false,
1139
1219
  cached: false,
@@ -1146,7 +1226,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1146
1226
  errorMessage = 'API 密钥认证失败';
1147
1227
  errorHint = '请检查 API 密钥是否正确配置';
1148
1228
  console.error(`[ModelDetector] Authentication failed for ${channel.name}: ${res.statusCode} - ${errorMessage}`);
1149
- resolve({
1229
+ resolveFailure({
1150
1230
  models: [],
1151
1231
  supported: true,
1152
1232
  cached: false,
@@ -1159,7 +1239,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1159
1239
  errorMessage = '访问被拒绝';
1160
1240
  errorHint = '请检查 API 密钥权限或联系服务提供商';
1161
1241
  console.error(`[ModelDetector] Access denied for ${channel.name}: ${res.statusCode} - ${errorMessage}`);
1162
- resolve({
1242
+ resolveFailure({
1163
1243
  models: [],
1164
1244
  supported: true,
1165
1245
  cached: false,
@@ -1171,7 +1251,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1171
1251
  }
1172
1252
  } else if (res.statusCode === 404) {
1173
1253
  console.warn(`[ModelDetector] Model list endpoint not found for ${channel.name}`);
1174
- resolve({
1254
+ resolveFailure({
1175
1255
  models: [],
1176
1256
  supported: false,
1177
1257
  cached: false,
@@ -1182,7 +1262,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1182
1262
  });
1183
1263
  } else if (res.statusCode === 429) {
1184
1264
  console.warn(`[ModelDetector] Rate limited for ${channel.name}`);
1185
- resolve({
1265
+ resolveFailure({
1186
1266
  models: [],
1187
1267
  supported: true,
1188
1268
  cached: false,
@@ -1206,7 +1286,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1206
1286
  })
1207
1287
  .catch((error) => {
1208
1288
  console.error(`[ModelDetector] Failed to read models response: ${error.message}`);
1209
- resolve({
1289
+ resolveFailure({
1210
1290
  models: [],
1211
1291
  supported: true,
1212
1292
  cached: false,
@@ -1218,7 +1298,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1218
1298
 
1219
1299
  req.on('error', (error) => {
1220
1300
  console.error(`[ModelDetector] Network error fetching models from ${channel.name}: ${error.message}`);
1221
- resolve({
1301
+ resolveFailure({
1222
1302
  models: [],
1223
1303
  supported: true,
1224
1304
  cached: false,
@@ -1230,7 +1310,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1230
1310
  req.on('timeout', () => {
1231
1311
  req.destroy();
1232
1312
  console.error(`[ModelDetector] Timeout fetching models from ${channel.name}`);
1233
- resolve({
1313
+ resolveFailure({
1234
1314
  models: [],
1235
1315
  supported: true,
1236
1316
  cached: false,
@@ -1243,7 +1323,7 @@ async function fetchModelsFromProvider(channel, channelType, options = {}) {
1243
1323
 
1244
1324
  } catch (error) {
1245
1325
  console.error(`[ModelDetector] Error in fetchModelsFromProvider: ${error.message}`);
1246
- resolve({
1326
+ resolveFailure({
1247
1327
  models: [],
1248
1328
  supported: true,
1249
1329
  cached: false,
@@ -351,6 +351,7 @@ function warmOmpAuthProviderSnapshot(options = {}) {
351
351
  try {
352
352
  const child = fork(workerPath, [], {
353
353
  stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
354
+ windowsHide: true,
354
355
  env: {
355
356
  ...process.env,
356
357
  CC_TOOL_OMP_AUTH_PROVIDER_WORKER: '1'
@@ -229,6 +229,28 @@ function readTextFile(filePath, fallback = '') {
229
229
  }
230
230
 
231
231
  function getOmpStatus(env = process.env, options = {}) {
232
+ if (options.resolveRuntime === false) {
233
+ const paths = getOmpPaths(env, options);
234
+ return {
235
+ installed: fs.existsSync(paths.agentDir),
236
+ runtime: 'omp',
237
+ command: getOmpCommand(env),
238
+ commandSource: 'not-probed',
239
+ agentDir: paths.agentDir,
240
+ settingsPath: paths.settings,
241
+ authPath: paths.auth,
242
+ modelsPath: paths.modelsYml,
243
+ modelsYmlPath: paths.modelsYml,
244
+ modelsJsonLegacyPath: paths.modelsJsonLegacy,
245
+ sessionsDir: paths.sessions,
246
+ skillsDir: paths.skills,
247
+ promptsDir: paths.prompts,
248
+ commandsDir: paths.commands,
249
+ extensionsDir: paths.extensions,
250
+ themesDir: paths.themes,
251
+ packagesDir: paths.packages
252
+ };
253
+ }
232
254
  const runtime = resolveOmpRuntime(env, options);
233
255
  const paths = getOmpPaths(env, { ...options, runtime });
234
256
  return {
@@ -20,8 +20,11 @@ const THINKING_MODES = new Set([
20
20
 
21
21
  let lastManagedOmpSyncResult = null;
22
22
 
23
- function getOmpPaths(...args) {
24
- return ompConfig.getOmpPaths(...args);
23
+ function getOmpPaths(env = process.env, options = {}) {
24
+ // Channel CRUD only needs the native OMP file layout. Do not start OMP just
25
+ // to discover a path: OMP_CONFIG_DIR, OMP_PROFILE and OMP_CODING_AGENT_DIR
26
+ // are sufficient to derive it, and a blocked CLI must not block a save.
27
+ return ompConfig.getOmpPaths(env, { ...options, resolveRuntime: false });
25
28
  }
26
29
 
27
30
  function ensureOmpDir(...args) {
@@ -257,7 +260,8 @@ function readOmpCatalogModels(command, providerId, options = {}) {
257
260
  ...process.env,
258
261
  ...(options.env || {})
259
262
  },
260
- timeout: options.catalogTimeout || 5000
263
+ timeout: options.catalogTimeout || 5000,
264
+ windowsHide: true
261
265
  });
262
266
  const status = result?.status === undefined || result?.status === null ? 0 : result.status;
263
267
  if (result?.error || status !== 0) return [];
@@ -270,6 +274,9 @@ function readOmpCatalogModels(command, providerId, options = {}) {
270
274
  }
271
275
 
272
276
  function buildCatalogIndex(channel = {}, options = {}) {
277
+ if (options.catalogFromCli !== true) {
278
+ return new Map();
279
+ }
273
280
  const runtime = options.runtime
274
281
  || (typeof ompConfig.resolveOmpRuntime === 'function'
275
282
  ? ompConfig.resolveOmpRuntime(options.env || process.env, options.runtimeOptions || {})
@@ -499,7 +506,8 @@ function runModelsJson(command, env, options = {}) {
499
506
  ...process.env,
500
507
  ...env
501
508
  },
502
- timeout: options.timeout || 5000
509
+ timeout: options.timeout || 5000,
510
+ windowsHide: true
503
511
  });
504
512
  if (typeof result === 'string' || Buffer.isBuffer(result)) {
505
513
  return { status: 0, stdout: result, stderr: '' };
@@ -515,7 +523,8 @@ function collectVisibleProviderIds(command, env, options = {}) {
515
523
  ...process.env,
516
524
  ...env
517
525
  },
518
- timeout: options.visibilityTimeout || options.timeout || 5000
526
+ timeout: options.visibilityTimeout || options.timeout || 5000,
527
+ windowsHide: true
519
528
  });
520
529
  const status = result?.status === undefined || result?.status === null ? 0 : result.status;
521
530
  if (result?.error || status !== 0) return [];
@@ -658,15 +667,17 @@ function collectManagedVisibility(channels = [], modelsConfig = { providers: {}
658
667
  }
659
668
  });
660
669
 
661
- const env = options.env || process.env;
662
- const runtime = options.runtime
663
- || (typeof ompConfig.resolveOmpRuntime === 'function'
664
- ? ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {})
665
- : null);
666
- if (runtime && runtime.runtime === 'omp' && runtime.installed && options.discoverDisabledProviders !== false) {
667
- collectVisibleProviderIds(runtime.command, env, options)
668
- .filter(providerId => providerId && !isManagedProviderId(providerId))
669
- .forEach(providerId => pushUnique(managedDisabledProviders, providerId));
670
+ if (options.discoverDisabledProviders === true) {
671
+ const env = options.env || process.env;
672
+ const runtime = options.runtime
673
+ || (typeof ompConfig.resolveOmpRuntime === 'function'
674
+ ? ompConfig.resolveOmpRuntime(env, options.runtimeOptions || {})
675
+ : null);
676
+ if (runtime && runtime.runtime === 'omp' && runtime.installed) {
677
+ collectVisibleProviderIds(runtime.command, env, options)
678
+ .filter(providerId => providerId && !isManagedProviderId(providerId))
679
+ .forEach(providerId => pushUnique(managedDisabledProviders, providerId));
680
+ }
670
681
  }
671
682
 
672
683
  return {
@@ -874,7 +885,9 @@ function writeManagedOmpProviders(channels = [], options = {}) {
874
885
  const backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
875
886
  writeModelsConfig(config, paths.modelsYml);
876
887
  const visibility = syncManagedOmpVisibility(channels, config, options);
877
- const validation = validateOmpModelsConfig(options);
888
+ const validation = options.validateWithCli === true
889
+ ? validateOmpModelsConfig(options)
890
+ : { skipped: true, reason: 'cli-validation-disabled', warnings: [] };
878
891
  const warnings = [
879
892
  ...(visibility.warnings || []),
880
893
  ...(validation.warnings || [])
@@ -916,7 +929,9 @@ function removeManagedOmpProviders(options = {}) {
916
929
  if (before !== after) {
917
930
  backupPath = createModelsBackupIfNeeded(paths.modelsYml, options);
918
931
  writeModelsConfig(config, paths.modelsYml);
919
- validation = validateOmpModelsConfig(options);
932
+ validation = options.validateWithCli === true
933
+ ? validateOmpModelsConfig(options)
934
+ : { skipped: true, reason: 'cli-validation-disabled', warnings: [] };
920
935
  }
921
936
  recordManagedOmpSyncResult({
922
937
  path: paths.modelsYml,
@@ -46,6 +46,7 @@ function runSessionSnapshotWorker(source, projectName, config = {}, options = {}
46
46
  const workerPath = path.join(__dirname, 'session-snapshot-worker.js');
47
47
  const child = fork(workerPath, [], {
48
48
  stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
49
+ windowsHide: true,
49
50
  env: {
50
51
  ...process.env,
51
52
  CC_TOOL_SESSION_SNAPSHOT_WORKER: '1'
@@ -17,6 +17,17 @@ const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
17
17
  const CODEX_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
18
18
  const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
19
19
 
20
+ function resolveProjectsDir(config = {}) {
21
+ return config.projectsDir || CLAUDE_PROJECTS_DIR;
22
+ }
23
+
24
+ function withResolvedProjectsDir(config = {}) {
25
+ return {
26
+ ...config,
27
+ projectsDir: resolveProjectsDir(config)
28
+ };
29
+ }
30
+
20
31
  // Base directory for cc-tool data
21
32
  function getCcToolDir() {
22
33
  return PATHS.base;
@@ -182,7 +193,7 @@ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber)
182
193
 
183
194
  // Get all projects with stats (async version)
184
195
  async function getProjects(config) {
185
- const projectsDir = config.projectsDir;
196
+ const projectsDir = resolveProjectsDir(config);
186
197
 
187
198
  if (!fs.existsSync(projectsDir)) {
188
199
  return [];
@@ -383,16 +394,17 @@ function extractCwdFromSessionHeader(sessionFile) {
383
394
 
384
395
  // Get projects with detailed stats (with caching) - async version
385
396
  async function getProjectsWithStats(config, options = {}) {
397
+ const resolvedConfig = withResolvedProjectsDir(config);
386
398
  if (!options.force) {
387
399
  // Check enhanced cache first
388
- const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
400
+ const cacheKey = `${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`;
389
401
  const enhancedCached = globalCache.get(cacheKey);
390
402
  if (enhancedCached) {
391
403
  return enhancedCached;
392
404
  }
393
405
 
394
406
  // Check old cache
395
- const cached = getCachedProjects(config);
407
+ const cached = getCachedProjects(resolvedConfig);
396
408
  if (cached) {
397
409
  globalCache.set(cacheKey, cached, 300000); // 5分钟
398
410
  return cached;
@@ -400,22 +412,22 @@ async function getProjectsWithStats(config, options = {}) {
400
412
  }
401
413
 
402
414
  try {
403
- const data = await buildProjectsWithStats(config);
415
+ const data = await buildProjectsWithStats(resolvedConfig);
404
416
  if (!Array.isArray(data)) {
405
- console.warn(`[getProjectsWithStats] Unexpected non-array result for ${config.projectsDir}, returning empty array.`);
417
+ console.warn(`[getProjectsWithStats] Unexpected non-array result for ${resolvedConfig.projectsDir}, returning empty array.`);
406
418
  return [];
407
419
  }
408
- setCachedProjects(config, data);
409
- globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
420
+ setCachedProjects(resolvedConfig, data);
421
+ globalCache.set(`${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`, data, 300000);
410
422
  return data;
411
423
  } catch (err) {
412
- console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
424
+ console.error(`[getProjectsWithStats] Failed to build projects for ${resolvedConfig.projectsDir}:`, err);
413
425
  return [];
414
426
  }
415
427
  }
416
428
 
417
429
  async function buildProjectsWithStats(config) {
418
- const projectsDir = config.projectsDir;
430
+ const projectsDir = resolveProjectsDir(config);
419
431
 
420
432
  if (!fs.existsSync(projectsDir)) {
421
433
  return [];
@@ -482,7 +494,7 @@ async function buildProjectsWithStats(config) {
482
494
 
483
495
  // 获取 Claude 项目/会话数量(轻量统计)
484
496
  function getProjectAndSessionCounts(config) {
485
- const projectsDir = config.projectsDir;
497
+ const projectsDir = resolveProjectsDir(config);
486
498
  if (!fs.existsSync(projectsDir)) {
487
499
  return { projectCount: 0, sessionCount: 0 };
488
500
  }
@@ -672,7 +684,8 @@ async function getSessionsForProject(config, projectName, options = {}) {
672
684
 
673
685
  // Delete a session
674
686
  function deleteSession(config, projectName, sessionId) {
675
- const projectDir = path.join(config.projectsDir, projectName);
687
+ const resolvedConfig = withResolvedProjectsDir(config);
688
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
676
689
  const sessionFile = path.join(projectDir, sessionId + '.jsonl');
677
690
 
678
691
  if (!fs.existsSync(sessionFile)) {
@@ -680,14 +693,15 @@ function deleteSession(config, projectName, sessionId) {
680
693
  }
681
694
 
682
695
  fs.unlinkSync(sessionFile);
683
- invalidateProjectsCache(config);
696
+ invalidateProjectsCache(resolvedConfig);
684
697
  invalidateSessionResultCache(projectName);
685
698
  return { success: true };
686
699
  }
687
700
 
688
701
  // Fork a session
689
702
  function forkSession(config, projectName, sessionId, options = {}) {
690
- const projectDir = path.join(config.projectsDir, projectName);
703
+ const resolvedConfig = withResolvedProjectsDir(config);
704
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
691
705
  const sessionFile = path.join(projectDir, sessionId + '.jsonl');
692
706
 
693
707
  if (!fs.existsSync(sessionFile)) {
@@ -716,7 +730,7 @@ function forkSession(config, projectName, sessionId, options = {}) {
716
730
  const { setAlias } = require('./alias');
717
731
  setAlias(newSessionId, options.alias);
718
732
  }
719
- invalidateProjectsCache(config);
733
+ invalidateProjectsCache(resolvedConfig);
720
734
  invalidateSessionResultCache(projectName);
721
735
 
722
736
  return {
@@ -768,7 +782,8 @@ function saveSessionOrder(projectName, order) {
768
782
 
769
783
  // Delete a project (remove the entire project directory)
770
784
  function deleteProject(config, projectName) {
771
- const projectDir = path.join(config.projectsDir, projectName);
785
+ const resolvedConfig = withResolvedProjectsDir(config);
786
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
772
787
 
773
788
  if (!fs.existsSync(projectDir)) {
774
789
  throw new Error('Project not found');
@@ -778,19 +793,20 @@ function deleteProject(config, projectName) {
778
793
  fs.rmSync(projectDir, { recursive: true, force: true });
779
794
 
780
795
  // Remove from order file if exists
781
- const order = getProjectOrder(config);
796
+ const order = getProjectOrder(resolvedConfig);
782
797
  const newOrder = order.filter(name => name !== projectName);
783
798
  if (newOrder.length !== order.length) {
784
- saveProjectOrder(config, newOrder);
799
+ saveProjectOrder(resolvedConfig, newOrder);
785
800
  }
786
801
 
787
- invalidateProjectsCache(config);
802
+ invalidateProjectsCache(resolvedConfig);
788
803
  return { success: true };
789
804
  }
790
805
 
791
806
  // Search sessions for keyword
792
807
  function searchSessions(config, projectName, keyword, contextLength = 15) {
793
- const projectDir = path.join(config.projectsDir, projectName);
808
+ const resolvedConfig = withResolvedProjectsDir(config);
809
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
794
810
 
795
811
  if (!fs.existsSync(projectDir)) {
796
812
  return [];
@@ -101,7 +101,8 @@ function runWebBuild(options = {}) {
101
101
  const child = spawn(npmCommand, ['run', 'build'], {
102
102
  cwd: sourceRoot,
103
103
  env,
104
- stdio
104
+ stdio,
105
+ windowsHide: true
105
106
  });
106
107
 
107
108
  child.once('error', (error) => {