diff --git a/folder-index-state.js b/folder-index-state.js new file mode 100644 index 0000000..7e52deb --- /dev/null +++ b/folder-index-state.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const path = require('path'); + +function getFolderIndexMtimeMs(folderPath) { + let indexMtimeMs = 0; + + try { + indexMtimeMs = fs.statSync(folderPath).mtimeMs; + } catch { + return 0; + } + + try { + // Session files are appended in place, which updates the file mtime but + // often leaves the containing directory mtime unchanged. + const entries = fs.readdirSync(folderPath, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue; + try { + const fileMtimeMs = fs.statSync(path.join(folderPath, entry.name)).mtimeMs; + if (fileMtimeMs > indexMtimeMs) indexMtimeMs = fileMtimeMs; + } catch {} + } + } catch {} + + return indexMtimeMs; +} + +module.exports = { getFolderIndexMtimeMs }; diff --git a/main.js b/main.js index 444aae9..9e24429 100644 --- a/main.js +++ b/main.js @@ -5,6 +5,7 @@ const fs = require('fs'); const os = require('os'); const pty = require('node-pty'); const log = require('electron-log'); +const { getFolderIndexMtimeMs } = require('./folder-index-state'); log.transports.file.level = app.isPackaged ? 'info' : 'debug'; log.transports.console.level = app.isPackaged ? 'info' : 'debug'; @@ -347,10 +348,7 @@ function refreshFolder(folder) { const projectPath = deriveProjectPath(folderPath, folder); if (!projectPath) { - // Still record mtime so backgroundRefresh doesn't keep retrying - let mtimeMs = 0; - try { mtimeMs = fs.statSync(folderPath).mtimeMs; } catch {} - setFolderMeta(folder, null, mtimeMs); + setFolderMeta(folder, null, getFolderIndexMtimeMs(folderPath)); return; } @@ -407,9 +405,7 @@ function refreshFolder(folder) { } // Update folder mtime - let mtimeMs = 0; - try { mtimeMs = fs.statSync(folderPath).mtimeMs; } catch {} - setFolderMeta(folder, projectPath, mtimeMs); + setFolderMeta(folder, projectPath, getFolderIndexMtimeMs(folderPath)); } /** Populate entire cache from filesystem (cold start) */ @@ -505,8 +501,7 @@ function backgroundRefresh() { // Check for new/changed folders for (const folder of folders) { const folderPath = path.join(PROJECTS_DIR, folder); - let currentMtime = 0; - try { currentMtime = fs.statSync(folderPath).mtimeMs; } catch {} + const currentMtime = getFolderIndexMtimeMs(folderPath); const cached = metaMap.get(folder); if (!cached || cached.indexMtimeMs !== currentMtime) { @@ -576,7 +571,7 @@ function populateCacheViaWorker() { // Write results to DB on main thread (fast) let sessionCount = 0; - for (const { folder, projectPath, sessions, mtimeMs } of msg.results) { + for (const { folder, projectPath, sessions, indexMtimeMs } of msg.results) { deleteCachedFolder(folder); deleteSearchFolder(folder); if (sessions.length > 0) { @@ -590,7 +585,7 @@ function populateCacheViaWorker() { if (s.customTitle) setName(s.sessionId, s.customTitle); } } - setFolderMeta(folder, projectPath, mtimeMs); + setFolderMeta(folder, projectPath, indexMtimeMs); } populatingCache = false; diff --git a/package.json b/package.json index 4559bc7..584312e 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "main": "main.js", "scripts": { "start": "npm run bundle:codemirror && electron .", + "test": "node --test", "electron": "electron .", "bundle:codemirror": "esbuild public/codemirror-setup.js --bundle --outfile=public/codemirror-bundle.js --format=iife --platform=browser --minify", "generate-icons": "node scripts/generate-icons.js && node scripts/generate-dmg-background.js", diff --git a/test/folder-index-state.test.js b/test/folder-index-state.test.js new file mode 100644 index 0000000..aa070b1 --- /dev/null +++ b/test/folder-index-state.test.js @@ -0,0 +1,28 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const { getFolderIndexMtimeMs } = require('../folder-index-state'); + +test('folder index timestamp advances when an existing session file is appended', async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'switchboard-folder-index-')); + + try { + const sessionPath = path.join(tmpDir, 'session.jsonl'); + fs.writeFileSync(sessionPath, '{"type":"user","message":"first"}\n', 'utf8'); + + const before = getFolderIndexMtimeMs(tmpDir); + + await new Promise(resolve => setTimeout(resolve, 1100)); + + fs.appendFileSync(sessionPath, '{"type":"assistant","message":"second"}\n', 'utf8'); + + const after = getFolderIndexMtimeMs(tmpDir); + + assert.ok(after > before, `expected index mtime to increase (${before} -> ${after})`); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); diff --git a/workers/scan-projects.js b/workers/scan-projects.js index 2caeb47..e29c22e 100644 --- a/workers/scan-projects.js +++ b/workers/scan-projects.js @@ -1,6 +1,7 @@ const { parentPort, workerData } = require('worker_threads'); const fs = require('fs'); const path = require('path'); +const { getFolderIndexMtimeMs } = require('../folder-index-state'); const PROJECTS_DIR = workerData.projectsDir; @@ -49,8 +50,7 @@ function readFolderFromFilesystem(folder) { const projectPath = deriveProjectPath(folderPath, folder); if (!projectPath) return null; const sessions = []; - let mtimeMs = 0; - try { mtimeMs = fs.statSync(folderPath).mtimeMs; } catch {} + const indexMtimeMs = getFolderIndexMtimeMs(folderPath); try { const jsonlFiles = fs.readdirSync(folderPath).filter(f => f.endsWith('.jsonl')); @@ -99,7 +99,7 @@ function readFolderFromFilesystem(folder) { } } catch {} - return { folder, projectPath, sessions, mtimeMs }; + return { folder, projectPath, sessions, indexMtimeMs }; } // Scan all folders