@pixelbyte-software/pixcode 1.53.10 → 1.53.12

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.
@@ -69,6 +69,15 @@ import sessionManager from './sessionManager.js';
69
69
  import { applyCustomSessionNames } from './database/db.js';
70
70
 
71
71
  const FILE_COUNT_LIMIT = 20000;
72
+ const FILE_COUNT_CACHE_TTL_MS = 5 * 60 * 1000;
73
+ const FILE_COUNT_CACHE_MAX_ENTRIES = 300;
74
+ const FILE_COUNT_SCAN_MAX_MS = 2500;
75
+ const CODEX_SESSION_INDEX_TTL_MS = 60 * 1000;
76
+ const CODEX_SESSION_INDEX_MAX_FILES = 1200;
77
+ const CODEX_SESSION_INDEX_MAX_DIRS = 2500;
78
+ const GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS = 60 * 1000;
79
+ const CLI_CHAT_FILE_READ_MAX_BYTES = 2 * 1024 * 1024;
80
+ const CLI_CHAT_FILES_PER_PROJECT_LIMIT = 150;
72
81
  const FILE_COUNT_IGNORED_DIRECTORIES = new Set([
73
82
  '.cache',
74
83
  '.git',
@@ -88,11 +97,45 @@ const FILE_COUNT_IGNORED_DIRECTORIES = new Set([
88
97
  'target',
89
98
  'vendor'
90
99
  ]);
100
+ const fileCountCache = new Map();
101
+ let codexSessionsIndexCache = null;
102
+ let geminiCliProjectEntriesCache = null;
103
+ let qwenCliProjectEntriesCache = null;
104
+
105
+ function pruneMapToMaxEntries(map, maxEntries) {
106
+ while (map.size > maxEntries) {
107
+ const firstKey = map.keys().next().value;
108
+ if (firstKey === undefined) break;
109
+ map.delete(firstKey);
110
+ }
111
+ }
112
+
113
+ function readFreshCacheEntry(cache, key, now = Date.now()) {
114
+ const entry = cache.get(key);
115
+ if (!entry || entry.expiresAt <= now) {
116
+ if (entry) cache.delete(key);
117
+ return undefined;
118
+ }
119
+ return entry.value;
120
+ }
121
+
122
+ function writeCacheEntry(cache, key, value, ttlMs, maxEntries) {
123
+ cache.set(key, {
124
+ value,
125
+ expiresAt: Date.now() + ttlMs,
126
+ });
127
+ pruneMapToMaxEntries(cache, maxEntries);
128
+ }
91
129
 
92
130
  async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
93
131
  if (!projectPath || typeof projectPath !== 'string') {
94
132
  return undefined;
95
133
  }
134
+ const cacheKey = `${path.resolve(projectPath)}:${maxFiles}`;
135
+ const cached = readFreshCacheEntry(fileCountCache, cacheKey);
136
+ if (cached !== undefined) {
137
+ return cached;
138
+ }
96
139
 
97
140
  try {
98
141
  const stats = await fs.stat(projectPath);
@@ -104,9 +147,15 @@ async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
104
147
  }
105
148
 
106
149
  let count = 0;
150
+ const startedAt = Date.now();
107
151
  const pendingDirectories = [projectPath];
108
152
 
109
153
  while (pendingDirectories.length > 0) {
154
+ if (Date.now() - startedAt > FILE_COUNT_SCAN_MAX_MS) {
155
+ writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
156
+ return count;
157
+ }
158
+
110
159
  const currentDirectory = pendingDirectories.pop();
111
160
  let entries = [];
112
161
 
@@ -127,12 +176,14 @@ async function countProjectFiles(projectPath, maxFiles = FILE_COUNT_LIMIT) {
127
176
  if (entry.isFile() || entry.isSymbolicLink()) {
128
177
  count += 1;
129
178
  if (count >= maxFiles) {
179
+ writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
130
180
  return count;
131
181
  }
132
182
  }
133
183
  }
134
184
  }
135
185
 
186
+ writeCacheEntry(fileCountCache, cacheKey, count, FILE_COUNT_CACHE_TTL_MS, FILE_COUNT_CACHE_MAX_ENTRIES);
136
187
  return count;
137
188
  }
138
189
 
@@ -207,6 +258,10 @@ function areStringArraysEqual(first, second) {
207
258
  // Gemini ones rather than sharing a parametric implementation, keeping
208
259
  // provider-specific logic discoverable in one place.
209
260
  async function listQwenCliProjectEntries() {
261
+ if (qwenCliProjectEntriesCache?.expiresAt > Date.now()) {
262
+ return qwenCliProjectEntriesCache.entries;
263
+ }
264
+
210
265
  const qwenTmpDir = path.join(os.homedir(), '.qwen', 'tmp');
211
266
  try {
212
267
  await fs.access(qwenTmpDir);
@@ -241,10 +296,18 @@ async function listQwenCliProjectEntries() {
241
296
  });
242
297
  }
243
298
 
299
+ qwenCliProjectEntriesCache = {
300
+ entries,
301
+ expiresAt: Date.now() + GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS,
302
+ };
244
303
  return entries;
245
304
  }
246
305
 
247
306
  async function listGeminiCliProjectEntries() {
307
+ if (geminiCliProjectEntriesCache?.expiresAt > Date.now()) {
308
+ return geminiCliProjectEntriesCache.entries;
309
+ }
310
+
248
311
  const geminiTmpDir = path.join(os.homedir(), '.gemini', 'tmp');
249
312
  try {
250
313
  await fs.access(geminiTmpDir);
@@ -282,9 +345,21 @@ async function listGeminiCliProjectEntries() {
282
345
  });
283
346
  }
284
347
 
348
+ geminiCliProjectEntriesCache = {
349
+ entries,
350
+ expiresAt: Date.now() + GEMINI_QWEN_PROJECT_ENTRY_CACHE_TTL_MS,
351
+ };
285
352
  return entries;
286
353
  }
