diff --git a/encode-project-path.js b/encode-project-path.js new file mode 100644 index 0000000..5f1ce44 --- /dev/null +++ b/encode-project-path.js @@ -0,0 +1,14 @@ +// Mirror Claude CLI's project-folder naming so Switchboard-created folders +// match the ones the CLI writes for the same project path. +// Reverse-engineered from claude CLI 2.1.126. +function encodeProjectPath(projectPath) { + const sanitized = projectPath.replace(/[^a-zA-Z0-9]/g, '-'); + if (sanitized.length <= 200) return sanitized; + let h = 0; + for (let i = 0; i < projectPath.length; i++) { + h = (h << 5) - h + projectPath.charCodeAt(i) | 0; + } + return sanitized.slice(0, 200) + '-' + Math.abs(h).toString(36); +} + +module.exports = { encodeProjectPath }; diff --git a/main.js b/main.js index 98a720c..fd414c6 100644 --- a/main.js +++ b/main.js @@ -80,6 +80,7 @@ ipcMain.handle('reload-shell-env', async () => { // Shell profiles → shell-profiles.js const { discoverShellProfiles, getShellProfiles, resolveShell, isWindows, isWslShell, windowsToWslPath, shellArgs } = require('./shell-profiles'); const { startScheduler } = require('./schedule-runner'); +const { encodeProjectPath } = require('./encode-project-path'); @@ -331,7 +332,7 @@ ipcMain.handle('add-project', (_event, projectPath) => { } // Create the corresponding folder in ~/.claude/projects/ so it persists - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); const folderPath = path.join(PROJECTS_DIR, folder); if (!fs.existsSync(folderPath)) { fs.mkdirSync(folderPath, { recursive: true }); @@ -367,7 +368,7 @@ ipcMain.handle('remove-project', (_event, projectPath) => { setSetting('global', global); // Clean up DB cache and search index for this folder - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); deleteCachedFolder(folder); deleteSearchFolder(folder); deleteSetting('project:' + projectPath); @@ -1031,7 +1032,7 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se if (!isPlainTerminal) { // Snapshot existing .jsonl files before spawning (for new session + fork/plan detection) - projectFolder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + projectFolder = encodeProjectPath(projectPath); const claudeProjectDir = path.join(PROJECTS_DIR, projectFolder); if (fs.existsSync(claudeProjectDir)) { try { diff --git a/package-lock.json b/package-lock.json index c2f6242..2d09a2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,24 @@ { "name": "switchboard", - "version": "0.0.25", + "version": "0.0.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "switchboard", - "version": "0.0.25", + "version": "0.0.29", "hasInstallScript": true, "dependencies": { "@xterm/addon-fit": "0.11.0", "@xterm/addon-search": "0.16.0", + "@xterm/addon-unicode-graphemes": "0.4.0", "@xterm/addon-web-links": "0.12.0", "@xterm/addon-webgl": "0.19.0", "@xterm/xterm": "6.0.0", "better-sqlite3": "12.8.0", "dompurify": "3.4.1", "electron-log": "5.4.3", + "electron-updater": "6.3.9", "marked": "17.0.4", "morphdom": "2.7.8", "node-pty": "1.1.0", @@ -638,31 +640,6 @@ "global-agent": "^3.0.0" } }, - "node_modules/@electron/get/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@electron/get/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@electron/get/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -688,24 +665,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@electron/notarize/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@electron/notarize/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -735,13 +694,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/notarize/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@electron/notarize/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -774,24 +726,6 @@ "node": ">=12.0.0" } }, - "node_modules/@electron/osx-sign/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@electron/osx-sign/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -833,13 +767,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/osx-sign/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@electron/osx-sign/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -878,31 +805,6 @@ "node": ">=22.12.0" } }, - "node_modules/@electron/rebuild/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@electron/rebuild/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@electron/rebuild/node_modules/node-abi": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.28.0.tgz", @@ -952,24 +854,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/@electron/universal/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@electron/universal/node_modules/fs-extra": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", @@ -1014,13 +898,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@electron/universal/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@electron/universal/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -1053,26 +930,6 @@ "node": ">=14.14" } }, - "node_modules/@electron/windows-sign/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@electron/windows-sign/node_modules/fs-extra": { "version": "11.3.4", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", @@ -1105,15 +962,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@electron/windows-sign/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@electron/windows-sign/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -1917,24 +1765,6 @@ "node": ">= 10.0.0" } }, - "node_modules/@malept/flatpak-bundler/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -1964,13 +1794,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/@malept/flatpak-bundler/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@malept/flatpak-bundler/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2460,6 +2283,12 @@ "integrity": "sha512-9OeuBFu0/uZJPu+9AHKY6g/w0Czyb/Ut0A5t79I4ULoU4IfU5BEpPFVGQxP4zTTMdfZEYkVIRYbHBX1xWwjeSA==", "license": "MIT" }, + "node_modules/@xterm/addon-unicode-graphemes": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-unicode-graphemes/-/addon-unicode-graphemes-0.4.0.tgz", + "integrity": "sha512-9+/CqwbKcnlkJU4d3wIgO+wjsL8f6vyz+UwUWLu6nADQz8Gr8ONqGCJfdDjIdI+yYZLABQqQy47FzEM6AWELjw==", + "license": "MIT" + }, "node_modules/@xterm/addon-web-links": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", @@ -2697,24 +2526,6 @@ "node": ">=8" } }, - "node_modules/app-builder-lib/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/app-builder-lib/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -2753,18 +2564,10 @@ "node": ">= 10.0.0" } }, - "node_modules/app-builder-lib/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/assert-plus": { @@ -3015,49 +2818,6 @@ "node": ">=12.0.0" } }, - "node_modules/builder-util-runtime/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/builder-util-runtime/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/builder-util/node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -3086,13 +2846,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/builder-util/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/builder-util/node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -3557,6 +3310,23 @@ "node": ">=6" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -4079,6 +3849,70 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/electron-updater": { + "version": "6.3.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.9.tgz", + "integrity": "sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.2.10", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "^7.6.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/builder-util-runtime": { + "version": "9.2.10", + "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz", + "integrity": "sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "sax": "^1.2.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-winstaller": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", @@ -4101,25 +3935,6 @@ "@electron/windows-sign": "^1.1.2" } }, - "node_modules/electron-winstaller/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/electron-winstaller/node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -4136,14 +3951,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/electron-winstaller/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4362,31 +4169,6 @@ "@types/yauzl": "^2.9.1" } }, - "node_modules/extract-zip/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/extsprintf": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", @@ -4828,7 +4610,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/has-flag": { @@ -4931,31 +4712,6 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -4984,31 +4740,6 @@ "node": ">= 14" } }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/iconv-corefoundation": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", @@ -5249,7 +4980,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5317,7 +5047,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -5343,6 +5072,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -5680,6 +5422,12 @@ "integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -6195,31 +5943,6 @@ "read-binary-file-arch": "cli.js" } }, - "node_modules/read-binary-file-arch/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/read-binary-file-arch/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -6394,7 +6117,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -6582,31 +6304,6 @@ "node": ">= 14" } }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6755,31 +6452,6 @@ "node": ">= 8.0" } }, - "node_modules/sumchecker/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/sumchecker/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -6952,6 +6624,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 3845f5c..c2e33a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "switchboard", - "version": "0.0.25", + "version": "0.0.29", "private": true, "description": "A desktop app for browsing, searching, and managing CLI coding sessions", "author": "Doctly ", @@ -25,12 +25,14 @@ "dependencies": { "@xterm/addon-fit": "0.11.0", "@xterm/addon-search": "0.16.0", + "@xterm/addon-unicode-graphemes": "0.4.0", "@xterm/addon-web-links": "0.12.0", "@xterm/addon-webgl": "0.19.0", "@xterm/xterm": "6.0.0", "better-sqlite3": "12.8.0", "dompurify": "3.4.1", "electron-log": "5.4.3", + "electron-updater": "6.3.9", "marked": "17.0.4", "morphdom": "2.7.8", "node-pty": "1.1.0", diff --git a/public/app.js b/public/app.js index 44f2a0e..6a8f684 100644 --- a/public/app.js +++ b/public/app.js @@ -670,7 +670,7 @@ async function loadProjects({ resort = false } = {}) { const activeTerminals = await window.api.getActiveTerminals(); for (const { sessionId, projectPath } of activeTerminals) { if (pendingSessions.has(sessionId)) continue; // already tracked - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); // Find the session object already injected by the backend let session; for (const proj of cachedAllProjects) { @@ -709,7 +709,7 @@ async function launchNewSession(project, sessionOptions) { }; // Track as pending (no .jsonl yet) - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); pendingSessions.set(sessionId, { session, projectPath, folder }); // Inject into cached project data so it appears in sidebar immediately diff --git a/public/dialogs.js b/public/dialogs.js index dab5b16..836ce2d 100644 --- a/public/dialogs.js +++ b/public/dialogs.js @@ -49,7 +49,7 @@ async function launchScheduleCreator(project) { }; // Inject into sidebar - const folder = project.projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(project.projectPath); pendingSessions.set(result.sessionId, { session, projectPath: project.projectPath, folder }); sessionMap.set(result.sessionId, session); for (const projList of [cachedProjects, cachedAllProjects]) { @@ -141,7 +141,7 @@ async function launchTerminalSession(project) { }; // Track as pending - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); pendingSessions.set(sessionId, { session, projectPath, folder }); // Inject into cached project data diff --git a/public/index.html b/public/index.html index 0d21cbb..642eb35 100644 --- a/public/index.html +++ b/public/index.html @@ -102,6 +102,7 @@ + diff --git a/public/terminal-manager.js b/public/terminal-manager.js index 5738184..6863b22 100644 --- a/public/terminal-manager.js +++ b/public/terminal-manager.js @@ -175,7 +175,7 @@ function createTerminalEntry(session) { fontSize: 12, fontFamily: "'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace", theme: TERMINAL_THEME, - cursorBlink: true, + cursorBlink: false, scrollback: 10000, convertEol: true, allowProposedApi: true, @@ -202,6 +202,8 @@ function createTerminalEntry(session) { })); const searchAddon = new SearchAddon.SearchAddon(); terminal.loadAddon(searchAddon); + terminal.loadAddon(new UnicodeGraphemesAddon.UnicodeGraphemesAddon()); + terminal.unicode.activeVersion = '15'; terminal.open(container); container.style.backgroundColor = TERMINAL_THEME.background; diff --git a/public/utils.js b/public/utils.js index 6046a93..a97a5f9 100644 --- a/public/utils.js +++ b/public/utils.js @@ -1,5 +1,17 @@ // --- Utility functions (shared across renderer modules) --- +// Mirror Claude CLI's project-folder naming. Must stay in sync with +// encode-project-path.js (main process). Reverse-engineered from claude CLI 2.1.126. +function encodeProjectPath(projectPath) { + const sanitized = projectPath.replace(/[^a-zA-Z0-9]/g, '-'); + if (sanitized.length <= 200) return sanitized; + let h = 0; + for (let i = 0; i < projectPath.length; i++) { + h = (h << 5) - h + projectPath.charCodeAt(i) | 0; + } + return sanitized.slice(0, 200) + '-' + Math.abs(h).toString(36); +} + function cleanDisplayName(name) { if (!name) return name; const prefix = 'Implement the following plan:'; diff --git a/read-session-file.js b/read-session-file.js index 26c2e8b..1123289 100644 --- a/read-session-file.js +++ b/read-session-file.js @@ -13,12 +13,16 @@ function readSessionFile(filePath, folder, projectPath) { let textContent = ''; let slug = null; let customTitle = null; + let aiTitle = null; for (const line of lines) { const entry = JSON.parse(line); if (entry.slug && !slug) slug = entry.slug; if (entry.type === 'custom-title' && entry.customTitle) { customTitle = entry.customTitle; } + if (entry.type === 'ai-title' && entry.aiTitle) { + aiTitle = entry.aiTitle; + } if (entry.type === 'user' || entry.type === 'assistant' || (entry.type === 'message' && (entry.role === 'user' || entry.role === 'assistant'))) { messageCount++; @@ -45,7 +49,7 @@ function readSessionFile(filePath, folder, projectPath) { summary, firstPrompt: summary, created: stat.birthtime.toISOString(), modified: stat.mtime.toISOString(), - messageCount, textContent, slug, customTitle, + messageCount, textContent, slug, customTitle: customTitle || aiTitle, }; } catch { return null; diff --git a/schedule-ipc.js b/schedule-ipc.js index c4cfcb7..dd838a4 100644 --- a/schedule-ipc.js +++ b/schedule-ipc.js @@ -5,6 +5,7 @@ const fs = require('fs'); const os = require('os'); const crypto = require('crypto'); const { assertPathAllowed } = require('./path-guard'); +const { encodeProjectPath } = require('./encode-project-path'); const CLAUDE_DIR = path.join(os.homedir(), '.claude'); const PROJECTS_DIR = path.join(CLAUDE_DIR, 'projects'); @@ -158,7 +159,7 @@ function init(log, runCommand) { const sessionId = crypto.randomUUID(); const msgId = crypto.randomUUID(); const timestamp = new Date().toISOString(); - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); const claudeProjectDir = path.join(PROJECTS_DIR, folder); fs.mkdirSync(claudeProjectDir, { recursive: true }); @@ -221,7 +222,7 @@ function init(log, runCommand) { return { ok: false, error: `derived project path not allowed: ${projectPath}` }; } - const folder = projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(projectPath); const schedule = { file: path.basename(filePath), filePath, projectPath, folder, diff --git a/session-cache.js b/session-cache.js index 4a306de..342eaff 100644 --- a/session-cache.js +++ b/session-cache.js @@ -4,6 +4,7 @@ const { Worker } = require('worker_threads'); const { getFolderIndexMtimeMs } = require('./folder-index-state'); const { deriveProjectPath } = require('./derive-project-path'); const { readSessionFile } = require('./read-session-file'); +const { encodeProjectPath } = require('./encode-project-path'); /** * Session cache module. @@ -170,13 +171,13 @@ function buildProjectsFromCache(showArchived) { const global = getSetting('global') || {}; const hiddenProjects = new Set(global.hiddenProjects || []); - // Group by folder (worktree sessions appear as separate projects) + // Group by folder (worktree sessions appear as separate projects). + // Only insert a project entry once we have a session that survives the + // archive filter — otherwise folders whose sessions are all archived would + // appear in the sidebar as undismissable phantom entries. const projectMap = new Map(); for (const row of cachedRows) { if (hiddenProjects.has(row.projectPath)) continue; - if (!projectMap.has(row.folder)) { - projectMap.set(row.folder, { folder: row.folder, projectPath: row.projectPath, sessions: [] }); - } const meta = metaMap.get(row.sessionId); const s = { sessionId: row.sessionId, @@ -192,6 +193,9 @@ function buildProjectsFromCache(showArchived) { archived: meta?.archived || 0, }; if (!showArchived && s.archived) continue; + if (!projectMap.has(row.folder)) { + projectMap.set(row.folder, { folder: row.folder, projectPath: row.projectPath, sessions: [] }); + } projectMap.get(row.folder).sessions.push(s); } @@ -212,7 +216,7 @@ function buildProjectsFromCache(showArchived) { // Inject active plain terminal sessions so they participate in sorting for (const [sessionId, session] of activeSessions) { if (session.exited || !session.isPlainTerminal) continue; - const folder = session.projectPath.replace(/[/_]/g, '-').replace(/^-/, '-'); + const folder = encodeProjectPath(session.projectPath); if (hiddenProjects.has(session.projectPath)) continue; if (!projectMap.has(folder)) { projectMap.set(folder, { folder, projectPath: session.projectPath, sessions: [] });