snow-ai 0.2.28 → 0.3.1

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.
@@ -48,6 +48,9 @@ export declare class ACECodeSearchService {
48
48
  private fileModTimes;
49
49
  private customExcludes;
50
50
  private excludesLoaded;
51
+ private regexCache;
52
+ private fileContentCache;
53
+ private readonly FILE_CONTENT_CACHE_SIZE;
51
54
  private readonly DEFAULT_EXCLUDES;
52
55
  constructor(basePath?: string);
53
56
  /**
@@ -62,6 +65,10 @@ export declare class ACECodeSearchService {
62
65
  * Detect programming language from file extension
63
66
  */
64
67
  private detectLanguage;
68
+ /**
69
+ * Read file with LRU cache to reduce repeated file system access
70
+ */
71
+ private readFileWithCache;
65
72
  /**
66
73
  * Parse file content to extract code symbols using regex patterns
67
74
  */
@@ -121,8 +121,8 @@ export class ACECodeSearchService {
121
121
  enumerable: true,
122
122
  configurable: true,
123
123
  writable: true,
124
- value: []
125
- });
124
+ value: new Set()
125
+ }); // 使用 Set 提高查找性能 O(1)
126
126
  Object.defineProperty(this, "fileModTimes", {
127
127
  enumerable: true,
128
128
  configurable: true,
@@ -141,6 +141,26 @@ export class ACECodeSearchService {
141
141
  writable: true,
142
142
  value: false
143
143
  }); // Track if exclusions have been loaded
144
+ // 预编译的正则表达式缓存
145
+ Object.defineProperty(this, "regexCache", {
146
+ enumerable: true,
147
+ configurable: true,
148
+ writable: true,
149
+ value: new Map()
150
+ });
151
+ // 文件内容缓存(用于减少重复读取)
152
+ Object.defineProperty(this, "fileContentCache", {
153
+ enumerable: true,
154
+ configurable: true,
155
+ writable: true,
156
+ value: new Map()
157
+ });
158
+ Object.defineProperty(this, "FILE_CONTENT_CACHE_SIZE", {
159
+ enumerable: true,
160
+ configurable: true,
161
+ writable: true,
162
+ value: 50
163
+ }); // 限制缓存大小
144
164
  // Default exclusion directories
145
165
  Object.defineProperty(this, "DEFAULT_EXCLUDES", {
146
166
  enumerable: true,
@@ -230,18 +250,24 @@ export class ACECodeSearchService {
230
250
  for (const pattern of this.customExcludes) {
231
251
  // Simple pattern matching: exact match or glob-style wildcards
232
252
  if (pattern.includes('*')) {
233
- // Convert simple glob to regex for matching
234
- const regexPattern = pattern
235
- .replace(/\./g, '\\.')
236
- .replace(/\*/g, '.*');
237
- const regex = new RegExp(`^${regexPattern}$`);
253
+ // 使用缓存的正则表达式,避免重复编译
254
+ let regex = this.regexCache.get(pattern);
255
+ if (!regex) {
256
+ const regexPattern = pattern
257
+ .replace(/\./g, '\\.')
258
+ .replace(/\*/g, '.*');
259
+ regex = new RegExp(`^${regexPattern}$`);
260
+ this.regexCache.set(pattern, regex);
261
+ }
238
262
  if (regex.test(relativePath) || regex.test(dirName)) {
239
263
  return true;
240
264
  }
241
265
  }
242
266
  else {
243
267
  // Exact match
244
- if (relativePath === pattern || dirName === pattern || relativePath.startsWith(pattern + '/')) {
268
+ if (relativePath === pattern ||
269
+ dirName === pattern ||
270
+ relativePath.startsWith(pattern + '/')) {
245
271
  return true;
246
272
  }
247
273
  }
@@ -260,6 +286,30 @@ export class ACECodeSearchService {
260
286
  }
261
287
  return null;
262
288
  }
289
+ /**
290
+ * Read file with LRU cache to reduce repeated file system access
291
+ */
292
+ async readFileWithCache(filePath) {
293
+ const stats = await fs.stat(filePath);
294
+ const mtime = stats.mtimeMs;
295
+ // Check cache
296
+ const cached = this.fileContentCache.get(filePath);
297
+ if (cached && cached.mtime === mtime) {
298
+ return cached.content;
299
+ }
300
+ // Read file
301
+ const content = await fs.readFile(filePath, 'utf-8');
302
+ // Manage cache size (simple LRU: remove oldest if over limit)
303
+ if (this.fileContentCache.size >= this.FILE_CONTENT_CACHE_SIZE) {
304
+ const firstKey = this.fileContentCache.keys().next().value;
305
+ if (firstKey) {
306
+ this.fileContentCache.delete(firstKey);
307
+ }
308
+ }
309
+ // Cache the content
310
+ this.fileContentCache.set(filePath, { content, mtime });
311
+ return content;
312
+ }
263
313
  /**
264
314
  * Parse file content to extract code symbols using regex patterns
265
315
  */
@@ -382,7 +432,11 @@ export class ACECodeSearchService {
382
432
  getContext(lines, lineIndex, contextSize) {
383
433
  const start = Math.max(0, lineIndex - contextSize);
384
434
  const end = Math.min(lines.length, lineIndex + contextSize + 1);
385
- return lines.slice(start, end).filter(l => l !== undefined).join('\n').trim();
435
+ return lines
436
+ .slice(start, end)
437
+ .filter(l => l !== undefined)
438
+ .join('\n')
439
+ .trim();
386
440
  }
387
441
  /**
388
442
  * Check if a directory is a Git repository
@@ -401,7 +455,7 @@ export class ACECodeSearchService {
401
455
  * Check if a command is available in the system PATH
402
456
  */
403
457
  isCommandAvailable(command) {
404
- return new Promise((resolve) => {
458
+ return new Promise(resolve => {
405
459
  try {
406
460
  let child;
407
461
  if (process.platform === 'win32') {
@@ -418,7 +472,7 @@ export class ACECodeSearchService {
418
472
  stdio: 'ignore',
419
473
  });
420
474
  }
421
- child.on('close', (code) => resolve(code === 0));
475
+ child.on('close', code => resolve(code === 0));
422
476
  child.on('error', () => resolve(false));
423
477
  }
424
478
  catch {
@@ -468,7 +522,9 @@ export class ACECodeSearchService {
468
522
  async buildIndex(forceRefresh = false) {
469
523
  const now = Date.now();
470
524
  // Use cache if available and not expired
471
- if (!forceRefresh && this.indexCache.size > 0 && (now - this.lastIndexTime) < this.INDEX_CACHE_DURATION) {
525
+ if (!forceRefresh &&
526
+ this.indexCache.size > 0 &&
527
+ now - this.lastIndexTime < this.INDEX_CACHE_DURATION) {
472
528
  return;
473
529
  }
474
530
  // Load exclusion patterns
@@ -477,7 +533,8 @@ export class ACECodeSearchService {
477
533
  if (forceRefresh) {
478
534
  this.indexCache.clear();
479
535
  this.fileModTimes.clear();
480
- this.allIndexedFiles = [];
536
+ this.allIndexedFiles.clear();
537
+ this.fileContentCache.clear();
481
538
  }
482
539
  const filesToProcess = [];
483
540
  const searchInDirectory = async (dirPath) => {
@@ -506,9 +563,7 @@ export class ACECodeSearchService {
506
563
  this.fileModTimes.set(fullPath, currentMtime);
507
564
  }
508
565
  // Track all indexed files (even if not modified)
509
- if (!this.allIndexedFiles.includes(fullPath)) {
510
- this.allIndexedFiles.push(fullPath);
511
- }
566
+ this.allIndexedFiles.add(fullPath);
512
567
  }
513
568
  catch (error) {
514
569
  // If we can't stat the file, skip it
@@ -522,24 +577,32 @@ export class ACECodeSearchService {
522
577
  }
523
578
  };
524
579
  await searchInDirectory(this.basePath);
525
- // Process only modified or new files
526
- for (const fullPath of filesToProcess) {
527
- try {
528
- const content = await fs.readFile(fullPath, 'utf-8');
529
- const symbols = await this.parseFileSymbols(fullPath, content);
530
- if (symbols.length > 0) {
531
- this.indexCache.set(fullPath, symbols);
580
+ // Process files in batches for better performance
581
+ const BATCH_SIZE = 10; // 并发处理批次大小
582
+ const batches = [];
583
+ for (let i = 0; i < filesToProcess.length; i += BATCH_SIZE) {
584
+ batches.push(filesToProcess.slice(i, i + BATCH_SIZE));
585
+ }
586
+ // Process batches concurrently
587
+ for (const batch of batches) {
588
+ await Promise.all(batch.map(async (fullPath) => {
589
+ try {
590
+ const content = await this.readFileWithCache(fullPath);
591
+ const symbols = await this.parseFileSymbols(fullPath, content);
592
+ if (symbols.length > 0) {
593
+ this.indexCache.set(fullPath, symbols);
594
+ }
595
+ else {
596
+ // Remove entry if no symbols found
597
+ this.indexCache.delete(fullPath);
598
+ }
532
599
  }
533
- else {
534
- // Remove entry if no symbols found
600
+ catch (error) {
601
+ // Remove from index if file cannot be read
535
602
  this.indexCache.delete(fullPath);
603
+ this.fileModTimes.delete(fullPath);
536
604
  }
537
- }
538
- catch (error) {
539
- // Remove from index if file cannot be read
540
- this.indexCache.delete(fullPath);
541
- this.fileModTimes.delete(fullPath);
542
- }
605
+ }));
543
606
  }
544
607
  // Clean up deleted files from cache
545
608
  for (const cachedPath of Array.from(this.indexCache.keys())) {
@@ -550,10 +613,7 @@ export class ACECodeSearchService {
550
613
  // File no longer exists, remove from cache
551
614
  this.indexCache.delete(cachedPath);
552
615
  this.fileModTimes.delete(cachedPath);
553
- const fileIndex = this.allIndexedFiles.indexOf(cachedPath);
554
- if (fileIndex !== -1) {
555
- this.allIndexedFiles.splice(fileIndex, 1);
556
- }
616
+ this.allIndexedFiles.delete(cachedPath);
557
617
  }
558
618
  }
559
619
  this.lastIndexTime = now;
@@ -654,7 +714,6 @@ export class ACECodeSearchService {
654
714
  * Fallback symbol search using manual fuzzy matching
655
715
  */
656
716
  async searchSymbolsManual(query, symbolType, language, maxResults = 100, startTime = Date.now()) {
657
- const symbols = [];
658
717
  const queryLower = query.toLowerCase();
659
718
  // Fuzzy match scoring
660
719
  const calculateScore = (symbolName) => {
@@ -669,7 +728,10 @@ export class ACECodeSearchService {
669
728
  if (nameLower.includes(queryLower))
670
729
  return 60;
671
730
  // Camel case match (e.g., "gfc" matches "getFileContent")
672
- const camelCaseMatch = symbolName.split(/(?=[A-Z])/).map(s => s[0]?.toLowerCase() || '').join('');
731
+ const camelCaseMatch = symbolName
732
+ .split(/(?=[A-Z])/)
733
+ .map(s => s[0]?.toLowerCase() || '')
734
+ .join('');
673
735
  if (camelCaseMatch.includes(queryLower))
674
736
  return 40;
675
737
  // Fuzzy match
@@ -685,7 +747,8 @@ export class ACECodeSearchService {
685
747
  return score;
686
748
  return 0;
687
749
  };
688
- // Search through all indexed symbols
750
+ // Search through all indexed symbols with score caching
751
+ const symbolsWithScores = [];
689
752
  for (const fileSymbols of this.indexCache.values()) {
690
753
  for (const symbol of fileSymbols) {
691
754
  // Apply filters
@@ -695,16 +758,20 @@ export class ACECodeSearchService {
695
758
  continue;
696
759
  const score = calculateScore(symbol.name);
697
760
  if (score > 0) {
698
- symbols.push({ ...symbol });
761
+ symbolsWithScores.push({ symbol: { ...symbol }, score });
699
762
  }
700
- if (symbols.length >= maxResults)
701
- break;
763
+ if (symbolsWithScores.length >= maxResults * 2)
764
+ break; // 获取更多候选以便排序
702
765
  }
703
- if (symbols.length >= maxResults)
766
+ if (symbolsWithScores.length >= maxResults * 2)
704
767
  break;
705
768
  }
706
- // Sort by relevance
707
- symbols.sort((a, b) => calculateScore(b.name) - calculateScore(a.name));
769
+ // Sort by score (避免重复计算)
770
+ symbolsWithScores.sort((a, b) => b.score - a.score);
771
+ // Extract top results
772
+ const symbols = symbolsWithScores
773
+ .slice(0, maxResults)
774
+ .map(item => item.symbol);
708
775
  const searchTime = Date.now() - startTime;
709
776
  return {
710
777
  query,
@@ -760,7 +827,8 @@ export class ACECodeSearchService {
760
827
  else if (line.match(new RegExp(`(?:function|class|const|let|var)\\s+${symbolName}`))) {
761
828
  referenceType = 'definition';
762
829
  }
763
- else if (line.includes(':') && line.includes(symbolName)) {
830
+ else if (line.includes(':') &&
831
+ line.includes(symbolName)) {
764
832
  referenceType = 'type';
765
833
  }
766
834
  references.push({
@@ -798,14 +866,20 @@ export class ACECodeSearchService {
798
866
  const fullPath = path.resolve(this.basePath, contextFile);
799
867
  const fileSymbols = this.indexCache.get(fullPath);
800
868
  if (fileSymbols) {
801
- const symbol = fileSymbols.find(s => s.name === symbolName && (s.type === 'function' || s.type === 'class' || s.type === 'variable'));
869
+ const symbol = fileSymbols.find(s => s.name === symbolName &&
870
+ (s.type === 'function' ||
871
+ s.type === 'class' ||
872
+ s.type === 'variable'));
802
873
  if (symbol)
803
874
  return symbol;
804
875
  }
805
876
  }
806
877
  // Search in all files
807
878
  for (const fileSymbols of this.indexCache.values()) {
808
- const symbol = fileSymbols.find(s => s.name === symbolName && (s.type === 'function' || s.type === 'class' || s.type === 'variable'));
879
+ const symbol = fileSymbols.find(s => s.name === symbolName &&
880
+ (s.type === 'function' ||
881
+ s.type === 'class' ||
882
+ s.type === 'variable'));
809
883
  if (symbol)
810
884
  return symbol;
811
885
  }
@@ -816,7 +890,14 @@ export class ACECodeSearchService {
816
890
  */
817
891
  async gitGrepSearch(pattern, fileGlob, maxResults = 100) {
818
892
  return new Promise((resolve, reject) => {
819
- const args = ['grep', '--untracked', '-n', '-E', '--ignore-case', pattern];
893
+ const args = [
894
+ 'grep',
895
+ '--untracked',
896
+ '-n',
897
+ '-E',
898
+ '--ignore-case',
899
+ pattern,
900
+ ];
820
901
  if (fileGlob) {
821
902
  args.push('--', fileGlob);
822
903
  }
@@ -826,12 +907,12 @@ export class ACECodeSearchService {
826
907
  });
827
908
  const stdoutChunks = [];
828
909
  const stderrChunks = [];
829
- child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
830
- child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
831
- child.on('error', (err) => {
910
+ child.stdout.on('data', chunk => stdoutChunks.push(chunk));
911
+ child.stderr.on('data', chunk => stderrChunks.push(chunk));
912
+ child.on('error', err => {
832
913
  reject(new Error(`Failed to start git grep: ${err.message}`));
833
914
  });
834
- child.on('close', (code) => {
915
+ child.on('close', code => {
835
916
  const stdoutData = Buffer.concat(stdoutChunks).toString('utf8');
836
917
  const stderrData = Buffer.concat(stderrChunks).toString('utf8');
837
918
  if (code === 0) {
@@ -853,7 +934,7 @@ export class ACECodeSearchService {
853
934
  */
854
935
  async systemGrepSearch(pattern, fileGlob, maxResults = 100) {
855
936
  // Prefer ripgrep (rg) over grep if available
856
- const grepCommand = await this.isCommandAvailable('rg') ? 'rg' : 'grep';
937
+ const grepCommand = (await this.isCommandAvailable('rg')) ? 'rg' : 'grep';
857
938
  const isRipgrep = grepCommand === 'rg';
858
939
  return new Promise((resolve, reject) => {
859
940
  const args = isRipgrep
@@ -861,8 +942,15 @@ export class ACECodeSearchService {
861
942
  : ['-r', '-n', '-H', '-E', '-i'];
862
943
  // Add exclusion patterns
863
944
  const excludeDirs = [
864
- 'node_modules', '.git', 'dist', 'build',
865
- '__pycache__', 'target', '.next', '.nuxt', 'coverage'
945
+ 'node_modules',
946
+ '.git',
947
+ 'dist',
948
+ 'build',
949
+ '__pycache__',
950
+ 'target',
951
+ '.next',
952
+ '.nuxt',
953
+ 'coverage',
866
954
  ];
867
955
  if (isRipgrep) {
868
956
  // Ripgrep uses --glob for filtering
@@ -885,8 +973,8 @@ export class ACECodeSearchService {
885
973
  });
886
974
  const stdoutChunks = [];
887
975
  const stderrChunks = [];
888
- child.stdout.on('data', (chunk) => stdoutChunks.push(chunk));
889
- child.stderr.on('data', (chunk) => {
976
+ child.stdout.on('data', chunk => stdoutChunks.push(chunk));
977
+ child.stderr.on('data', chunk => {
890
978
  const stderrStr = chunk.toString();
891
979
  // Suppress common harmless stderr messages
892
980
  if (!stderrStr.includes('Permission denied') &&
@@ -894,10 +982,10 @@ export class ACECodeSearchService {
894
982
  stderrChunks.push(chunk);
895
983
  }
896
984
  });
897
- child.on('error', (err) => {
985
+ child.on('error', err => {
898
986
  reject(new Error(`Failed to start ${grepCommand}: ${err.message}`));
899
987
  });
900
- child.on('close', (code) => {
988
+ child.on('close', code => {
901
989
  const stdoutData = Buffer.concat(stdoutChunks).toString('utf8');
902
990
  const stderrData = Buffer.concat(stderrChunks).toString('utf8').trim();
903
991
  if (code === 0) {
@@ -974,13 +1062,37 @@ export class ACECodeSearchService {
974
1062
  // Skip binary files
975
1063
  const ext = path.extname(entry.name).toLowerCase();
976
1064
  const binaryExts = [
977
- '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.ico', '.svg',
978
- '.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
979
- '.exe', '.dll', '.so', '.dylib',
980
- '.mp3', '.mp4', '.avi', '.mov',
981
- '.woff', '.woff2', '.ttf', '.eot',
982
- '.class', '.jar', '.war',
983
- '.o', '.a', '.lib'
1065
+ '.jpg',
1066
+ '.jpeg',
1067
+ '.png',
1068
+ '.gif',
1069
+ '.bmp',
1070
+ '.ico',
1071
+ '.svg',
1072
+ '.pdf',
1073
+ '.zip',
1074
+ '.tar',
1075
+ '.gz',
1076
+ '.rar',
1077
+ '.7z',
1078
+ '.exe',
1079
+ '.dll',
1080
+ '.so',
1081
+ '.dylib',
1082
+ '.mp3',
1083
+ '.mp4',
1084
+ '.avi',
1085
+ '.mov',
1086
+ '.woff',
1087
+ '.woff2',
1088
+ '.ttf',
1089
+ '.eot',
1090
+ '.class',
1091
+ '.jar',
1092
+ '.war',
1093
+ '.o',
1094
+ '.a',
1095
+ '.lib',
984
1096
  ];
985
1097
  if (binaryExts.includes(ext)) {
986
1098
  continue;
@@ -1048,8 +1160,8 @@ export class ACECodeSearchService {
1048
1160
  }
1049
1161
  // Strategy 2: Try system grep/ripgrep
1050
1162
  try {
1051
- const grepAvailable = await this.isCommandAvailable('rg') ||
1052
- await this.isCommandAvailable('grep');
1163
+ const grepAvailable = (await this.isCommandAvailable('rg')) ||
1164
+ (await this.isCommandAvailable('grep'));
1053
1165
  if (grepAvailable) {
1054
1166
  const results = await this.systemGrepSearch(pattern, fileGlob, maxResults);
1055
1167
  return await this.sortResultsByRecency(results);
@@ -1091,8 +1203,8 @@ export class ACECodeSearchService {
1091
1203
  return results.sort((a, b) => {
1092
1204
  const aMtime = fileModTimes.get(a.filePath) || 0;
1093
1205
  const bMtime = fileModTimes.get(b.filePath) || 0;
1094
- const aIsRecent = (now - aMtime) < recentThreshold;
1095
- const bIsRecent = (now - bMtime) < recentThreshold;
1206
+ const aIsRecent = now - aMtime < recentThreshold;
1207
+ const bIsRecent = now - bMtime < recentThreshold;
1096
1208
  // Recent files come first
1097
1209
  if (aIsRecent && !bIsRecent)
1098
1210
  return -1;
@@ -1181,7 +1293,8 @@ export class ACECodeSearchService {
1181
1293
  clearCache() {
1182
1294
  this.indexCache.clear();
1183
1295
  this.fileModTimes.clear();
1184
- this.allIndexedFiles = [];
1296
+ this.allIndexedFiles.clear();
1297
+ this.fileContentCache.clear();
1185
1298
  this.lastIndexTime = 0;
1186
1299
  }
1187
1300
  /**
@@ -1193,7 +1306,8 @@ export class ACECodeSearchService {
1193
1306
  for (const symbols of this.indexCache.values()) {
1194
1307
  totalSymbols += symbols.length;
1195
1308
  for (const symbol of symbols) {
1196
- languageBreakdown[symbol.language] = (languageBreakdown[symbol.language] || 0) + 1;
1309
+ languageBreakdown[symbol.language] =
1310
+ (languageBreakdown[symbol.language] || 0) + 1;
1197
1311
  }
1198
1312
  }
1199
1313
  return {
@@ -1220,12 +1334,31 @@ export const mcpTools = [
1220
1334
  },
1221
1335
  symbolType: {
1222
1336
  type: 'string',
1223
- enum: ['function', 'class', 'method', 'variable', 'constant', 'interface', 'type', 'enum', 'import', 'export'],
1337
+ enum: [
1338
+ 'function',
1339
+ 'class',
1340
+ 'method',
1341
+ 'variable',
1342
+ 'constant',
1343
+ 'interface',
1344
+ 'type',
1345
+ 'enum',
1346
+ 'import',
1347
+ 'export',
1348
+ ],
1224
1349
  description: 'Filter by specific symbol type (optional)',
1225
1350
  },
1226
1351
  language: {
1227
1352
  type: 'string',
1228
- enum: ['typescript', 'javascript', 'python', 'go', 'rust', 'java', 'csharp'],
1353
+ enum: [
1354
+ 'typescript',
1355
+ 'javascript',
1356
+ 'python',
1357
+ 'go',
1358
+ 'rust',
1359
+ 'java',
1360
+ 'csharp',
1361
+ ],
1229
1362
  description: 'Filter by programming language (optional)',
1230
1363
  },
1231
1364
  maxResults: {
@@ -1292,7 +1425,15 @@ export const mcpTools = [
1292
1425
  },
1293
1426
  language: {
1294
1427
  type: 'string',
1295
- enum: ['typescript', 'javascript', 'python', 'go', 'rust', 'java', 'csharp'],
1428
+ enum: [
1429
+ 'typescript',
1430
+ 'javascript',
1431
+ 'python',
1432
+ 'go',
1433
+ 'rust',
1434
+ 'java',
1435
+ 'csharp',
1436
+ ],
1296
1437
  description: 'Filter by programming language (optional)',
1297
1438
  },
1298
1439
  maxResults: {
@@ -1306,7 +1447,7 @@ export const mcpTools = [
1306
1447
  },
1307
1448
  {
1308
1449
  name: 'ace_file_outline',
1309
- description: 'ACE Code Search: Get complete code outline for a file. Shows all functions, classes, variables, and other symbols defined in the file with their locations. Similar to VS Code\'s outline view.',
1450
+ description: "ACE Code Search: Get complete code outline for a file. Shows all functions, classes, variables, and other symbols defined in the file with their locations. Similar to VS Code's outline view.",
1310
1451
  inputSchema: {
1311
1452
  type: 'object',
1312
1453
  properties: {
@@ -166,6 +166,7 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
166
166
  if (value === '__DELETE__') {
167
167
  if (activeProfile === 'default') {
168
168
  setErrors(['Cannot delete the default profile']);
169
+ setIsEditing(false); // Exit editing mode to prevent Select component error
169
170
  return;
170
171
  }
171
172
  setProfileMode('deleting');
@@ -222,6 +223,10 @@ export default function ConfigScreen({ onBack, onSave, inlineMode = false, }) {
222
223
  const handleDeleteProfile = () => {
223
224
  try {
224
225
  deleteProfile(activeProfile);
226
+ // Important: Update activeProfile state BEFORE loading profiles
227
+ // because deleteProfile switches to 'default' if the active profile is deleted
228
+ const newActiveProfile = getActiveProfileName();
229
+ setActiveProfile(newActiveProfile);
225
230
  loadProfilesAndConfig();
226
231
  setProfileMode('normal');
227
232
  setIsEditing(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snow-ai",
3
- "version": "0.2.28",
3
+ "version": "0.3.1",
4
4
  "description": "Intelligent Command Line Assistant powered by AI",
5
5
  "license": "MIT",
6
6
  "bin": {