287
354
 
355
+ async function readJsonTextFileLimited(filePath, maxBytes = CLI_CHAT_FILE_READ_MAX_BYTES) {
356
+ const stat = await fs.stat(filePath);
357
+ if (!stat.isFile() || stat.size > maxBytes) {
358
+ return null;
359
+ }
360
+ return fs.readFile(filePath, 'utf8');
361
+ }
362
+
288
363
  function addDiscoveredProject(discoveredProjectsByPath, projectPath, provider) {
289
364
  if (typeof projectPath !== 'string' || projectPath.trim().length === 0) {
290
365
  return;
@@ -1614,27 +1689,46 @@ function normalizeComparablePath(inputPath) {
1614
1689
  return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
1615
1690
  }
1616
1691
 
1617
- async function findCodexJsonlFiles(dir) {
1692
+ async function findCodexJsonlFiles(dir, {
1693
+ maxFiles = CODEX_SESSION_INDEX_MAX_FILES,
1694
+ maxDirs = CODEX_SESSION_INDEX_MAX_DIRS,
1695
+ } = {}) {
1618
1696
  const files = [];
1697
+ const pendingDirs = [dir];
1698
+ let visitedDirs = 0;
1619
1699
 
1620
- try {
1621
- const entries = await fs.readdir(dir, { withFileTypes: true });
1622
- for (const entry of entries) {
1623
- const fullPath = path.join(dir, entry.name);
1624
- if (entry.isDirectory()) {
1625
- files.push(...await findCodexJsonlFiles(fullPath));
1626
- } else if (entry.name.endsWith('.jsonl')) {
1627
- files.push(fullPath);
1700
+ while (pendingDirs.length > 0 && files.length < maxFiles && visitedDirs < maxDirs) {
1701
+ const currentDir = pendingDirs.pop();
1702
+ visitedDirs += 1;
1703
+
1704
+ try {
1705
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
1706
+ entries.sort((a, b) => b.name.localeCompare(a.name));
1707
+
1708
+ for (const entry of entries) {
1709
+ if (files.length >= maxFiles) break;
1710
+ const fullPath = path.join(currentDir, entry.name);
1711
+ if (entry.isDirectory()) {
1712
+ if (visitedDirs + pendingDirs.length < maxDirs) {
1713
+ pendingDirs.push(fullPath);
1714
+ }
1715
+ } else if (entry.name.endsWith('.jsonl')) {
1716
+ files.push(fullPath);
1717
+ }
1628
1718
  }
1719
+ } catch {
1720
+ // Skip directories we can't read
1629
1721
  }
1630
- } catch (error) {
1631
- // Skip directories we can't read
1632
1722
  }
1633
1723
 
1634
1724
  return files;
1635
1725
  }
1636
1726
 
1637
1727
  async function buildCodexSessionsIndex() {
1728
+ if (codexSessionsIndexCache?.expiresAt > Date.now()) {
1729
+ return codexSessionsIndexCache.sessionsByProject;
1730
+ }
1731
+
1638
1732
  const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
1639
1733
  const sessionsByProject = new Map();
1640
1734
 
@@ -1683,6 +1777,10 @@ async function buildCodexSessionsIndex() {
1683
1777
  sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
1684
1778
  }
1685
1779
 
1780
+ codexSessionsIndexCache = {
1781
+ sessionsByProject,
1782
+ expiresAt: Date.now() + CODEX_SESSION_INDEX_TTL_MS,
1783
+ };
1686
1784
  return sessionsByProject;
1687
1785
  }
1688
1786
 
@@ -2049,24 +2147,7 @@ async function getCodexSessionMessages(sessionId, limit = null, offset = 0) {
2049
2147
  async function deleteCodexSession(sessionId) {
2050
2148
  try {
2051
2149
  const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
2052
-
2053
- const findJsonlFiles = async (dir) => {
2054
- const files = [];
2055
- try {
2056
- const entries = await fs.readdir(dir, { withFileTypes: true });
2057
- for (const entry of entries) {
2058
- const fullPath = path.join(dir, entry.name);
2059
- if (entry.isDirectory()) {
2060
- files.push(...await findJsonlFiles(fullPath));
2061
- } else if (entry.name.endsWith('.jsonl')) {
2062
- files.push(fullPath);
2063
- }
2064
- }
2065
- } catch (error) { }
2066
- return files;
2067
- };
2068
-
2069
- const jsonlFiles = await findJsonlFiles(codexSessionsDir);
2150
+ const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
2070
2151
 
2071
2152
  for (const filePath of jsonlFiles) {
2072
2153
  const sessionData = await parseCodexSessionFile(filePath);
@@ -2582,13 +2663,14 @@ async function searchGeminiSessionsForProject(
2582
2663
  continue;
2583
2664
  }
2584
2665
 
2585
- for (const chatFile of chatFiles) {
2666
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2586
2667
  if (getTotalMatches() >= limit || isAborted()) break;
2587
2668
  if (!chatFile.endsWith('.json')) continue;
2588
2669
 
2589
2670
  try {
2590
2671
  const filePath = path.join(chatsDir, chatFile);
2591
- const data = await fs.readFile(filePath, 'utf8');
2672
+ const data = await readJsonTextFileLimited(filePath);
2673
+ if (!data) continue;
2592
2674
  const session = JSON.parse(data);
2593
2675
  if (!session.messages || !Array.isArray(session.messages)) continue;
2594
2676
 
@@ -2680,11 +2762,12 @@ async function getQwenCliSessions(projectPath, options = {}) {
2680
2762
  continue;
2681
2763
  }
2682
2764
 
2683
- for (const chatFile of chatFiles) {
2765
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2684
2766
  if (!chatFile.endsWith('.json')) continue;
2685
2767
  try {
2686
2768
  const filePath = path.join(chatsDir, chatFile);
2687
- const data = await fs.readFile(filePath, 'utf8');
2769
+ const data = await readJsonTextFileLimited(filePath);
2770
+ if (!data) continue;
2688
2771
  const session = JSON.parse(data);
2689
2772
  if (!session.messages || !Array.isArray(session.messages)) continue;
2690
2773
 
@@ -2763,10 +2846,11 @@ async function getOpencodeCliSessions(projectPath) {
2763
2846
  continue;
2764
2847
  }
2765
2848
 
2766
- for (const file of sessionFiles) {
2849
+ for (const file of sessionFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2767
2850
  if (!file.startsWith('ses_') || !file.endsWith('.json')) continue;
2768
2851
  try {
2769
- const data = await fs.readFile(path.join(dir, file), 'utf8');
2852
+ const data = await readJsonTextFileLimited(path.join(dir, file));
2853
+ if (!data) continue;
2770
2854
  const sess = JSON.parse(data);
2771
2855
  const sessDir = typeof sess?.directory === 'string'
2772
2856
  ? normalizeComparablePath(sess.directory)
@@ -2830,11 +2914,12 @@ async function getQwenCliSessionMessages(sessionId) {
2830
2914
  continue;
2831
2915
  }
2832
2916
 
2833
- for (const chatFile of chatFiles) {
2917
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2834
2918
  if (!chatFile.endsWith('.json')) continue;
2835
2919
  try {
2836
2920
  const filePath = path.join(chatsDir, chatFile);
2837
- const data = await fs.readFile(filePath, 'utf8');
2921
+ const data = await readJsonTextFileLimited(filePath);
2922
+ if (!data) continue;
2838
2923
  const session = JSON.parse(data);
2839
2924
  const fileSessionId = session.sessionId || chatFile.replace('.json', '');
2840
2925
  if (fileSessionId !== sessionId) continue;
@@ -2890,11 +2975,12 @@ async function getGeminiCliSessions(projectPath, options = {}) {
2890
2975
  continue;
2891
2976
  }
2892
2977
 
2893
- for (const chatFile of chatFiles) {
2978
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2894
2979
  if (!chatFile.endsWith('.json')) continue;
2895
2980
  try {
2896
2981
  const filePath = path.join(chatsDir, chatFile);
2897
- const data = await fs.readFile(filePath, 'utf8');
2982
+ const data = await readJsonTextFileLimited(filePath);
2983
+ if (!data) continue;
2898
2984
  const session = JSON.parse(data);
2899
2985
  if (!session.messages || !Array.isArray(session.messages)) continue;
2900
2986
 
@@ -2946,11 +3032,12 @@ async function getGeminiCliSessionMessages(sessionId) {
2946
3032
  continue;
2947
3033
  }
2948
3034
 
2949
- for (const chatFile of chatFiles) {
3035
+ for (const chatFile of chatFiles.slice(0, CLI_CHAT_FILES_PER_PROJECT_LIMIT)) {
2950
3036
  if (!chatFile.endsWith('.json')) continue;
2951
3037
  try {
2952
3038
  const filePath = path.join(chatsDir, chatFile);
2953
- const data = await fs.readFile(filePath, 'utf8');
3039
+ const data = await readJsonTextFileLimited(filePath);
3040
+ if (!data) continue;
2954
3041
  const session = JSON.parse(data);
2955
3042
  const fileSessionId = session.sessionId || chatFile.replace('.json', '');
2956
3043
  if (fileSessionId !== sessionId) continue;
@@ -51,6 +51,14 @@ function isNotGitRepositoryMessage(message = '') {
51
51
  || message.includes('Project directory is not a git repository');
52
52
  }
53
53
 
54
+ function isMissingFileError(error) {
55
+ const message = `${error?.message || ''} ${error?.stderr || ''} ${error?.stdout || ''}`.toLowerCase();
56
+ return error?.code === 'ENOENT'
57
+ || message.includes('enoent')
58
+ || message.includes('no such file or directory')
59
+ || message.includes('path does not exist');
60
+ }
61
+
54
62
  function shouldSkipFilesystemEntry(entryName) {
55
63
  return FILESYSTEM_SCAN_EXCLUDED_DIRS.has(entryName)
56
64
  || entryName.endsWith('.log')
@@ -746,6 +754,15 @@ router.get('/file-with-diff', async (req, res) => {
746
754
  isUntracked
747
755
  });
748
756
  } catch (error) {
757
+ if (isMissingFileError(error)) {
758
+ return res.status(404).json({
759
+ error: 'File not found',
760
+ currentContent: '',
761
+ oldContent: '',
762
+ isDeleted: true,
763
+ isUntracked: false,
764
+ });
765
+ }
749
766
  console.error('Git file-with-diff error:', error);
750
767
  res.json({ error: error.message });
751
768
  }