From cf6f0e7321a38b6b501ef187bd24f7504b78e7e7 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 10:49:04 +0300 Subject: [PATCH 01/11] feat: Enhance session management and tool settings for Claude and Cursor - Updated ClaudeStatus component to accept a provider prop for better flexibility. - Added CursorLogo component for displaying cursor sessions. - Modified MainContent to conditionally display session names based on provider. - Updated Shell component to show session names and summaries based on provider. - Enhanced Sidebar to handle both Claude and Cursor sessions, including sorting and displaying session icons. - Introduced new ToolsSettings functionality to manage tools for both Claude and Cursor, including allowed and disallowed commands. - Implemented fetching and saving of Cursor-specific settings and commands. - Added UI elements for managing Cursor tools, including permission settings and command lists. --- .gitignore | 25 +- package-lock.json | 1345 +++++++++++++++++++++++++++++- package.json | 4 +- public/icons/cursor.svg | 1 + server/cursor-cli.js | 250 ++++++ server/index.js | 34 +- server/routes/cursor.js | 637 ++++++++++++++ src/App.jsx | 49 +- src/components/ChatInterface.jsx | 441 +++++++++- src/components/ClaudeStatus.jsx | 2 +- src/components/CursorLogo.jsx | 9 + src/components/MainContent.jsx | 2 +- src/components/Shell.jsx | 22 +- src/components/Sidebar.jsx | 97 ++- src/components/ToolsSettings.jsx | 322 ++++++- 15 files changed, 3146 insertions(+), 94 deletions(-) create mode 100644 public/icons/cursor.svg create mode 100644 server/cursor-cli.js create mode 100644 server/routes/cursor.js create mode 100644 src/components/CursorLogo.jsx diff --git a/.gitignore b/.gitignore index bb65f13..9df63b3 100755 --- a/.gitignore +++ b/.gitignore @@ -98,10 +98,31 @@ temp/ # Local Netlify folder .netlify -# Claude specific +# AI specific .claude/ +.cursor/ +.roo/ +.taskmaster/ +.cline/ +.windsurf/ # Database files *.db *.sqlite -*.sqlite3 \ No newline at end of file +*.sqlite3 + +logs +dev-debug.log +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# OS specific + +# Task files +tasks.json +tasks/ diff --git a/package-lock.json b/package-lock.json index 12fc9c1..ce68869 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-ui", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-ui", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "dependencies": { "@codemirror/lang-css": "^6.3.1", @@ -39,6 +39,8 @@ "react-dropzone": "^14.2.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "tailwind-merge": "^3.3.1", "ws": "^8.14.2", "xterm": "^5.3.0", @@ -1002,6 +1004,13 @@ "node": ">=18" } }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, "node_modules/@img/sharp-libvips-linux-ppc64": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", @@ -1404,6 +1413,58 @@ "node": ">= 8" } }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/move-file/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1725,6 +1786,16 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1958,6 +2029,13 @@ "license": "MIT", "peer": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1992,6 +2070,46 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", @@ -2044,6 +2162,28 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/aproba": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", + "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -2348,6 +2488,138 @@ "node": ">= 0.8" } }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "optional": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2520,6 +2792,16 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2665,6 +2947,16 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2684,6 +2976,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -2727,6 +3026,13 @@ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2892,6 +3198,13 @@ "node": ">=4.0.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3011,6 +3324,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -3020,6 +3356,23 @@ "once": "^1.4.0" } }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3357,6 +3710,43 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3380,6 +3770,79 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3487,6 +3950,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "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==", + "license": "ISC", + "optional": true + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3509,6 +3979,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -3571,6 +4048,13 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3587,6 +4071,45 @@ "node": ">= 0.8" } }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -3619,6 +4142,45 @@ ], "license": "BSD-3-Clause" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3637,6 +4199,20 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -3754,6 +4330,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3817,6 +4400,13 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -4018,6 +4608,67 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/make-fetch-happen/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4749,6 +5400,207 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-fetch/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -4888,6 +5740,31 @@ } } }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -4899,6 +5776,65 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-pty": { "version": "1.1.0-beta9", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta9.tgz", @@ -4922,6 +5858,22 @@ "dev": true, "license": "MIT" }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -4941,6 +5893,23 @@ "node": ">=0.10.0" } }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4992,6 +5961,22 @@ "wrappy": "1" } }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5032,6 +6017,16 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -5293,6 +6288,27 @@ "node": ">=10" } }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5627,6 +6643,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5637,6 +6663,69 @@ "node": ">=0.10.0" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/rollup": { "version": "4.45.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz", @@ -5818,6 +6907,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -6273,6 +7369,47 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6298,6 +7435,82 @@ "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", "dev": true }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz", + "integrity": "sha512-oBkezXa2hnkfuJwUo44Hl9hS3er+YFtueifoajrgidvqsJRQFpc5fKoAkAor1O5ZnLoa28GBScfHXs8j0K358Q==", + "license": "MIT" + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/sqlite3/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ssri/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "optional": true + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -6625,6 +7838,23 @@ "node": ">=8.10.0" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tar-fs": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", @@ -6653,6 +7883,42 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6859,6 +8125,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, "node_modules/unist-util-is": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", @@ -7159,6 +8445,61 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index b665cb7..58df385 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "react-dropzone": "^14.2.3", "react-markdown": "^10.1.0", "react-router-dom": "^6.8.1", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", "tailwind-merge": "^3.3.1", "ws": "^8.14.2", "xterm": "^5.3.0", @@ -68,4 +70,4 @@ "tailwindcss": "^3.4.0", "vite": "^7.0.4" } -} \ No newline at end of file +} diff --git a/public/icons/cursor.svg b/public/icons/cursor.svg new file mode 100644 index 0000000..abadee5 --- /dev/null +++ b/public/icons/cursor.svg @@ -0,0 +1 @@ +Cursor \ No newline at end of file diff --git a/server/cursor-cli.js b/server/cursor-cli.js new file mode 100644 index 0000000..dcb14ca --- /dev/null +++ b/server/cursor-cli.js @@ -0,0 +1,250 @@ +import { spawn } from 'child_process'; +import crossSpawn from 'cross-spawn'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; + +// Use cross-spawn on Windows for better command execution +const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn; + +let activeCursorProcesses = new Map(); // Track active processes by session ID + +async function spawnCursor(command, options = {}, ws) { + return new Promise(async (resolve, reject) => { + const { sessionId, projectPath, cwd, resume, toolsSettings, skipPermissions, model, images } = options; + let capturedSessionId = sessionId; // Track session ID throughout the process + let sessionCreatedSent = false; // Track if we've already sent session-created event + let messageBuffer = ''; // Buffer for accumulating assistant messages + + // Use tools settings passed from frontend, or defaults + const settings = toolsSettings || { + allowedShellCommands: [], + skipPermissions: false + }; + + // Build Cursor CLI command + const args = []; + + // Build flags allowing both resume and prompt together (reply in existing session) + if (resume && sessionId) { + // Resume existing session + args.push('--resume=' + sessionId); + } + + if (command && command.trim()) { + // Provide a prompt (works for both new and resumed sessions) + args.push('-p', command); + + // Add model flag if specified (only meaningful for new sessions; harmless on resume) + if (!resume && model) { + args.push('--model', model); + } + + // Request streaming JSON when we are providing a prompt + args.push('--output-format', 'stream-json'); + } + + // Add skip permissions flag if enabled + if (skipPermissions || settings.skipPermissions) { + args.push('-f'); + console.log('⚠️ Using -f flag (skip permissions)'); + } + + // Use cwd (actual project directory) instead of projectPath + const workingDir = cwd || projectPath || process.cwd(); + + console.log('Spawning Cursor CLI:', 'cursor-agent', args.join(' ')); + console.log('Working directory:', workingDir); + console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume); + + const cursorProcess = spawnFunction('cursor-agent', args, { + cwd: workingDir, + stdio: ['pipe', 'pipe', 'pipe'], + env: { ...process.env } // Inherit all environment variables + }); + + // Store process reference for potential abort + const processKey = capturedSessionId || Date.now().toString(); + activeCursorProcesses.set(processKey, cursorProcess); + + // Handle stdout (streaming JSON responses) + cursorProcess.stdout.on('data', (data) => { + const rawOutput = data.toString(); + console.log('📤 Cursor CLI stdout:', rawOutput); + + const lines = rawOutput.split('\n').filter(line => line.trim()); + + for (const line of lines) { + try { + const response = JSON.parse(line); + console.log('📄 Parsed JSON response:', response); + + // Handle different message types + switch (response.type) { + case 'system': + if (response.subtype === 'init') { + // Capture session ID + if (response.session_id && !capturedSessionId) { + capturedSessionId = response.session_id; + console.log('📝 Captured session ID:', capturedSessionId); + + // Update process key with captured session ID + if (processKey !== capturedSessionId) { + activeCursorProcesses.delete(processKey); + activeCursorProcesses.set(capturedSessionId, cursorProcess); + } + + // Send session-created event only once for new sessions + if (!sessionId && !sessionCreatedSent) { + sessionCreatedSent = true; + ws.send(JSON.stringify({ + type: 'session-created', + sessionId: capturedSessionId, + model: response.model, + cwd: response.cwd + })); + } + } + + // Send system info to frontend + ws.send(JSON.stringify({ + type: 'cursor-system', + data: response + })); + } + break; + + case 'user': + // Forward user message + ws.send(JSON.stringify({ + type: 'cursor-user', + data: response + })); + break; + + case 'assistant': + // Accumulate assistant message chunks + if (response.message && response.message.content && response.message.content.length > 0) { + const textContent = response.message.content[0].text; + messageBuffer += textContent; + + // Send as Claude-compatible format for frontend + ws.send(JSON.stringify({ + type: 'claude-response', + data: { + type: 'content_block_delta', + delta: { + type: 'text_delta', + text: textContent + } + } + })); + } + break; + + case 'result': + // Session complete + console.log('Cursor session result:', response); + + // Send final message if we have buffered content + if (messageBuffer) { + ws.send(JSON.stringify({ + type: 'claude-response', + data: { + type: 'content_block_stop' + } + })); + } + + // Send completion event + ws.send(JSON.stringify({ + type: 'cursor-result', + data: response, + success: response.subtype === 'success' + })); + break; + + default: + // Forward any other message types + ws.send(JSON.stringify({ + type: 'cursor-response', + data: response + })); + } + } catch (parseError) { + console.log('📄 Non-JSON response:', line); + // If not JSON, send as raw text + ws.send(JSON.stringify({ + type: 'cursor-output', + data: line + })); + } + } + }); + + // Handle stderr + cursorProcess.stderr.on('data', (data) => { + console.error('Cursor CLI stderr:', data.toString()); + ws.send(JSON.stringify({ + type: 'cursor-error', + error: data.toString() + })); + }); + + // Handle process completion + cursorProcess.on('close', async (code) => { + console.log(`Cursor CLI process exited with code ${code}`); + + // Clean up process reference + const finalSessionId = capturedSessionId || sessionId || processKey; + activeCursorProcesses.delete(finalSessionId); + + ws.send(JSON.stringify({ + type: 'claude-complete', + exitCode: code, + isNewSession: !sessionId && !!command // Flag to indicate this was a new session + })); + + if (code === 0) { + resolve(); + } else { + reject(new Error(`Cursor CLI exited with code ${code}`)); + } + }); + + // Handle process errors + cursorProcess.on('error', (error) => { + console.error('Cursor CLI process error:', error); + + // Clean up process reference on error + const finalSessionId = capturedSessionId || sessionId || processKey; + activeCursorProcesses.delete(finalSessionId); + + ws.send(JSON.stringify({ + type: 'cursor-error', + error: error.message + })); + + reject(error); + }); + + // Close stdin since Cursor doesn't need interactive input + cursorProcess.stdin.end(); + }); +} + +function abortCursorSession(sessionId) { + const process = activeCursorProcesses.get(sessionId); + if (process) { + console.log(`🛑 Aborting Cursor session: ${sessionId}`); + process.kill('SIGTERM'); + activeCursorProcesses.delete(sessionId); + return true; + } + return false; +} + +export { + spawnCursor, + abortCursorSession +}; \ No newline at end of file diff --git a/server/index.js b/server/index.js index b6103a1..eca0f98 100755 --- a/server/index.js +++ b/server/index.js @@ -38,9 +38,11 @@ import mime from 'mime-types'; import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js'; import { spawnClaude, abortClaudeSession } from './claude-cli.js'; +import { spawnCursor, abortCursorSession } from './cursor-cli.js'; import gitRoutes from './routes/git.js'; import authRoutes from './routes/auth.js'; import mcpRoutes from './routes/mcp.js'; +import cursorRoutes from './routes/cursor.js'; import { initializeDatabase } from './database/db.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; @@ -175,6 +177,9 @@ app.use('/api/git', authenticateToken, gitRoutes); // MCP API Routes (protected) app.use('/api/mcp', authenticateToken, mcpRoutes); +// Cursor API Routes (protected) +app.use('/api/cursor', authenticateToken, cursorRoutes); + // Static files served after API routes app.use(express.static(path.join(__dirname, '../dist'))); @@ -460,12 +465,39 @@ function handleChatConnection(ws) { console.log('📁 Project:', data.options?.projectPath || 'Unknown'); console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); await spawnClaude(data.command, data.options, ws); + } else if (data.type === 'cursor-command') { + console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]'); + console.log('📁 Project:', data.options?.cwd || 'Unknown'); + console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New'); + console.log('🤖 Model:', data.options?.model || 'default'); + await spawnCursor(data.command, data.options, ws); + } else if (data.type === 'cursor-resume') { + // Backward compatibility: treat as cursor-command with resume and no prompt + console.log('🖱️ Cursor resume session (compat):', data.sessionId); + await spawnCursor('', { + sessionId: data.sessionId, + resume: true, + cwd: data.options?.cwd + }, ws); } else if (data.type === 'abort-session') { console.log('🛑 Abort session request:', data.sessionId); - const success = abortClaudeSession(data.sessionId); + const provider = data.provider || 'claude'; + const success = provider === 'cursor' + ? abortCursorSession(data.sessionId) + : abortClaudeSession(data.sessionId); ws.send(JSON.stringify({ type: 'session-aborted', sessionId: data.sessionId, + provider, + success + })); + } else if (data.type === 'cursor-abort') { + console.log('🛑 Abort Cursor session:', data.sessionId); + const success = abortCursorSession(data.sessionId); + ws.send(JSON.stringify({ + type: 'session-aborted', + sessionId: data.sessionId, + provider: 'cursor', success })); } diff --git a/server/routes/cursor.js b/server/routes/cursor.js new file mode 100644 index 0000000..6950f4a --- /dev/null +++ b/server/routes/cursor.js @@ -0,0 +1,637 @@ +import express from 'express'; +import { promises as fs } from 'fs'; +import path from 'path'; +import os from 'os'; +import { spawn } from 'child_process'; +import sqlite3 from 'sqlite3'; +import { open } from 'sqlite'; +import crypto from 'crypto'; + +const router = express.Router(); + +// GET /api/cursor/config - Read Cursor CLI configuration +router.get('/config', async (req, res) => { + try { + const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json'); + + try { + const configContent = await fs.readFile(configPath, 'utf8'); + const config = JSON.parse(configContent); + + res.json({ + success: true, + config: config, + path: configPath + }); + } catch (error) { + // Config doesn't exist or is invalid + console.log('Cursor config not found or invalid:', error.message); + + // Return default config + res.json({ + success: true, + config: { + version: 1, + model: { + modelId: "gpt-5", + displayName: "GPT-5" + }, + permissions: { + allow: [], + deny: [] + } + }, + isDefault: true + }); + } + } catch (error) { + console.error('Error reading Cursor config:', error); + res.status(500).json({ + error: 'Failed to read Cursor configuration', + details: error.message + }); + } +}); + +// POST /api/cursor/config - Update Cursor CLI configuration +router.post('/config', async (req, res) => { + try { + const { permissions, model } = req.body; + const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json'); + + // Read existing config or create default + let config = { + version: 1, + editor: { + vimMode: false + }, + hasChangedDefaultModel: false, + privacyCache: { + ghostMode: false, + privacyMode: 3, + updatedAt: Date.now() + } + }; + + try { + const existing = await fs.readFile(configPath, 'utf8'); + config = JSON.parse(existing); + } catch (error) { + // Config doesn't exist, use defaults + console.log('Creating new Cursor config'); + } + + // Update permissions if provided + if (permissions) { + config.permissions = { + allow: permissions.allow || [], + deny: permissions.deny || [] + }; + } + + // Update model if provided + if (model) { + config.model = model; + config.hasChangedDefaultModel = true; + } + + // Ensure directory exists + const configDir = path.dirname(configPath); + await fs.mkdir(configDir, { recursive: true }); + + // Write updated config + await fs.writeFile(configPath, JSON.stringify(config, null, 2)); + + res.json({ + success: true, + config: config, + message: 'Cursor configuration updated successfully' + }); + } catch (error) { + console.error('Error updating Cursor config:', error); + res.status(500).json({ + error: 'Failed to update Cursor configuration', + details: error.message + }); + } +}); + +// GET /api/cursor/mcp - Read Cursor MCP servers configuration +router.get('/mcp', async (req, res) => { + try { + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + try { + const mcpContent = await fs.readFile(mcpPath, 'utf8'); + const mcpConfig = JSON.parse(mcpContent); + + // Convert to UI-friendly format + const servers = []; + if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') { + for (const [name, config] of Object.entries(mcpConfig.mcpServers)) { + const server = { + id: name, + name: name, + type: 'stdio', + scope: 'cursor', + config: {}, + raw: config + }; + + // Determine transport type and extract config + if (config.command) { + server.type = 'stdio'; + server.config.command = config.command; + server.config.args = config.args || []; + server.config.env = config.env || {}; + } else if (config.url) { + server.type = config.transport || 'http'; + server.config.url = config.url; + server.config.headers = config.headers || {}; + } + + servers.push(server); + } + } + + res.json({ + success: true, + servers: servers, + path: mcpPath + }); + } catch (error) { + // MCP config doesn't exist + console.log('Cursor MCP config not found:', error.message); + res.json({ + success: true, + servers: [], + isDefault: true + }); + } + } catch (error) { + console.error('Error reading Cursor MCP config:', error); + res.status(500).json({ + error: 'Failed to read Cursor MCP configuration', + details: error.message + }); + } +}); + +// POST /api/cursor/mcp/add - Add MCP server to Cursor configuration +router.post('/mcp/add', async (req, res) => { + try { + const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body; + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + console.log(`➕ Adding MCP server to Cursor config: ${name}`); + + // Read existing config or create new + let mcpConfig = { mcpServers: {} }; + + try { + const existing = await fs.readFile(mcpPath, 'utf8'); + mcpConfig = JSON.parse(existing); + if (!mcpConfig.mcpServers) { + mcpConfig.mcpServers = {}; + } + } catch (error) { + console.log('Creating new Cursor MCP config'); + } + + // Build server config based on type + let serverConfig = {}; + + if (type === 'stdio') { + serverConfig = { + command: command, + args: args, + env: env + }; + } else if (type === 'http' || type === 'sse') { + serverConfig = { + url: url, + transport: type, + headers: headers + }; + } + + // Add server to config + mcpConfig.mcpServers[name] = serverConfig; + + // Ensure directory exists + const mcpDir = path.dirname(mcpPath); + await fs.mkdir(mcpDir, { recursive: true }); + + // Write updated config + await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); + + res.json({ + success: true, + message: `MCP server "${name}" added to Cursor configuration`, + config: mcpConfig + }); + } catch (error) { + console.error('Error adding MCP server to Cursor:', error); + res.status(500).json({ + error: 'Failed to add MCP server', + details: error.message + }); + } +}); + +// DELETE /api/cursor/mcp/:name - Remove MCP server from Cursor configuration +router.delete('/mcp/:name', async (req, res) => { + try { + const { name } = req.params; + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + console.log(`🗑️ Removing MCP server from Cursor config: ${name}`); + + // Read existing config + let mcpConfig = { mcpServers: {} }; + + try { + const existing = await fs.readFile(mcpPath, 'utf8'); + mcpConfig = JSON.parse(existing); + } catch (error) { + return res.status(404).json({ + error: 'Cursor MCP configuration not found' + }); + } + + // Check if server exists + if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) { + return res.status(404).json({ + error: `MCP server "${name}" not found in Cursor configuration` + }); + } + + // Remove server from config + delete mcpConfig.mcpServers[name]; + + // Write updated config + await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); + + res.json({ + success: true, + message: `MCP server "${name}" removed from Cursor configuration`, + config: mcpConfig + }); + } catch (error) { + console.error('Error removing MCP server from Cursor:', error); + res.status(500).json({ + error: 'Failed to remove MCP server', + details: error.message + }); + } +}); + +// POST /api/cursor/mcp/add-json - Add MCP server using JSON format +router.post('/mcp/add-json', async (req, res) => { + try { + const { name, jsonConfig } = req.body; + const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json'); + + console.log(`➕ Adding MCP server to Cursor config via JSON: ${name}`); + + // Validate and parse JSON config + let parsedConfig; + try { + parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig; + } catch (parseError) { + return res.status(400).json({ + error: 'Invalid JSON configuration', + details: parseError.message + }); + } + + // Read existing config or create new + let mcpConfig = { mcpServers: {} }; + + try { + const existing = await fs.readFile(mcpPath, 'utf8'); + mcpConfig = JSON.parse(existing); + if (!mcpConfig.mcpServers) { + mcpConfig.mcpServers = {}; + } + } catch (error) { + console.log('Creating new Cursor MCP config'); + } + + // Add server to config + mcpConfig.mcpServers[name] = parsedConfig; + + // Ensure directory exists + const mcpDir = path.dirname(mcpPath); + await fs.mkdir(mcpDir, { recursive: true }); + + // Write updated config + await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); + + res.json({ + success: true, + message: `MCP server "${name}" added to Cursor configuration via JSON`, + config: mcpConfig + }); + } catch (error) { + console.error('Error adding MCP server to Cursor via JSON:', error); + res.status(500).json({ + error: 'Failed to add MCP server', + details: error.message + }); + } +}); + +// GET /api/cursor/sessions - Get Cursor sessions from SQLite database +router.get('/sessions', async (req, res) => { + try { + const { projectPath } = req.query; + + // Calculate cwdID hash for the project path (Cursor uses MD5 hash) + const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); + const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId); + + console.log(`🔍 Looking for Cursor sessions in: ${cursorChatsPath}`); + + // Check if the directory exists + try { + await fs.access(cursorChatsPath); + } catch (error) { + // No sessions for this project + return res.json({ + success: true, + sessions: [], + cwdId: cwdId, + path: cursorChatsPath + }); + } + + // List all session directories + const sessionDirs = await fs.readdir(cursorChatsPath); + const sessions = []; + + for (const sessionId of sessionDirs) { + const sessionPath = path.join(cursorChatsPath, sessionId); + const storeDbPath = path.join(sessionPath, 'store.db'); + + try { + // Check if store.db exists + await fs.access(storeDbPath); + + // Open SQLite database + const db = await open({ + filename: storeDbPath, + driver: sqlite3.Database, + mode: sqlite3.OPEN_READONLY + }); + + // Get metadata from meta table + const metaRows = await db.all(` + SELECT key, value FROM meta + `); + + let sessionData = { + id: sessionId, + name: 'Untitled Session', + createdAt: null, + mode: null, + projectPath: projectPath, + lastMessage: null, + messageCount: 0 + }; + + // Parse meta table entries + for (const row of metaRows) { + if (row.value) { + try { + // Try to decode as hex-encoded JSON + const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/); + if (hexMatch) { + const jsonStr = Buffer.from(row.value, 'hex').toString('utf8'); + const data = JSON.parse(jsonStr); + + if (row.key === 'agent') { + sessionData.name = data.name || sessionData.name; + sessionData.createdAt = data.createdAt; + sessionData.mode = data.mode; + sessionData.agentId = data.agentId; + sessionData.latestRootBlobId = data.latestRootBlobId; + } + } else { + // If not hex, use raw value for simple keys + if (row.key === 'name') { + sessionData.name = row.value.toString(); + } + } + } catch (e) { + console.log(`Could not parse meta value for key ${row.key}:`, e.message); + } + } + } + + // Get message count from blobs table + try { + const blobCount = await db.get(` + SELECT COUNT(*) as count FROM blobs + `); + sessionData.messageCount = blobCount.count; + + // Get the most recent blob for preview + const lastBlob = await db.get(` + SELECT data FROM blobs + ORDER BY id DESC + LIMIT 1 + `); + + if (lastBlob && lastBlob.data) { + try { + // Try to extract readable preview from blob (may contain binary with embedded JSON) + const raw = lastBlob.data.toString('utf8'); + let preview = ''; + // Attempt direct JSON parse + try { + const parsed = JSON.parse(raw); + if (parsed?.content) { + if (Array.isArray(parsed.content)) { + const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || ''; + preview = firstText; + } else if (typeof parsed.content === 'string') { + preview = parsed.content; + } + } + } catch (_) {} + if (!preview) { + // Strip non-printable and try to find JSON chunk + const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ''); + const s = cleaned; + const start = s.indexOf('{'); + const end = s.lastIndexOf('}'); + if (start !== -1 && end > start) { + const jsonStr = s.slice(start, end + 1); + try { + const parsed = JSON.parse(jsonStr); + if (parsed?.content) { + if (Array.isArray(parsed.content)) { + const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || ''; + preview = firstText; + } else if (typeof parsed.content === 'string') { + preview = parsed.content; + } + } + } catch (_) { + preview = s; + } + } else { + preview = s; + } + } + if (preview && preview.length > 0) { + sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : ''); + } + } catch (e) { + console.log('Could not parse blob data:', e.message); + } + } + } catch (e) { + console.log('Could not read blobs:', e.message); + } + + await db.close(); + + sessions.push(sessionData); + + } catch (error) { + console.log(`Could not read session ${sessionId}:`, error.message); + } + } + + // Sort sessions by creation date (newest first) + sessions.sort((a, b) => { + if (!a.createdAt) return 1; + if (!b.createdAt) return -1; + return new Date(b.createdAt) - new Date(a.createdAt); + }); + + res.json({ + success: true, + sessions: sessions, + cwdId: cwdId, + path: cursorChatsPath + }); + + } catch (error) { + console.error('Error reading Cursor sessions:', error); + res.status(500).json({ + error: 'Failed to read Cursor sessions', + details: error.message + }); + } +}); + +// GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite +router.get('/sessions/:sessionId', async (req, res) => { + try { + const { sessionId } = req.params; + const { projectPath } = req.query; + + // Calculate cwdID hash for the project path + const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); + const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db'); + + console.log(`📖 Reading Cursor session from: ${storeDbPath}`); + + // Open SQLite database + const db = await open({ + filename: storeDbPath, + driver: sqlite3.Database, + mode: sqlite3.OPEN_READONLY + }); + + // Get all blobs (conversation data) + const blobs = await db.all(` + SELECT id, data FROM blobs + ORDER BY id ASC + `); + + // Get metadata from meta table + const metaRows = await db.all(` + SELECT key, value FROM meta + `); + + // Parse metadata + let metadata = {}; + for (const row of metaRows) { + if (row.value) { + try { + // Try to decode as hex-encoded JSON + const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/); + if (hexMatch) { + const jsonStr = Buffer.from(row.value, 'hex').toString('utf8'); + metadata[row.key] = JSON.parse(jsonStr); + } else { + metadata[row.key] = row.value.toString(); + } + } catch (e) { + metadata[row.key] = row.value.toString(); + } + } + } + + // Parse blob data to extract messages + const messages = []; + for (const blob of blobs) { + try { + // Attempt direct JSON parse first + const raw = blob.data.toString('utf8'); + let parsed; + try { + parsed = JSON.parse(raw); + } catch (_) { + // If not JSON, try to extract JSON from within binary-looking string + const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ''); + const start = cleaned.indexOf('{'); + const end = cleaned.lastIndexOf('}'); + if (start !== -1 && end > start) { + const jsonStr = cleaned.slice(start, end + 1); + try { + parsed = JSON.parse(jsonStr); + } catch (_) { + parsed = null; + } + } + } + if (parsed) { + messages.push({ id: blob.id, content: parsed }); + } else { + // Fallback to cleaned text content + const text = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '').trim(); + messages.push({ id: blob.id, content: text }); + } + } catch (e) { + messages.push({ id: blob.id, content: blob.data.toString() }); + } + } + + await db.close(); + + res.json({ + success: true, + session: { + id: sessionId, + projectPath: projectPath, + messages: messages, + metadata: metadata, + cwdId: cwdId + } + }); + + } catch (error) { + console.error('Error reading Cursor session:', error); + res.status(500).json({ + error: 'Failed to read Cursor session', + details: error.message + }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 14231f6..1cbd7eb 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -31,7 +31,7 @@ import { ThemeProvider } from './contexts/ThemeContext'; import { AuthProvider } from './contexts/AuthContext'; import ProtectedRoute from './components/ProtectedRoute'; import { useVersionCheck } from './hooks/useVersionCheck'; -import { api } from './utils/api'; +import { api, authenticatedFetch } from './utils/api'; // Main App component with routing @@ -192,6 +192,27 @@ function AppContent() { const response = await api.projects(); const data = await response.json(); + // Always fetch Cursor sessions for each project so we can combine views + for (let project of data) { + try { + const url = `/api/cursor/sessions?projectPath=${encodeURIComponent(project.fullPath || project.path)}`; + const cursorResponse = await authenticatedFetch(url); + if (cursorResponse.ok) { + const cursorData = await cursorResponse.json(); + if (cursorData.success && cursorData.sessions) { + project.cursorSessions = cursorData.sessions; + } else { + project.cursorSessions = []; + } + } else { + project.cursorSessions = []; + } + } catch (error) { + console.error(`Error fetching Cursor sessions for project ${project.name}:`, error); + project.cursorSessions = []; + } + } + // Optimize to preserve object references when data hasn't changed setProjects(prevProjects => { // If no previous projects, just set the new data @@ -210,7 +231,8 @@ function AppContent() { newProject.displayName !== prevProject.displayName || newProject.fullPath !== prevProject.fullPath || JSON.stringify(newProject.sessionMeta) !== JSON.stringify(prevProject.sessionMeta) || - JSON.stringify(newProject.sessions) !== JSON.stringify(prevProject.sessions) + JSON.stringify(newProject.sessions) !== JSON.stringify(prevProject.sessions) || + JSON.stringify(newProject.cursorSessions) !== JSON.stringify(prevProject.cursorSessions) ); }) || data.length !== prevProjects.length; @@ -236,16 +258,26 @@ function AppContent() { const shouldSwitchTab = !selectedSession || selectedSession.id !== sessionId; // Find the session across all projects for (const project of projects) { - const session = project.sessions?.find(s => s.id === sessionId); + let session = project.sessions?.find(s => s.id === sessionId); if (session) { setSelectedProject(project); - setSelectedSession(session); + setSelectedSession({ ...session, __provider: 'claude' }); // Only switch to chat tab if we're loading a different session if (shouldSwitchTab) { setActiveTab('chat'); } return; } + // Also check Cursor sessions + const cSession = project.cursorSessions?.find(s => s.id === sessionId); + if (cSession) { + setSelectedProject(project); + setSelectedSession({ ...cSession, __provider: 'cursor' }); + if (shouldSwitchTab) { + setActiveTab('chat'); + } + return; + } } // If session not found, it might be a newly created session @@ -270,6 +302,15 @@ function AppContent() { if (activeTab !== 'git' && activeTab !== 'preview') { setActiveTab('chat'); } + + // For Cursor sessions, we need to set the session ID differently + // since they're persistent and not created by Claude + const provider = localStorage.getItem('selected-provider') || 'claude'; + if (provider === 'cursor') { + // Cursor sessions have persistent IDs + sessionStorage.setItem('cursorSessionId', session.id); + } + if (isMobile) { setSidebarOpen(false); } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index d61ddd3..a7587b0 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -21,10 +21,11 @@ import ReactMarkdown from 'react-markdown'; import { useDropzone } from 'react-dropzone'; import TodoList from './TodoList'; import ClaudeLogo from './ClaudeLogo.jsx'; +import CursorLogo from './CursorLogo.jsx'; import ClaudeStatus from './ClaudeStatus'; import { MicButton } from './MicButton.jsx'; -import { api } from '../utils/api'; +import { api, authenticatedFetch } from '../utils/api'; // Safe localStorage utility to handle quota exceeded errors const safeLocalStorage = { @@ -189,11 +190,15 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile ) : (
- + {(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? ( + + ) : ( + + )}
)}
- {message.type === 'error' ? 'Error' : 'Claude'} + {message.type === 'error' ? 'Error' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')}
)} @@ -1143,6 +1148,48 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const [slashPosition, setSlashPosition] = useState(-1); const [visibleMessageCount, setVisibleMessageCount] = useState(100); const [claudeStatus, setClaudeStatus] = useState(null); + const [provider, setProvider] = useState(() => { + return localStorage.getItem('selected-provider') || 'claude'; + }); + const [cursorModel, setCursorModel] = useState(() => { + return localStorage.getItem('cursor-model') || 'gpt-5'; + }); + // When selecting a session from Sidebar, auto-switch provider to match session's origin + useEffect(() => { + if (selectedSession && selectedSession.__provider && selectedSession.__provider !== provider) { + setProvider(selectedSession.__provider); + localStorage.setItem('selected-provider', selectedSession.__provider); + } + }, [selectedSession]); + + // Load Cursor default model from config + useEffect(() => { + if (provider === 'cursor') { + fetch('/api/cursor/config', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('auth-token')}` + } + }) + .then(res => res.json()) + .then(data => { + if (data.success && data.config?.model?.modelId) { + // Map Cursor model IDs to our simplified names + const modelMap = { + 'gpt-5': 'gpt-5', + 'claude-4-sonnet': 'sonnet-4', + 'sonnet-4': 'sonnet-4', + 'claude-4-opus': 'opus-4.1', + 'opus-4.1': 'opus-4.1' + }; + const mappedModel = modelMap[data.config.model.modelId] || data.config.model.modelId; + if (!localStorage.getItem('cursor-model')) { + setCursorModel(mappedModel); + } + } + }) + .catch(err => console.error('Error loading Cursor config:', err)); + } + }, [provider]); // Memoized diff calculation to prevent recalculating on every render @@ -1184,6 +1231,97 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } }, []); + // Load Cursor session messages from SQLite via backend + const loadCursorSessionMessages = useCallback(async (projectPath, sessionId) => { + if (!projectPath || !sessionId) return []; + setIsLoadingSessionMessages(true); + try { + const url = `/api/cursor/sessions/${encodeURIComponent(sessionId)}?projectPath=${encodeURIComponent(projectPath)}`; + const res = await authenticatedFetch(url); + if (!res.ok) return []; + const data = await res.json(); + const blobs = data?.session?.messages || []; + const converted = []; + const now = Date.now(); + let idx = 0; + for (const blob of blobs) { + const content = blob.content; + let text = ''; + let role = 'assistant'; + try { + if (typeof content === 'string') { + // Attempt to extract embedded JSON first + const cleaned = content.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ''); + let extractedTexts = []; + const start = cleaned.indexOf('{'); + const end = cleaned.lastIndexOf('}'); + if (start !== -1 && end !== -1 && end > start) { + const jsonStr = cleaned.slice(start, end + 1); + try { + const parsed = JSON.parse(jsonStr); + if (parsed && parsed.content && Array.isArray(parsed.content)) { + for (const part of parsed.content) { + if (part?.type === 'text' && part.text) { + extractedTexts.push(part.text); + } + } + } + } catch (_) { + // JSON parse failed; fall back to cleaned text + } + } + if (extractedTexts.length > 0) { + extractedTexts.forEach(t => converted.push({ type: 'assistant', content: t, timestamp: new Date(now + (idx++)) })); + continue; + } + // No JSON; use cleaned readable text if any + const readable = cleaned.trim(); + if (readable) { + // Heuristic: short single token like 'hey' → user, otherwise assistant + const isLikelyUser = /^[a-zA-Z0-9.,!?\s]{1,10}$/.test(readable) && readable.toLowerCase().includes('hey'); + role = isLikelyUser ? 'user' : 'assistant'; + text = readable; + } else { + text = ''; + } + } else if (content?.message?.role && content?.message?.content) { + role = content.message.role === 'user' ? 'user' : 'assistant'; + if (Array.isArray(content.message.content)) { + text = content.message.content + .map(p => (typeof p === 'string' ? p : (p?.text || ''))) + .filter(Boolean) + .join('\n'); + } else if (typeof content.message.content === 'string') { + text = content.message.content; + } else { + text = JSON.stringify(content.message.content); + } + } else if (content?.content) { + // Some Cursor blobs may have { content: string } + text = typeof content.content === 'string' ? content.content : JSON.stringify(content.content); + } else { + text = JSON.stringify(content); + } + } catch (e) { + text = String(content); + } + if (text && text.trim()) { + converted.push({ + type: role, + content: text, + timestamp: new Date(now + (idx++)) + }); + } + } + return converted; + } catch (e) { + console.error('Error loading Cursor session messages:', e); + return []; + } finally { + setIsLoadingSessionMessages(false); + } + }, []); + // Actual diff calculation function const calculateDiff = (oldStr, newStr) => { const oldLines = oldStr.split('\n'); @@ -1349,31 +1487,47 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Load session messages when session changes const loadMessages = async () => { if (selectedSession && selectedProject) { - setCurrentSessionId(selectedSession.id); + const provider = localStorage.getItem('selected-provider') || 'claude'; - // Only load messages from API if this is a user-initiated session change - // For system-initiated changes, preserve existing messages and rely on WebSocket - if (!isSystemSessionChange) { - const messages = await loadSessionMessages(selectedProject.name, selectedSession.id); - setSessionMessages(messages); - // convertedMessages will be automatically updated via useMemo - // Scroll to bottom after loading session messages if auto-scroll is enabled - if (autoScrollToBottom) { - setTimeout(() => scrollToBottom(), 200); - } + if (provider === 'cursor') { + // For Cursor, set the session ID for resuming + setCurrentSessionId(selectedSession.id); + sessionStorage.setItem('cursorSessionId', selectedSession.id); + + // Load historical messages for Cursor session from SQLite + const projectPath = selectedProject.fullPath || selectedProject.path; + const converted = await loadCursorSessionMessages(projectPath, selectedSession.id); + setSessionMessages([]); + setChatMessages(converted); } else { - // Reset the flag after handling system session change - setIsSystemSessionChange(false); + // For Claude, load messages normally + setCurrentSessionId(selectedSession.id); + + // Only load messages from API if this is a user-initiated session change + // For system-initiated changes, preserve existing messages and rely on WebSocket + if (!isSystemSessionChange) { + const messages = await loadSessionMessages(selectedProject.name, selectedSession.id); + setSessionMessages(messages); + // convertedMessages will be automatically updated via useMemo + // Scroll to bottom after loading session messages if auto-scroll is enabled + if (autoScrollToBottom) { + setTimeout(() => scrollToBottom(), 200); + } + } else { + // Reset the flag after handling system session change + setIsSystemSessionChange(false); + } } } else { setChatMessages([]); setSessionMessages([]); setCurrentSessionId(null); + sessionStorage.removeItem('cursorSessionId'); } }; loadMessages(); - }, [selectedSession, selectedProject, loadSessionMessages, scrollToBottom, isSystemSessionChange]); + }, [selectedSession, selectedProject, loadSessionMessages, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); // Update chatMessages when convertedMessages changes useEffect(() => { @@ -1441,6 +1595,22 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'claude-response': const messageData = latestMessage.data.message || latestMessage.data; + // Handle Cursor streaming format (content_block_delta / content_block_stop) + if (messageData && typeof messageData === 'object' && messageData.type) { + if (messageData.type === 'content_block_delta' && messageData.delta?.text) { + setChatMessages(prev => [...prev, { + type: 'assistant', + content: messageData.delta.text, + timestamp: new Date() + }]); + return; + } + if (messageData.type === 'content_block_stop') { + // Nothing specific to do; leave as-is + return; + } + } + // Handle Claude CLI session duplication bug workaround: // When resuming a session, Claude CLI creates a new session instead of resuming. // We detect this by checking for system/init messages with session_id that differs @@ -1605,6 +1775,113 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }]); break; + case 'cursor-system': + // Handle Cursor system/init messages similar to Claude + try { + const cdata = latestMessage.data; + if (cdata && cdata.type === 'system' && cdata.subtype === 'init' && cdata.session_id) { + // If we already have a session and this differs, switch (duplication/redirect) + if (currentSessionId && cdata.session_id !== currentSessionId) { + console.log('🔄 Cursor session switch detected:', { originalSession: currentSessionId, newSession: cdata.session_id }); + setIsSystemSessionChange(true); + if (onNavigateToSession) { + onNavigateToSession(cdata.session_id); + } + return; + } + // If we don't yet have a session, adopt this one + if (!currentSessionId) { + console.log('🔄 Cursor new session init detected:', { newSession: cdata.session_id }); + setIsSystemSessionChange(true); + if (onNavigateToSession) { + onNavigateToSession(cdata.session_id); + } + return; + } + } + // For other cursor-system messages, avoid dumping raw objects to chat + console.log('Cursor system message:', latestMessage.data); + } catch (e) { + console.warn('Error handling cursor-system message:', e); + } + break; + + case 'cursor-user': + // Handle Cursor user messages (usually echoes) + console.log('Cursor user message:', latestMessage.data); + // Don't add user messages as they're already shown from input + break; + + case 'cursor-tool-use': + // Handle Cursor tool use messages + setChatMessages(prev => [...prev, { + type: 'assistant', + content: `Using tool: ${latestMessage.tool} ${latestMessage.input ? `with ${latestMessage.input}` : ''}`, + timestamp: new Date(), + isToolUse: true, + toolName: latestMessage.tool, + toolInput: latestMessage.input + }]); + break; + + case 'cursor-error': + // Show Cursor errors as error messages in chat + setChatMessages(prev => [...prev, { + type: 'error', + content: `Cursor error: ${latestMessage.error || 'Unknown error'}`, + timestamp: new Date() + }]); + break; + + case 'cursor-result': + // Handle Cursor completion and final result text + setIsLoading(false); + setCanAbortSession(false); + setClaudeStatus(null); + try { + const r = latestMessage.data || {}; + const textResult = typeof r.result === 'string' ? r.result : ''; + if (textResult && textResult.trim()) { + setChatMessages(prev => [...prev, { + type: r.is_error ? 'error' : 'assistant', + content: textResult, + timestamp: new Date() + }]); + } + } catch (e) { + console.warn('Error handling cursor-result message:', e); + } + + // Mark session as inactive + const cursorSessionId = currentSessionId || sessionStorage.getItem('pendingSessionId'); + if (cursorSessionId && onSessionInactive) { + onSessionInactive(cursorSessionId); + } + + // Store session ID for future use + if (cursorSessionId && !currentSessionId) { + setCurrentSessionId(cursorSessionId); + sessionStorage.removeItem('pendingSessionId'); + } + break; + + case 'cursor-output': + // Handle Cursor raw terminal output; strip ANSI and ignore empty control-only payloads + try { + const raw = String(latestMessage.data ?? ''); + const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim(); + if (cleaned) { + setChatMessages(prev => [...prev, { + type: 'assistant', + content: cleaned, + timestamp: new Date() + }]); + } + } catch (e) { + console.warn('Error handling cursor-output message:', e); + } + break; + case 'claude-complete': setIsLoading(false); setCanAbortSession(false); @@ -2027,10 +2304,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess onSessionActive(sessionToActivate); } - // Get tools settings from localStorage + // Get tools settings from localStorage based on provider const getToolsSettings = () => { try { - const savedSettings = safeLocalStorage.getItem('claude-tools-settings'); + const settingsKey = provider === 'cursor' ? 'cursor-tools-settings' : 'claude-tools-settings'; + const savedSettings = safeLocalStorage.getItem(settingsKey); if (savedSettings) { return JSON.parse(savedSettings); } @@ -2046,20 +2324,40 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const toolsSettings = getToolsSettings(); - // Send command to Claude CLI via WebSocket with images - sendMessage({ - type: 'claude-command', - command: input, - options: { - projectPath: selectedProject.path, - cwd: selectedProject.fullPath, + // Send command based on provider + if (provider === 'cursor') { + // Send Cursor command (always use cursor-command; include resume/sessionId when replying) + sendMessage({ + type: 'cursor-command', + command: input, sessionId: currentSessionId, - resume: !!currentSessionId, - toolsSettings: toolsSettings, - permissionMode: permissionMode, - images: uploadedImages // Pass images to backend - } - }); + options: { + // Prefer fullPath (actual cwd for project), fallback to path + cwd: selectedProject.fullPath || selectedProject.path, + projectPath: selectedProject.fullPath || selectedProject.path, + sessionId: currentSessionId, + resume: !!currentSessionId, + model: cursorModel, + skipPermissions: toolsSettings?.skipPermissions || false, + toolsSettings: toolsSettings + } + }); + } else { + // Send Claude command (existing code) + sendMessage({ + type: 'claude-command', + command: input, + options: { + projectPath: selectedProject.path, + cwd: selectedProject.fullPath, + sessionId: currentSessionId, + resume: !!currentSessionId, + toolsSettings: toolsSettings, + permissionMode: permissionMode, + images: uploadedImages // Pass images to backend + } + }); + } setInput(''); setAttachedImages([]); @@ -2211,7 +2509,8 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (currentSessionId && canAbortSession) { sendMessage({ type: 'abort-session', - sessionId: currentSessionId + sessionId: currentSessionId, + provider: provider }); } }; @@ -2303,10 +2602,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
-
- C +
+ {(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? ( + + ) : ( + + )}
-
Claude
+
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude'}
{/* Abort button removed - functionality not yet implemented at backend */}
@@ -2329,12 +2632,66 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
- {/* Claude Working Status - positioned above the input form */} - + {/* Provider Selection and Working Status - positioned above the input form */} +
+
+ {/* Provider & Model Selection or Fixed Provider for existing session */} +
+ {selectedSession?.__provider ? ( +
+ {selectedSession.__provider === 'cursor' ? ( + + ) : ( + + )} + {selectedSession.__provider} +
+ ) : ( + <> + + {provider === 'cursor' && ( + + )} + + )} +
+ + {/* Status Display */} +
+ +
+
+
{/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
diff --git a/src/components/ClaudeStatus.jsx b/src/components/ClaudeStatus.jsx index 52bf39d..c19e37b 100644 --- a/src/components/ClaudeStatus.jsx +++ b/src/components/ClaudeStatus.jsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from 'react'; import { cn } from '../lib/utils'; -function ClaudeStatus({ status, onAbort, isLoading }) { +function ClaudeStatus({ status, onAbort, isLoading, provider = 'claude' }) { const [elapsedTime, setElapsedTime] = useState(0); const [animationPhase, setAnimationPhase] = useState(0); const [fakeTokens, setFakeTokens] = useState(0); diff --git a/src/components/CursorLogo.jsx b/src/components/CursorLogo.jsx new file mode 100644 index 0000000..18bda9d --- /dev/null +++ b/src/components/CursorLogo.jsx @@ -0,0 +1,9 @@ +import React from 'react'; + +const CursorLogo = ({ className = 'w-5 h-5' }) => { + return ( + Cursor + ); +}; + +export default CursorLogo; diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx index 52dc236..645e84d 100644 --- a/src/components/MainContent.jsx +++ b/src/components/MainContent.jsx @@ -157,7 +157,7 @@ function MainContent({ {activeTab === 'chat' && selectedSession ? (

- {selectedSession.summary} + {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')}

{selectedProject.displayName} • {selectedSession.id} diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx index 3033b03..7864812 100644 --- a/src/components/Shell.jsx +++ b/src/components/Shell.jsx @@ -530,11 +530,16 @@ function Shell({ selectedProject, selectedSession, isActive }) {
- {selectedSession && ( - - ({selectedSession.summary.slice(0, 30)}...) - - )} + {selectedSession && (() => { + const displaySessionName = selectedSession.__provider === 'cursor' + ? (selectedSession.name || 'Untitled Session') + : (selectedSession.summary || 'New Session'); + return ( + + ({displaySessionName.slice(0, 30)}...) + + ); + })()} {!selectedSession && ( (New Session) )} @@ -601,7 +606,12 @@ function Shell({ selectedProject, selectedSession, isActive }) {

{selectedSession ? - `Resume session: ${selectedSession.summary.slice(0, 50)}...` : + (() => { + const displaySessionName = selectedSession.__provider === 'cursor' + ? (selectedSession.name || 'Untitled Session') + : (selectedSession.summary || 'New Session'); + return `Resume session: ${displaySessionName.slice(0, 50)}...`; + })() : 'Start a new Claude session' }

diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index 9a1e111..36f3bbf 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -7,6 +7,7 @@ import { Input } from './ui/input'; import { FolderOpen, Folder, Plus, MessageSquare, Clock, ChevronDown, ChevronRight, Edit3, Check, X, Trash2, Settings, FolderPlus, RefreshCw, Sparkles, Edit2, Star, Search } from 'lucide-react'; import { cn } from '../lib/utils'; import ClaudeLogo from './ClaudeLogo'; +import CursorLogo from './CursorLogo.jsx'; import { api } from '../utils/api'; // Move formatTimeAgo outside component to avoid recreation on every render @@ -202,9 +203,12 @@ function Sidebar({ // Helper function to get all sessions for a project (initial + additional) const getAllSessions = (project) => { - const initialSessions = project.sessions || []; - const additional = additionalSessions[project.name] || []; - return [...initialSessions, ...additional]; + // Combine Claude and Cursor sessions; Sidebar will display icon per row + const claudeSessions = [...(project.sessions || []), ...(additionalSessions[project.name] || [])].map(s => ({ ...s, __provider: 'claude' })); + const cursorSessions = (project.cursorSessions || []).map(s => ({ ...s, __provider: 'cursor' })); + // Sort by most recent activity/date + const normalizeDate = (s) => new Date(s.__provider === 'cursor' ? s.createdAt : s.lastActivity); + return [...claudeSessions, ...cursorSessions].sort((a, b) => normalizeDate(b) - normalizeDate(a)); }; // Helper function to get the last activity date for a project @@ -979,11 +983,19 @@ function Sidebar({
) : ( getAllSessions(project).map((session) => { + // Handle both Claude and Cursor session formats + const isCursorSession = session.__provider === 'cursor'; + // Calculate if session is active (within last 10 minutes) - const sessionDate = new Date(session.lastActivity); + const sessionDate = new Date(isCursorSession ? session.createdAt : session.lastActivity); const diffInMinutes = Math.floor((currentTime - sessionDate) / (1000 * 60)); const isActive = diffInMinutes < 10; + // Get session display values + const sessionName = isCursorSession ? (session.name || 'Untitled Session') : (session.summary || 'New Session'); + const sessionTime = isCursorSession ? session.createdAt : session.lastActivity; + const messageCount = session.messageCount || 0; + return (
{/* Active session indicator dot */} @@ -1014,38 +1026,49 @@ function Sidebar({ "w-5 h-5 rounded-md flex items-center justify-center flex-shrink-0", selectedSession?.id === session.id ? "bg-primary/10" : "bg-muted/50" )}> - + {isCursorSession ? ( + + ) : ( + + )}
- {session.summary || 'New Session'} + {sessionName}
-
+
- {formatTimeAgo(session.lastActivity, currentTime)} + {formatTimeAgo(sessionTime, currentTime)} - {session.messageCount > 0 && ( + {messageCount > 0 && ( - {session.messageCount} + {messageCount} )} + {/* Provider tiny icon */} + + {isCursorSession ? ( + + ) : ( + + )} +
- {/* Mobile delete button */} - + {/* Mobile delete button - only for Claude sessions */} + {!isCursorSession && ( + + )}
@@ -1062,26 +1085,39 @@ function Sidebar({ onTouchEnd={handleTouchClick(() => onSessionSelect(session))} >
- + {isCursorSession ? ( + + ) : ( + + )}
- {session.summary || 'New Session'} + {sessionName}
- {formatTimeAgo(session.lastActivity, currentTime)} + {formatTimeAgo(sessionTime, currentTime)} - {session.messageCount > 0 && ( + {messageCount > 0 && ( - {session.messageCount} + {messageCount} )} + {/* Provider tiny icon */} + + {isCursorSession ? ( + + ) : ( + + )} +
- {/* Desktop hover buttons */} + {/* Desktop hover buttons - only for Claude sessions */} + {!isCursorSession && (
{editingSession === session.id ? ( <> @@ -1168,6 +1204,7 @@ function Sidebar({ )}
+ )}
); diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx index 0c8eb0a..b0939c3 100644 --- a/src/components/ToolsSettings.jsx +++ b/src/components/ToolsSettings.jsx @@ -41,7 +41,16 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { const [mcpToolsLoading, setMcpToolsLoading] = useState({}); const [activeTab, setActiveTab] = useState('tools'); const [jsonValidationError, setJsonValidationError] = useState(''); - // Common tool patterns + const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor' + + // Cursor-specific states + const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]); + const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]); + const [cursorSkipPermissions, setCursorSkipPermissions] = useState(false); + const [newCursorCommand, setNewCursorCommand] = useState(''); + const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState(''); + const [cursorMcpServers, setCursorMcpServers] = useState([]); + // Common tool patterns for Claude const commonTools = [ 'Bash(git log:*)', 'Bash(git diff:*)', @@ -58,7 +67,45 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { 'WebFetch', 'WebSearch' ]; + + // Common shell commands for Cursor + const commonCursorCommands = [ + 'Shell(ls)', + 'Shell(mkdir)', + 'Shell(cd)', + 'Shell(cat)', + 'Shell(echo)', + 'Shell(git status)', + 'Shell(git diff)', + 'Shell(git log)', + 'Shell(npm install)', + 'Shell(npm run)', + 'Shell(python)', + 'Shell(node)' + ]; + // Fetch Cursor MCP servers + const fetchCursorMcpServers = async () => { + try { + const token = localStorage.getItem('auth-token'); + const response = await fetch('/api/cursor/mcp', { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (response.ok) { + const data = await response.json(); + setCursorMcpServers(data.servers || []); + } else { + console.error('Failed to fetch Cursor MCP servers'); + } + } catch (error) { + console.error('Error fetching Cursor MCP servers:', error); + } + }; + // MCP API functions const fetchMcpServers = async () => { try { @@ -268,7 +315,7 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { const loadSettings = async () => { try { - // Load from localStorage + // Load Claude settings from localStorage const savedSettings = localStorage.getItem('claude-tools-settings'); if (savedSettings) { @@ -284,9 +331,27 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { setSkipPermissions(false); setProjectSortOrder('name'); } + + // Load Cursor settings from localStorage + const savedCursorSettings = localStorage.getItem('cursor-tools-settings'); + + if (savedCursorSettings) { + const cursorSettings = JSON.parse(savedCursorSettings); + setCursorAllowedCommands(cursorSettings.allowedCommands || []); + setCursorDisallowedCommands(cursorSettings.disallowedCommands || []); + setCursorSkipPermissions(cursorSettings.skipPermissions || false); + } else { + // Set Cursor defaults + setCursorAllowedCommands([]); + setCursorDisallowedCommands([]); + setCursorSkipPermissions(false); + } // Load MCP servers from API await fetchMcpServers(); + + // Load Cursor MCP servers + await fetchCursorMcpServers(); } catch (error) { console.error('Error loading tool settings:', error); // Set defaults on error @@ -302,7 +367,8 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { setSaveStatus(null); try { - const settings = { + // Save Claude settings + const claudeSettings = { allowedTools, disallowedTools, skipPermissions, @@ -310,9 +376,17 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { lastUpdated: new Date().toISOString() }; + // Save Cursor settings + const cursorSettings = { + allowedCommands: cursorAllowedCommands, + disallowedCommands: cursorDisallowedCommands, + skipPermissions: cursorSkipPermissions, + lastUpdated: new Date().toISOString() + }; // Save to localStorage - localStorage.setItem('claude-tools-settings', JSON.stringify(settings)); + localStorage.setItem('claude-tools-settings', JSON.stringify(claudeSettings)); + localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings)); setSaveStatus('success'); @@ -635,6 +709,36 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { {activeTab === 'tools' && (
+ {/* Provider Tabs */} +
+
+ + +
+
+ + {/* Claude Tools Content */} + {toolsProvider === 'claude' && ( +
+ {/* Skip Permissions */}
@@ -1360,6 +1464,216 @@ function ToolsSettings({ isOpen, onClose, projects = [] }) { )}
)} + + {/* Cursor Tools Content */} + {toolsProvider === 'cursor' && ( +
+ + {/* Skip Permissions for Cursor */} +
+
+ +

+ Cursor Permission Settings +

+
+
+ +
+
+ + {/* Allowed Shell Commands */} +
+
+ +

+ Allowed Shell Commands +

+
+

+ Shell commands that are automatically allowed without prompting for permission +

+ +
+ setNewCursorCommand(e.target.value)} + placeholder='e.g., "Shell(ls)" or "Shell(git status)"' + onKeyPress={(e) => { + if (e.key === 'Enter') { + if (newCursorCommand && !cursorAllowedCommands.includes(newCursorCommand)) { + setCursorAllowedCommands([...cursorAllowedCommands, newCursorCommand]); + setNewCursorCommand(''); + } + } + }} + className="flex-1 h-10 touch-manipulation" + style={{ fontSize: '16px' }} + /> + +
+ + {/* Common commands quick add */} +
+

+ Quick add common commands: +

+
+ {commonCursorCommands.map(cmd => ( + + ))} +
+
+ +
+ {cursorAllowedCommands.map(cmd => ( +
+ + {cmd} + + +
+ ))} + {cursorAllowedCommands.length === 0 && ( +
+ No allowed shell commands configured +
+ )} +
+
+ + {/* Disallowed Shell Commands */} +
+
+ +

+ Disallowed Shell Commands +

+
+

+ Shell commands that should always be denied +

+ +
+ setNewCursorDisallowedCommand(e.target.value)} + placeholder='e.g., "Shell(rm -rf)" or "Shell(sudo)"' + onKeyPress={(e) => { + if (e.key === 'Enter') { + if (newCursorDisallowedCommand && !cursorDisallowedCommands.includes(newCursorDisallowedCommand)) { + setCursorDisallowedCommands([...cursorDisallowedCommands, newCursorDisallowedCommand]); + setNewCursorDisallowedCommand(''); + } + } + }} + className="flex-1 h-10 touch-manipulation" + style={{ fontSize: '16px' }} + /> + +
+ +
+ {cursorDisallowedCommands.map(cmd => ( +
+ + {cmd} + + +
+ ))} + {cursorDisallowedCommands.length === 0 && ( +
+ No disallowed shell commands configured +
+ )} +
+
+ + {/* Help Section */} +
+

+ Cursor Shell Command Examples: +

+
    +
  • "Shell(ls)" - Allow ls command
  • +
  • "Shell(git status)" - Allow git status command
  • +
  • "Shell(mkdir)" - Allow mkdir command
  • +
  • "-f" flag - Skip all permission prompts (dangerous)
  • +
+
+
+ )} +
+ )}
From 4e5aa50505999be8955cf5c2f7902c5dfb545923 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 12:10:23 +0300 Subject: [PATCH 02/11] feat: Add pagination support for session messages and enhance loading logic in ChatInterface --- package.json | 2 +- server/index.js | 18 ++++- server/projects.js | 32 +++++++-- server/routes/cursor.js | 47 ++++++++++++- src/components/ChatInterface.jsx | 116 +++++++++++++++++++++++++++---- src/utils/api.js | 12 +++- 6 files changed, 202 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 58df385..ea084d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-ui", - "version": "1.6.0", + "version": "1.6.1", "description": "A web-based UI for Claude Code CLI", "type": "module", "main": "server/index.js", diff --git a/server/index.js b/server/index.js index eca0f98..727a5ce 100755 --- a/server/index.js +++ b/server/index.js @@ -219,8 +219,22 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re app.get('/api/projects/:projectName/sessions/:sessionId/messages', authenticateToken, async (req, res) => { try { const { projectName, sessionId } = req.params; - const messages = await getSessionMessages(projectName, sessionId); - res.json({ messages }); + const { limit, offset } = req.query; + + // Parse limit and offset if provided + const parsedLimit = limit ? parseInt(limit, 10) : null; + const parsedOffset = offset ? parseInt(offset, 10) : 0; + + const result = await getSessionMessages(projectName, sessionId, parsedLimit, parsedOffset); + + // Handle both old and new response formats + if (Array.isArray(result)) { + // Backward compatibility: no pagination parameters were provided + res.json({ messages: result }); + } else { + // New format with pagination info + res.json(result); + } } catch (error) { res.status(500).json({ error: error.message }); } diff --git a/server/projects.js b/server/projects.js index 23ee462..b7189d3 100755 --- a/server/projects.js +++ b/server/projects.js @@ -385,8 +385,8 @@ async function parseJsonlSessions(filePath) { ); } -// Get messages for a specific session -async function getSessionMessages(projectName, sessionId) { +// Get messages for a specific session with pagination support +async function getSessionMessages(projectName, sessionId, limit = null, offset = 0) { const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName); try { @@ -394,7 +394,7 @@ async function getSessionMessages(projectName, sessionId) { const jsonlFiles = files.filter(file => file.endsWith('.jsonl')); if (jsonlFiles.length === 0) { - return []; + return { messages: [], total: 0, hasMore: false }; } const messages = []; @@ -423,12 +423,34 @@ async function getSessionMessages(projectName, sessionId) { } // Sort messages by timestamp - return messages.sort((a, b) => + const sortedMessages = messages.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0) ); + + const total = sortedMessages.length; + + // If no limit is specified, return all messages (backward compatibility) + if (limit === null) { + return sortedMessages; + } + + // Apply pagination - for recent messages, we need to slice from the end + // offset 0 should give us the most recent messages + const startIndex = Math.max(0, total - offset - limit); + const endIndex = total - offset; + const paginatedMessages = sortedMessages.slice(startIndex, endIndex); + const hasMore = startIndex > 0; + + return { + messages: paginatedMessages, + total, + hasMore, + offset, + limit + }; } catch (error) { console.error(`Error reading messages for session ${sessionId}:`, error); - return []; + return limit === null ? [] : { messages: [], total: 0, hasMore: false }; } } diff --git a/server/routes/cursor.js b/server/routes/cursor.js index 6950f4a..61d201e 100644 --- a/server/routes/cursor.js +++ b/server/routes/cursor.js @@ -373,11 +373,18 @@ router.get('/sessions', async (req, res) => { for (const sessionId of sessionDirs) { const sessionPath = path.join(cursorChatsPath, sessionId); const storeDbPath = path.join(sessionPath, 'store.db'); + let dbStatMtimeMs = null; try { // Check if store.db exists await fs.access(storeDbPath); + // Capture store.db mtime as a reliable fallback timestamp (last activity) + try { + const stat = await fs.stat(storeDbPath); + dbStatMtimeMs = stat.mtimeMs; + } catch (_) {} + // Open SQLite database const db = await open({ filename: storeDbPath, @@ -412,7 +419,26 @@ router.get('/sessions', async (req, res) => { if (row.key === 'agent') { sessionData.name = data.name || sessionData.name; - sessionData.createdAt = data.createdAt; + // Normalize createdAt to ISO string in milliseconds + let createdAt = data.createdAt; + if (typeof createdAt === 'number') { + if (createdAt < 1e12) { + createdAt = createdAt * 1000; // seconds -> ms + } + sessionData.createdAt = new Date(createdAt).toISOString(); + } else if (typeof createdAt === 'string') { + const n = Number(createdAt); + if (!Number.isNaN(n)) { + const ms = n < 1e12 ? n * 1000 : n; + sessionData.createdAt = new Date(ms).toISOString(); + } else { + // Assume it's already an ISO/date string + const d = new Date(createdAt); + sessionData.createdAt = isNaN(d.getTime()) ? null : d.toISOString(); + } + } else { + sessionData.createdAt = sessionData.createdAt || null; + } sessionData.mode = data.mode; sessionData.agentId = data.agentId; sessionData.latestRootBlobId = data.latestRootBlobId; @@ -497,6 +523,13 @@ router.get('/sessions', async (req, res) => { } await db.close(); + + // Finalize createdAt: use parsed meta value when valid, else fall back to store.db mtime + if (!sessionData.createdAt) { + if (dbStatMtimeMs && Number.isFinite(dbStatMtimeMs)) { + sessionData.createdAt = new Date(dbStatMtimeMs).toISOString(); + } + } sessions.push(sessionData); @@ -505,6 +538,18 @@ router.get('/sessions', async (req, res) => { } } + // Fallback: ensure createdAt is a valid ISO string (use session directory mtime as last resort) + for (const s of sessions) { + if (!s.createdAt) { + try { + const sessionDir = path.join(cursorChatsPath, s.id); + const st = await fs.stat(sessionDir); + s.createdAt = new Date(st.mtimeMs).toISOString(); + } catch { + s.createdAt = new Date().toISOString(); + } + } + } // Sort sessions by creation date (newest first) sessions.sort((a, b) => { if (!a.createdAt) return 1; diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index a7587b0..85fcbc0 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1122,6 +1122,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const [isInputFocused, setIsInputFocused] = useState(false); const [sessionMessages, setSessionMessages] = useState([]); const [isLoadingSessionMessages, setIsLoadingSessionMessages] = useState(false); + const [isLoadingMoreMessages, setIsLoadingMoreMessages] = useState(false); + const [messagesOffset, setMessagesOffset] = useState(0); + const [hasMoreMessages, setHasMoreMessages] = useState(false); + const [totalMessages, setTotalMessages] = useState(0); + const MESSAGES_PER_PAGE = 20; const [isSystemSessionChange, setIsSystemSessionChange] = useState(false); const [permissionMode, setPermissionMode] = useState('default'); const [attachedImages, setAttachedImages] = useState([]); @@ -1211,25 +1216,49 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }; }, []); - // Load session messages from API - const loadSessionMessages = useCallback(async (projectName, sessionId) => { + // Load session messages from API with pagination + const loadSessionMessages = useCallback(async (projectName, sessionId, loadMore = false) => { if (!projectName || !sessionId) return []; - setIsLoadingSessionMessages(true); + const isInitialLoad = !loadMore; + if (isInitialLoad) { + setIsLoadingSessionMessages(true); + } else { + setIsLoadingMoreMessages(true); + } + try { - const response = await api.sessionMessages(projectName, sessionId); + const currentOffset = loadMore ? messagesOffset : 0; + const response = await api.sessionMessages(projectName, sessionId, MESSAGES_PER_PAGE, currentOffset); if (!response.ok) { throw new Error('Failed to load session messages'); } const data = await response.json(); - return data.messages || []; + + // Handle paginated response + if (data.hasMore !== undefined) { + setHasMoreMessages(data.hasMore); + setTotalMessages(data.total); + setMessagesOffset(currentOffset + (data.messages?.length || 0)); + return data.messages || []; + } else { + // Backward compatibility for non-paginated response + const messages = data.messages || []; + setHasMoreMessages(false); + setTotalMessages(messages.length); + return messages; + } } catch (error) { console.error('Error loading session messages:', error); return []; } finally { - setIsLoadingSessionMessages(false); + if (isInitialLoad) { + setIsLoadingSessionMessages(false); + } else { + setIsLoadingMoreMessages(false); + } } - }, []); + }, [messagesOffset]); // Load Cursor session messages from SQLite via backend const loadCursorSessionMessages = useCallback(async (projectPath, sessionId) => { @@ -1475,13 +1504,41 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess return scrollHeight - scrollTop - clientHeight < 50; }, []); - // Handle scroll events to detect when user manually scrolls up - const handleScroll = useCallback(() => { + // Handle scroll events to detect when user manually scrolls up and load more messages + const handleScroll = useCallback(async () => { if (scrollContainerRef.current) { + const container = scrollContainerRef.current; const nearBottom = isNearBottom(); setIsUserScrolledUp(!nearBottom); + + // Check if we should load more messages (scrolled near top) + const scrolledNearTop = container.scrollTop < 100; + const provider = localStorage.getItem('selected-provider') || 'claude'; + + if (scrolledNearTop && hasMoreMessages && !isLoadingMoreMessages && selectedSession && selectedProject && provider !== 'cursor') { + // Save current scroll position + const previousScrollHeight = container.scrollHeight; + const previousScrollTop = container.scrollTop; + + // Load more messages + const moreMessages = await loadSessionMessages(selectedProject.name, selectedSession.id, true); + + if (moreMessages.length > 0) { + // Prepend new messages to the existing ones + setSessionMessages(prev => [...moreMessages, ...prev]); + + // Restore scroll position after DOM update + setTimeout(() => { + if (scrollContainerRef.current) { + const newScrollHeight = scrollContainerRef.current.scrollHeight; + const scrollDiff = newScrollHeight - previousScrollHeight; + scrollContainerRef.current.scrollTop = previousScrollTop + scrollDiff; + } + }, 0); + } + } } - }, [isNearBottom]); + }, [isNearBottom, hasMoreMessages, isLoadingMoreMessages, selectedSession, selectedProject, loadSessionMessages]); useEffect(() => { // Load session messages when session changes @@ -1489,6 +1546,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (selectedSession && selectedProject) { const provider = localStorage.getItem('selected-provider') || 'claude'; + // Reset pagination state when switching sessions + setMessagesOffset(0); + setHasMoreMessages(false); + setTotalMessages(0); + if (provider === 'cursor') { // For Cursor, set the session ID for resuming setCurrentSessionId(selectedSession.id); @@ -1500,13 +1562,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setSessionMessages([]); setChatMessages(converted); } else { - // For Claude, load messages normally + // For Claude, load messages normally with pagination setCurrentSessionId(selectedSession.id); // Only load messages from API if this is a user-initiated session change // For system-initiated changes, preserve existing messages and rely on WebSocket if (!isSystemSessionChange) { - const messages = await loadSessionMessages(selectedProject.name, selectedSession.id); + const messages = await loadSessionMessages(selectedProject.name, selectedSession.id, false); setSessionMessages(messages); // convertedMessages will be automatically updated via useMemo // Scroll to bottom after loading session messages if auto-scroll is enabled @@ -1523,11 +1585,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setSessionMessages([]); setCurrentSessionId(null); sessionStorage.removeItem('cursorSessionId'); + setMessagesOffset(0); + setHasMoreMessages(false); + setTotalMessages(0); } }; loadMessages(); - }, [selectedSession, selectedProject, loadSessionMessages, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); + }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); // Update chatMessages when convertedMessages changes useEffect(() => { @@ -2566,7 +2631,30 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
) : ( <> - {chatMessages.length > visibleMessageCount && ( + {/* Loading indicator for older messages */} + {isLoadingMoreMessages && ( +
+
+
+

Loading older messages...

+
+
+ )} + + {/* Indicator showing there are more messages to load */} + {hasMoreMessages && !isLoadingMoreMessages && ( +
+ {totalMessages > 0 && ( + + Showing {sessionMessages.length} of {totalMessages} messages • + Scroll up to load more + + )} +
+ )} + + {/* Legacy message count indicator (for non-paginated view) */} + {!hasMoreMessages && chatMessages.length > visibleMessageCount && (
Showing last {visibleMessageCount} messages ({chatMessages.length} total) •
) : ( - /* Claude/Error messages on the left */ + /* Claude/Error/Tool messages on the left */
{!isGrouped && (
@@ -188,6 +189,10 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
!
+ ) : message.type === 'tool' ? ( +
+ 🔧 +
) : (
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? ( @@ -198,7 +203,7 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
)}
- {message.type === 'error' ? 'Error' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')} + {message.type === 'error' ? 'Error' : message.type === 'tool' ? 'Tool' : ((localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? 'Cursor' : 'Claude')}
)} @@ -330,11 +335,9 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile })()} {message.toolInput && message.toolName !== 'Edit' && (() => { // Debug log to see what we're dealing with - console.log('Tool display - name:', message.toolName, 'input type:', typeof message.toolInput); // Special handling for Write tool if (message.toolName === 'Write') { - console.log('Write tool detected, toolInput:', message.toolInput); try { let input; // Handle both JSON string and already parsed object @@ -344,7 +347,6 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile input = message.toolInput; } - console.log('Parsed Write input:', input); if (input.file_path && input.content !== undefined) { return ( @@ -1003,6 +1005,20 @@ const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFile
) : (
+ {/* Thinking accordion for reasoning */} + {message.reasoning && ( +
+ + 💭 Thinking... + +
+
+ {message.reasoning} +
+
+
+ )} + {message.type === 'assistant' ? (
start) { - const jsonStr = cleaned.slice(start, end + 1); - try { - const parsed = JSON.parse(jsonStr); - if (parsed && parsed.content && Array.isArray(parsed.content)) { - for (const part of parsed.content) { - if (part?.type === 'text' && part.text) { - extractedTexts.push(part.text); + // Handle different Cursor message formats + if (content?.role && content?.content) { + // Direct format: {"role":"user","content":[{"type":"text","text":"..."}]} + // Skip system messages + if (content.role === 'system') { + continue; + } + + // Handle tool messages + if (content.role === 'tool') { + // Tool result format - find the matching tool use message and update it + if (Array.isArray(content.content)) { + for (const item of content.content) { + if (item?.type === 'tool-result') { + // Map ApplyPatch to Edit for consistency + let toolName = item.toolName || 'Unknown Tool'; + if (toolName === 'ApplyPatch') { + toolName = 'Edit'; + } + const toolCallId = item.toolCallId || content.id; + const result = item.result || ''; + + // Store the tool result to be linked later + if (toolUseMap[toolCallId]) { + toolUseMap[toolCallId].toolResult = { + content: result, + isError: false + }; + } else { + // No matching tool use found, create a standalone result message + converted.push({ + type: 'assistant', + content: '', + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id, + isToolUse: true, + toolName: toolName, + toolId: toolCallId, + toolInput: null, + toolResult: { + content: result, + isError: false + } + }); } } } - } catch (_) { - // JSON parse failed; fall back to cleaned text + } + continue; // Don't add tool messages as regular messages + } else { + // User or assistant messages + role = content.role === 'user' ? 'user' : 'assistant'; + + if (Array.isArray(content.content)) { + // Extract text, reasoning, and tool calls from content array + const textParts = []; + + for (const part of content.content) { + if (part?.type === 'text' && part?.text) { + textParts.push(part.text); + } else if (part?.type === 'reasoning' && part?.text) { + // Handle reasoning type - will be displayed in a collapsible section + reasoningText = part.text; + } else if (part?.type === 'tool-call') { + // First, add any text/reasoning we've collected so far as a message + if (textParts.length > 0 || reasoningText) { + converted.push({ + type: role, + content: textParts.join('\n'), + reasoning: reasoningText, + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id + }); + textParts.length = 0; + reasoningText = null; + } + + // Tool call in assistant message - format like Claude Code + // Map ApplyPatch to Edit for consistency with Claude Code + let toolName = part.toolName || 'Unknown Tool'; + if (toolName === 'ApplyPatch') { + toolName = 'Edit'; + } + const toolId = part.toolCallId || `tool_${blobIdx}`; + + // Create a tool use message with Claude Code format + // Map Cursor args format to Claude Code format + let toolInput = part.args; + + if (toolName === 'Edit' && part.args) { + // ApplyPatch uses 'patch' format, convert to Edit format + if (part.args.patch) { + // Parse the patch to extract old and new content + const patchLines = part.args.patch.split('\n'); + let oldLines = []; + let newLines = []; + let inPatch = false; + + for (const line of patchLines) { + if (line.startsWith('@@')) { + inPatch = true; + } else if (inPatch) { + if (line.startsWith('-')) { + oldLines.push(line.substring(1)); + } else if (line.startsWith('+')) { + newLines.push(line.substring(1)); + } else if (line.startsWith(' ')) { + // Context line - add to both + oldLines.push(line.substring(1)); + newLines.push(line.substring(1)); + } + } + } + + const filePath = part.args.file_path; + const absolutePath = filePath && !filePath.startsWith('/') + ? `${projectPath}/${filePath}` + : filePath; + toolInput = { + file_path: absolutePath, + old_string: oldLines.join('\n') || part.args.patch, + new_string: newLines.join('\n') || part.args.patch + }; + } else { + // Direct edit format + toolInput = part.args; + } + } else if (toolName === 'Read' && part.args) { + // Map 'path' to 'file_path' + // Convert relative path to absolute if needed + const filePath = part.args.path || part.args.file_path; + const absolutePath = filePath && !filePath.startsWith('/') + ? `${projectPath}/${filePath}` + : filePath; + toolInput = { + file_path: absolutePath + }; + } else if (toolName === 'Write' && part.args) { + // Map fields for Write tool + const filePath = part.args.path || part.args.file_path; + const absolutePath = filePath && !filePath.startsWith('/') + ? `${projectPath}/${filePath}` + : filePath; + toolInput = { + file_path: absolutePath, + content: part.args.contents || part.args.content + }; + } + + const toolMessage = { + type: 'assistant', + content: '', + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id, + isToolUse: true, + toolName: toolName, + toolId: toolId, + toolInput: toolInput ? JSON.stringify(toolInput) : null, + toolResult: null // Will be filled when we get the tool result + }; + converted.push(toolMessage); + toolUseMap[toolId] = toolMessage; // Store for linking results + } else if (part?.type === 'tool_use') { + // Old format support + if (textParts.length > 0 || reasoningText) { + converted.push({ + type: role, + content: textParts.join('\n'), + reasoning: reasoningText, + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id + }); + textParts.length = 0; + reasoningText = null; + } + + const toolName = part.name || 'Unknown Tool'; + const toolId = part.id || `tool_${blobIdx}`; + + const toolMessage = { + type: 'assistant', + content: '', + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id, + isToolUse: true, + toolName: toolName, + toolId: toolId, + toolInput: part.input ? JSON.stringify(part.input) : null, + toolResult: null + }; + converted.push(toolMessage); + toolUseMap[toolId] = toolMessage; + } else if (typeof part === 'string') { + textParts.push(part); + } + } + + // Add any remaining text/reasoning + if (textParts.length > 0) { + text = textParts.join('\n'); + if (reasoningText && !text) { + // Just reasoning, no text + converted.push({ + type: role, + content: '', + reasoning: reasoningText, + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id + }); + text = ''; // Clear to avoid duplicate + } + } else { + text = ''; + } + } else if (typeof content.content === 'string') { + text = content.content; } } - if (extractedTexts.length > 0) { - extractedTexts.forEach(t => converted.push({ type: 'assistant', content: t, timestamp: new Date(now + (idx++)) })); + } else if (content?.message?.role && content?.message?.content) { + // Nested message format + if (content.message.role === 'system') { continue; } - // No JSON; use cleaned readable text if any - const readable = cleaned.trim(); - if (readable) { - // Heuristic: short single token like 'hey' → user, otherwise assistant - const isLikelyUser = /^[a-zA-Z0-9.,!?\s]{1,10}$/.test(readable) && readable.toLowerCase().includes('hey'); - role = isLikelyUser ? 'user' : 'assistant'; - text = readable; - } else { - text = ''; - } - } else if (content?.message?.role && content?.message?.content) { role = content.message.role === 'user' ? 'user' : 'assistant'; if (Array.isArray(content.message.content)) { text = content.message.content @@ -1322,26 +1528,38 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess .join('\n'); } else if (typeof content.message.content === 'string') { text = content.message.content; - } else { - text = JSON.stringify(content.message.content); } - } else if (content?.content) { - // Some Cursor blobs may have { content: string } - text = typeof content.content === 'string' ? content.content : JSON.stringify(content.content); - } else { - text = JSON.stringify(content); } } catch (e) { - text = String(content); + console.log('Error parsing blob content:', e); } if (text && text.trim()) { - converted.push({ + const message = { type: role, content: text, - timestamp: new Date(now + (idx++)) - }); + timestamp: new Date(Date.now() + blobIdx), + blobId: blob.id + }; + + // Add reasoning if we have it + if (reasoningText) { + message.reasoning = reasoningText; + } + + converted.push(message); } } + + // Sort messages by blob ID to maintain chronological order + converted.sort((a, b) => { + // First sort by blobId if available + if (a.blobId && b.blobId) { + return parseInt(a.blobId) - parseInt(b.blobId); + } + // Fallback to timestamp + return new Date(a.timestamp) - new Date(b.timestamp); + }); + return converted; } catch (e) { console.error('Error loading Cursor session messages:', e); @@ -1865,7 +2083,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } } // For other cursor-system messages, avoid dumping raw objects to chat - console.log('Cursor system message:', latestMessage.data); } catch (e) { console.warn('Error handling cursor-system message:', e); } @@ -1873,7 +2090,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'cursor-user': // Handle Cursor user messages (usually echoes) - console.log('Cursor user message:', latestMessage.data); // Don't add user messages as they're already shown from input break; @@ -1994,7 +2210,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess case 'claude-status': // Handle Claude working status messages - console.log('🔔 Received claude-status message:', latestMessage); const statusData = latestMessage.data; if (statusData) { // Parse the status message to extract relevant information @@ -2025,7 +2240,6 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess statusInfo.can_interrupt = statusData.can_interrupt; } - console.log('📊 Setting claude status:', statusInfo); setClaudeStatus(statusInfo); setIsLoading(true); setCanAbortSession(statusInfo.can_interrupt); @@ -2622,12 +2836,118 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
) : chatMessages.length === 0 ? (
-
-

Start a conversation with Claude

-

- Ask questions about your code, request changes, or get help with development tasks -

-
+ {!selectedSession && ( +
+

Choose Your AI Assistant

+

+ Select a provider to start a new conversation +

+ +
+ {/* Claude Button */} + + + {/* Cursor Button */} + +
+ + {/* Model Selection for Cursor - Always reserve space to prevent jumping */} +
+ + +
+ +

+ {provider === 'claude' + ? 'Ready to use Claude AI. Start typing your message below.' + : provider === 'cursor' + ? `Ready to use Cursor with ${cursorModel}. Start typing your message below.` + : 'Select a provider above to begin' + } +

+
+ )} + {selectedSession && ( +
+

Continue your conversation

+

+ Ask questions about your code, request changes, or get help with development tasks +

+
+ )}
) : ( <> @@ -2734,6 +3054,56 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess )} {selectedSession.__provider}
+ ) : chatMessages.length === 0 ? ( +
+ + + {/* Always reserve space for model dropdown to prevent jumping */} +
+ +
+
) : ( <> - {provider === 'cursor' && ( + {/* Always reserve space for model dropdown to prevent jumping */} +
- )} +
)}
diff --git a/src/components/GitPanel.jsx b/src/components/GitPanel.jsx index 06d6217..b71d2ef 100644 --- a/src/components/GitPanel.jsx +++ b/src/components/GitPanel.jsx @@ -60,14 +60,12 @@ function GitPanel({ selectedProject, isMobile }) { const fetchGitStatus = async () => { if (!selectedProject) return; - console.log('Fetching git status for project:', selectedProject.name, 'path:', selectedProject.path); setIsLoading(true); try { const response = await authenticatedFetch(`/api/git/status?project=${encodeURIComponent(selectedProject.name)}`); const data = await response.json(); - console.log('Git status response:', data); if (data.error) { console.error('Git status error:', data.error); diff --git a/test.html b/test.html new file mode 100644 index 0000000..7f005fd --- /dev/null +++ b/test.html @@ -0,0 +1,2 @@ +world world 3 +world world 4 From 003e8f4be33f0b1eea6ae6d8b67e9c9b354fb2df Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 13:11:24 +0300 Subject: [PATCH 04/11] refactor: Simplify input area layout and remove unused provider selection components in ChatInterface --- src/components/ChatInterface.jsx | 114 +------------------------------ 1 file changed, 2 insertions(+), 112 deletions(-) diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 58f761d..bf30dfc 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -3037,120 +3037,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess {/* Input Area - Fixed Bottom */} -
- {/* Provider Selection and Working Status - positioned above the input form */} -
-
- {/* Provider & Model Selection or Fixed Provider for existing session */} -
- {selectedSession?.__provider ? ( -
- {selectedSession.__provider === 'cursor' ? ( - - ) : ( - - )} - {selectedSession.__provider} -
- ) : chatMessages.length === 0 ? ( -
- - - {/* Always reserve space for model dropdown to prevent jumping */} -
- -
-
- ) : ( - <> - - {/* Always reserve space for model dropdown to prevent jumping */} -
- -
- - )} -
- - {/* Status Display */} -
- -
-
-
+ {/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}
From cd6e5befb8d4299add6b8b3a93ca1d49f7a93744 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 13:25:32 +0300 Subject: [PATCH 05/11] feat: Add provider logos for session indication in MainContent --- src/components/MainContent.jsx | 67 ++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/src/components/MainContent.jsx b/src/components/MainContent.jsx index 645e84d..09a210a 100644 --- a/src/components/MainContent.jsx +++ b/src/components/MainContent.jsx @@ -18,6 +18,8 @@ import CodeEditor from './CodeEditor'; import Shell from './Shell'; import GitPanel from './GitPanel'; import ErrorBoundary from './ErrorBoundary'; +import ClaudeLogo from './ClaudeLogo'; +import CursorLogo from './CursorLogo'; function MainContent({ selectedProject, @@ -153,35 +155,46 @@ function MainContent({ )} -
- {activeTab === 'chat' && selectedSession ? ( -
-

- {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')} -

-
- {selectedProject.displayName} • {selectedSession.id} -
-
- ) : activeTab === 'chat' && !selectedSession ? ( -
-

- New Session -

-
- {selectedProject.displayName} -
-
- ) : ( -
-

- {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'} -

-
- {selectedProject.displayName} -
+
+ {activeTab === 'chat' && selectedSession && ( +
+ {selectedSession.__provider === 'cursor' ? ( + + ) : ( + + )}
)} +
+ {activeTab === 'chat' && selectedSession ? ( +
+

+ {selectedSession.__provider === 'cursor' ? (selectedSession.name || 'Untitled Session') : (selectedSession.summary || 'New Session')} +

+
+ {selectedProject.displayName} • {selectedSession.id} +
+
+ ) : activeTab === 'chat' && !selectedSession ? ( +
+

+ New Session +

+
+ {selectedProject.displayName} +
+
+ ) : ( +
+

+ {activeTab === 'files' ? 'Project Files' : activeTab === 'git' ? 'Source Control' : 'Project'} +

+
+ {selectedProject.displayName} +
+
+ )} +
From 3e7e60a3a85f0fd3b4d51ecae33c18550301a6e6 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 13:43:36 +0300 Subject: [PATCH 06/11] feat: Enhance session handling by adding cursor support and improving cursor messages order --- server/index.js | 43 +++++++++++++++++++++++++++++---------- server/routes/cursor.js | 14 ++++++++----- server/routes/git.js | 14 +++++++------ src/components/Shell.jsx | 1 + store.db-shm | Bin 0 -> 32768 bytes store.db-wal | 0 test.html | 3 +-- 7 files changed, 51 insertions(+), 24 deletions(-) create mode 100644 store.db-shm create mode 100644 store.db-wal diff --git a/server/index.js b/server/index.js index 727a5ce..fb617b7 100755 --- a/server/index.js +++ b/server/index.js @@ -546,14 +546,17 @@ function handleShellConnection(ws) { const projectPath = data.projectPath || process.cwd(); const sessionId = data.sessionId; const hasSession = data.hasSession; + const provider = data.provider || 'claude'; console.log('🚀 Starting shell in:', projectPath); console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : 'New session'); + console.log('🤖 Provider:', provider); // First send a welcome message + const providerName = provider === 'cursor' ? 'Cursor' : 'Claude'; const welcomeMsg = hasSession ? - `\x1b[36mResuming Claude session ${sessionId} in: ${projectPath}\x1b[0m\r\n` : - `\x1b[36mStarting new Claude session in: ${projectPath}\x1b[0m\r\n`; + `\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` : + `\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`; ws.send(JSON.stringify({ type: 'output', @@ -561,20 +564,38 @@ function handleShellConnection(ws) { })); try { - // Prepare the shell command adapted to the platform + // Prepare the shell command adapted to the platform and provider let shellCommand; - if (os.platform() === 'win32') { - if (hasSession && sessionId) { - // Try to resume session, but with fallback to new session if it fails - shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`; + if (provider === 'cursor') { + // Use cursor-agent command + if (os.platform() === 'win32') { + if (hasSession && sessionId) { + shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent --resume="${sessionId}"`; + } else { + shellCommand = `Set-Location -Path "${projectPath}"; cursor-agent`; + } } else { - shellCommand = `Set-Location -Path "${projectPath}"; claude`; + if (hasSession && sessionId) { + shellCommand = `cd "${projectPath}" && cursor-agent --resume="${sessionId}"`; + } else { + shellCommand = `cd "${projectPath}" && cursor-agent`; + } } } else { - if (hasSession && sessionId) { - shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`; + // Use claude command (default) + if (os.platform() === 'win32') { + if (hasSession && sessionId) { + // Try to resume session, but with fallback to new session if it fails + shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`; + } else { + shellCommand = `Set-Location -Path "${projectPath}"; claude`; + } } else { - shellCommand = `cd "${projectPath}" && claude`; + if (hasSession && sessionId) { + shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`; + } else { + shellCommand = `cd "${projectPath}" && claude`; + } } } diff --git a/server/routes/cursor.js b/server/routes/cursor.js index 5ff5da5..9de9951 100644 --- a/server/routes/cursor.js +++ b/server/routes/cursor.js @@ -351,7 +351,6 @@ router.get('/sessions', async (req, res) => { const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId); - console.log(`🔍 Looking for Cursor sessions in: ${cursorChatsPath}`); // Check if the directory exists try { @@ -465,7 +464,7 @@ router.get('/sessions', async (req, res) => { // Get the most recent blob for preview const lastBlob = await db.get(` SELECT data FROM blobs - ORDER BY id DESC + ORDER BY rowid DESC LIMIT 1 `); @@ -593,9 +592,10 @@ router.get('/sessions/:sessionId', async (req, res) => { }); // Get all blobs (conversation data) + // Use rowid for chronological ordering (it's an auto-incrementing integer) const blobs = await db.all(` - SELECT id, data FROM blobs - ORDER BY id ASC + SELECT rowid, id, data FROM blobs + ORDER BY rowid ASC `); // Get metadata from meta table @@ -659,7 +659,11 @@ router.get('/sessions/:sessionId', async (req, res) => { if (role === 'system') { continue; // Skip only system messages } - messages.push({ id: blob.id, content: parsed }); + messages.push({ + id: blob.id, + rowid: blob.rowid, + content: parsed + }); } // Skip non-JSON blobs (binary data) completely } catch (e) { diff --git a/server/routes/git.js b/server/routes/git.js index b56b3e4..cfc1eec 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -56,7 +56,6 @@ router.get('/status', async (req, res) => { try { const projectPath = await getActualProjectPath(project); - console.log('Git status for project:', project, '-> path:', projectPath); // Validate git repository await validateGitRepository(projectPath); @@ -136,13 +135,16 @@ router.get('/diff', async (req, res) => { lines.map(line => `+${line}`).join('\n'); } else { // Get diff for tracked files - const { stdout } = await execAsync(`git diff HEAD -- "${file}"`, { cwd: projectPath }); - diff = stdout || ''; + // First check for unstaged changes (working tree vs index) + const { stdout: unstagedDiff } = await execAsync(`git diff -- "${file}"`, { cwd: projectPath }); - // If no unstaged changes, check for staged changes - if (!diff) { + if (unstagedDiff) { + // Show unstaged changes if they exist + diff = unstagedDiff; + } else { + // If no unstaged changes, check for staged changes (index vs HEAD) const { stdout: stagedDiff } = await execAsync(`git diff --cached -- "${file}"`, { cwd: projectPath }); - diff = stagedDiff; + diff = stagedDiff || ''; } } diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx index 7864812..03d2bfd 100644 --- a/src/components/Shell.jsx +++ b/src/components/Shell.jsx @@ -436,6 +436,7 @@ function Shell({ selectedProject, selectedSession, isActive }) { projectPath: selectedProject.fullPath || selectedProject.path, sessionId: selectedSession?.id, hasSession: !!selectedSession, + provider: selectedSession?.__provider || 'claude', cols: terminal.current.cols, rows: terminal.current.rows }; diff --git a/store.db-shm b/store.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..fe9ac2845eca6fe6da8a63cd096d9cf9e24ece10 GIT binary patch literal 32768 zcmeIuAr62r3 Date: Tue, 12 Aug 2025 14:37:02 +0300 Subject: [PATCH 07/11] refactor: Improve session message handling and enhance loading logic in ChatInterface --- server/index.js | 2 +- server/routes/cursor.js | 13 +++-- server/routes/git.js | 1 - src/components/ChatInterface.jsx | 97 +++++++++++++++++++++++--------- 4 files changed, 79 insertions(+), 34 deletions(-) diff --git a/server/index.js b/server/index.js index fb617b7..ca45133 100755 --- a/server/index.js +++ b/server/index.js @@ -1067,7 +1067,7 @@ async function startServer() { console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`); // Start watching the projects folder for changes - await setupProjectsWatcher(); // Re-enabled with better-sqlite3 + await setupProjectsWatcher(); }); } catch (error) { console.error('❌ Failed to start server:', error); diff --git a/server/routes/cursor.js b/server/routes/cursor.js index 9de9951..1302784 100644 --- a/server/routes/cursor.js +++ b/server/routes/cursor.js @@ -582,7 +582,6 @@ router.get('/sessions/:sessionId', async (req, res) => { const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex'); const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db'); - console.log(`📖 Reading Cursor session from: ${storeDbPath}`); // Open SQLite database const db = await open({ @@ -592,9 +591,14 @@ router.get('/sessions/:sessionId', async (req, res) => { }); // Get all blobs (conversation data) - // Use rowid for chronological ordering (it's an auto-incrementing integer) + // Use ROW_NUMBER() for clean sequential numbering and rowid for chronological ordering const blobs = await db.all(` - SELECT rowid, id, data FROM blobs + SELECT + ROW_NUMBER() OVER (ORDER BY rowid) as sequence_num, + rowid as original_rowid, + id, + data + FROM blobs ORDER BY rowid ASC `); @@ -661,7 +665,8 @@ router.get('/sessions/:sessionId', async (req, res) => { } messages.push({ id: blob.id, - rowid: blob.rowid, + sequence: blob.sequence_num, + rowid: blob.original_rowid, content: parsed }); } diff --git a/server/routes/git.js b/server/routes/git.js index cfc1eec..e6e9b96 100755 --- a/server/routes/git.js +++ b/server/routes/git.js @@ -194,7 +194,6 @@ router.get('/branches', async (req, res) => { try { const projectPath = await getActualProjectPath(project); - console.log('Git branches for project:', project, '-> path:', projectPath); // Validate git repository await validateGitRepository(projectPath); diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index bf30dfc..51b7f77 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1330,8 +1330,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess converted.push({ type: 'assistant', content: '', - timestamp: new Date(Date.now() + blobIdx), + timestamp: new Date(Date.now() + blobIdx * 1000), blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid, isToolUse: true, toolName: toolName, toolId: toolCallId, @@ -1367,8 +1369,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess type: role, content: textParts.join('\n'), reasoning: reasoningText, - timestamp: new Date(Date.now() + blobIdx), - blobId: blob.id + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid }); textParts.length = 0; reasoningText = null; @@ -1449,8 +1453,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const toolMessage = { type: 'assistant', content: '', - timestamp: new Date(Date.now() + blobIdx), + timestamp: new Date(Date.now() + blobIdx * 1000), blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid, isToolUse: true, toolName: toolName, toolId: toolId, @@ -1466,8 +1472,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess type: role, content: textParts.join('\n'), reasoning: reasoningText, - timestamp: new Date(Date.now() + blobIdx), - blobId: blob.id + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid }); textParts.length = 0; reasoningText = null; @@ -1479,8 +1487,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const toolMessage = { type: 'assistant', content: '', - timestamp: new Date(Date.now() + blobIdx), + timestamp: new Date(Date.now() + blobIdx * 1000), blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid, isToolUse: true, toolName: toolName, toolId: toolId, @@ -1503,8 +1513,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess type: role, content: '', reasoning: reasoningText, - timestamp: new Date(Date.now() + blobIdx), - blobId: blob.id + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid }); text = ''; // Clear to avoid duplicate } @@ -1537,8 +1549,10 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const message = { type: role, content: text, - timestamp: new Date(Date.now() + blobIdx), - blobId: blob.id + timestamp: new Date(Date.now() + blobIdx * 1000), + blobId: blob.id, + sequence: blob.sequence, + rowid: blob.rowid }; // Add reasoning if we have it @@ -1550,11 +1564,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } } - // Sort messages by blob ID to maintain chronological order + // Sort messages by sequence/rowid to maintain chronological order converted.sort((a, b) => { - // First sort by blobId if available - if (a.blobId && b.blobId) { - return parseInt(a.blobId) - parseInt(b.blobId); + // First sort by sequence if available (clean 1,2,3... numbering) + if (a.sequence !== undefined && b.sequence !== undefined) { + return a.sequence - b.sequence; + } + // Then try rowid (original SQLite row IDs) + if (a.rowid !== undefined && b.rowid !== undefined) { + return a.rowid - b.rowid; } // Fallback to timestamp return new Date(a.timestamp) - new Date(b.timestamp); @@ -1774,11 +1792,18 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setCurrentSessionId(selectedSession.id); sessionStorage.setItem('cursorSessionId', selectedSession.id); - // Load historical messages for Cursor session from SQLite - const projectPath = selectedProject.fullPath || selectedProject.path; - const converted = await loadCursorSessionMessages(projectPath, selectedSession.id); - setSessionMessages([]); - setChatMessages(converted); + // Only load messages from SQLite if this is NOT a system-initiated session change + // For system-initiated changes, preserve existing messages + if (!isSystemSessionChange) { + // Load historical messages for Cursor session from SQLite + const projectPath = selectedProject.fullPath || selectedProject.path; + const converted = await loadCursorSessionMessages(projectPath, selectedSession.id); + setSessionMessages([]); + setChatMessages(converted); + } else { + // Reset the flag after handling system session change + setIsSystemSessionChange(false); + } } else { // For Claude, load messages normally with pagination setCurrentSessionId(selectedSession.id); @@ -1799,8 +1824,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess } } } else { - setChatMessages([]); - setSessionMessages([]); + // Only clear messages if this is NOT a system-initiated session change AND we're not loading + // During system session changes or while loading, preserve the chat messages + if (!isSystemSessionChange && !isLoading) { + setChatMessages([]); + setSessionMessages([]); + } setCurrentSessionId(null); sessionStorage.removeItem('cursorSessionId'); setMessagesOffset(0); @@ -1810,7 +1839,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }; loadMessages(); - }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); + }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange, isLoading]); // Update chatMessages when convertedMessages changes useEffect(() => { @@ -2152,11 +2181,23 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const raw = String(latestMessage.data ?? ''); const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim(); if (cleaned) { - setChatMessages(prev => [...prev, { - type: 'assistant', - content: cleaned, - timestamp: new Date() - }]); + setChatMessages(prev => { + // If the last message is from assistant and not a tool use, append to it + if (prev.length > 0 && prev[prev.length - 1].type === 'assistant' && !prev[prev.length - 1].isToolUse) { + const updatedMessages = [...prev]; + const lastMessage = updatedMessages[updatedMessages.length - 1]; + // Append with a newline if there's already content + lastMessage.content = lastMessage.content ? `${lastMessage.content}\n${cleaned}` : cleaned; + return updatedMessages; + } else { + // Otherwise create a new assistant message + return [...prev, { + type: 'assistant', + content: cleaned, + timestamp: new Date() + }]; + } + }); } } catch (e) { console.warn('Error handling cursor-output message:', e); From 0f4547240291685f55f17ea7d0e5ba82376cb95c Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 14:41:22 +0300 Subject: [PATCH 08/11] feat: Enhance session retrieval by implementing DAG structure for blob processing and improving JSON message extraction --- server/routes/cursor.js | 165 +++++++++++++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 38 deletions(-) diff --git a/server/routes/cursor.js b/server/routes/cursor.js index 1302784..959fbd8 100644 --- a/server/routes/cursor.js +++ b/server/routes/cursor.js @@ -590,18 +590,131 @@ router.get('/sessions/:sessionId', async (req, res) => { mode: sqlite3.OPEN_READONLY }); - // Get all blobs (conversation data) - // Use ROW_NUMBER() for clean sequential numbering and rowid for chronological ordering - const blobs = await db.all(` - SELECT - ROW_NUMBER() OVER (ORDER BY rowid) as sequence_num, - rowid as original_rowid, - id, - data - FROM blobs - ORDER BY rowid ASC + // Get all blobs to build the DAG structure + const allBlobs = await db.all(` + SELECT rowid, id, data FROM blobs `); + // Build the DAG structure from parent-child relationships + const blobMap = new Map(); // id -> blob data + const parentRefs = new Map(); // blob id -> [parent blob ids] + const childRefs = new Map(); // blob id -> [child blob ids] + const jsonBlobs = []; // Clean JSON messages + + for (const blob of allBlobs) { + blobMap.set(blob.id, blob); + + // Check if this is a JSON blob (actual message) or protobuf (DAG structure) + if (blob.data && blob.data[0] === 0x7B) { // Starts with '{' - JSON blob + try { + const parsed = JSON.parse(blob.data.toString('utf8')); + jsonBlobs.push({ ...blob, parsed }); + } catch (e) { + console.log('Failed to parse JSON blob:', blob.rowid); + } + } else if (blob.data) { // Protobuf blob - extract parent references + const parents = []; + let i = 0; + + // Scan for parent references (0x0A 0x20 followed by 32-byte hash) + while (i < blob.data.length - 33) { + if (blob.data[i] === 0x0A && blob.data[i+1] === 0x20) { + const parentHash = blob.data.slice(i+2, i+34).toString('hex'); + if (blobMap.has(parentHash)) { + parents.push(parentHash); + } + i += 34; + } else { + i++; + } + } + + if (parents.length > 0) { + parentRefs.set(blob.id, parents); + // Update child references + for (const parentId of parents) { + if (!childRefs.has(parentId)) { + childRefs.set(parentId, []); + } + childRefs.get(parentId).push(blob.id); + } + } + } + } + + // Perform topological sort to get chronological order + const visited = new Set(); + const sorted = []; + + // DFS-based topological sort + function visit(nodeId) { + if (visited.has(nodeId)) return; + visited.add(nodeId); + + // Visit all parents first (dependencies) + const parents = parentRefs.get(nodeId) || []; + for (const parentId of parents) { + visit(parentId); + } + + // Add this node after all its parents + const blob = blobMap.get(nodeId); + if (blob) { + sorted.push(blob); + } + } + + // Start with nodes that have no parents (roots) + for (const blob of allBlobs) { + if (!parentRefs.has(blob.id)) { + visit(blob.id); + } + } + + // Visit any remaining nodes (disconnected components) + for (const blob of allBlobs) { + visit(blob.id); + } + + // Now extract JSON messages in the order they appear in the sorted DAG + const messageOrder = new Map(); // JSON blob id -> order index + let orderIndex = 0; + + for (const blob of sorted) { + // Check if this blob references any JSON messages + if (blob.data && blob.data[0] !== 0x7B) { // Protobuf blob + // Look for JSON blob references + for (const jsonBlob of jsonBlobs) { + try { + const jsonIdBytes = Buffer.from(jsonBlob.id, 'hex'); + if (blob.data.includes(jsonIdBytes)) { + if (!messageOrder.has(jsonBlob.id)) { + messageOrder.set(jsonBlob.id, orderIndex++); + } + } + } catch (e) { + // Skip if can't convert ID + } + } + } + } + + // Sort JSON blobs by their appearance order in the DAG + const sortedJsonBlobs = jsonBlobs.sort((a, b) => { + const orderA = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER; + const orderB = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER; + if (orderA !== orderB) return orderA - orderB; + // Fallback to rowid if not in order map + return a.rowid - b.rowid; + }); + + // Use sorted JSON blobs + const blobs = sortedJsonBlobs.map((blob, idx) => ({ + ...blob, + sequence_num: idx + 1, + original_rowid: blob.rowid + })); + // Get metadata from meta table const metaRows = await db.all(` SELECT key, value FROM meta @@ -626,37 +739,14 @@ router.get('/sessions/:sessionId', async (req, res) => { } } - // Parse blob data to extract messages - only include blobs with valid JSON + // Extract messages from sorted JSON blobs const messages = []; for (const blob of blobs) { try { - // Attempt direct JSON parse first - const raw = blob.data.toString('utf8'); - let parsed; - let isValidJson = false; + // We already parsed JSON blobs earlier + const parsed = blob.parsed; - try { - parsed = JSON.parse(raw); - isValidJson = true; - } catch (_) { - // If not JSON, try to extract JSON from within binary-looking string - const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, ''); - const start = cleaned.indexOf('{'); - const end = cleaned.lastIndexOf('}'); - if (start !== -1 && end > start) { - const jsonStr = cleaned.slice(start, end + 1); - try { - parsed = JSON.parse(jsonStr); - isValidJson = true; - } catch (_) { - parsed = null; - isValidJson = false; - } - } - } - - // Only include blobs that contain valid JSON data - if (isValidJson && parsed) { + if (parsed) { // Filter out ONLY system messages at the server level // Check both direct role and nested message.role const role = parsed?.role || parsed?.message?.role; @@ -670,7 +760,6 @@ router.get('/sessions/:sessionId', async (req, res) => { content: parsed }); } - // Skip non-JSON blobs (binary data) completely } catch (e) { // Skip blobs that cause errors console.log(`Skipping blob ${blob.id}: ${e.message}`); From cdce59edb49db87dc88a4ea629dc91f979d82d09 Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 14:45:07 +0300 Subject: [PATCH 09/11] feat: Update message count retrieval to count only JSON blobs in sessions --- server/routes/cursor.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/server/routes/cursor.js b/server/routes/cursor.js index 959fbd8..5f7e873 100644 --- a/server/routes/cursor.js +++ b/server/routes/cursor.js @@ -454,16 +454,19 @@ router.get('/sessions', async (req, res) => { } } - // Get message count from blobs table + // Get message count from JSON blobs only (actual messages, not DAG structure) try { const blobCount = await db.get(` - SELECT COUNT(*) as count FROM blobs + SELECT COUNT(*) as count + FROM blobs + WHERE substr(data, 1, 1) = X'7B' `); sessionData.messageCount = blobCount.count; - // Get the most recent blob for preview + // Get the most recent JSON blob for preview (actual message, not DAG structure) const lastBlob = await db.get(` SELECT data FROM blobs + WHERE substr(data, 1, 1) = X'7B' ORDER BY rowid DESC LIMIT 1 `); From 50f6cdfac9273256adea90db44b060abf48c7a8f Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 14:48:47 +0300 Subject: [PATCH 10/11] feat: Enhance chat message handling by appending assistant messages and triggering project refresh on session updates --- src/components/ChatInterface.jsx | 36 +++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 51b7f77..57081fb 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1910,11 +1910,23 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Handle Cursor streaming format (content_block_delta / content_block_stop) if (messageData && typeof messageData === 'object' && messageData.type) { if (messageData.type === 'content_block_delta' && messageData.delta?.text) { - setChatMessages(prev => [...prev, { - type: 'assistant', - content: messageData.delta.text, - timestamp: new Date() - }]); + setChatMessages(prev => { + // Check if the last message is an assistant message we can append to + if (prev.length > 0 && prev[prev.length - 1].type === 'assistant' && !prev[prev.length - 1].isToolUse) { + // Append to the last assistant message + const updatedMessages = [...prev]; + const lastMessage = updatedMessages[updatedMessages.length - 1]; + lastMessage.content = (lastMessage.content || '') + messageData.delta.text; + return updatedMessages; + } else { + // Create a new assistant message for the first delta + return [...prev, { + type: 'assistant', + content: messageData.delta.text, + timestamp: new Date() + }]; + } + }); return; } if (messageData.type === 'content_block_stop') { @@ -2168,10 +2180,15 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess onSessionInactive(cursorSessionId); } - // Store session ID for future use + // Store session ID for future use and trigger refresh if (cursorSessionId && !currentSessionId) { setCurrentSessionId(cursorSessionId); sessionStorage.removeItem('pendingSessionId'); + + // Trigger a project refresh to update the sidebar with the new session + if (window.refreshProjects) { + setTimeout(() => window.refreshProjects(), 500); + } } break; @@ -2223,6 +2240,11 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess if (pendingSessionId && !currentSessionId && latestMessage.exitCode === 0) { setCurrentSessionId(pendingSessionId); sessionStorage.removeItem('pendingSessionId'); + + // Trigger a project refresh to update the sidebar with the new session + if (window.refreshProjects) { + setTimeout(() => window.refreshProjects(), 500); + } } // Clear persisted chat messages after successful completion @@ -2877,7 +2899,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess
) : chatMessages.length === 0 ? (
- {!selectedSession && ( + {!selectedSession && !currentSessionId && (

Choose Your AI Assistant

From db7ce4dd74386eebfe66308ee044a9f7c8fb994d Mon Sep 17 00:00:00 2001 From: simos Date: Tue, 12 Aug 2025 15:05:36 +0300 Subject: [PATCH 11/11] feat: Update README to include Cursor CLI support and enhance chat message handling with streaming improvements --- README.md | 23 +++- public/screenshots/cli-selection.png | Bin 0 -> 174946 bytes server/cursor-cli.js | 6 +- src/components/ChatInterface.jsx | 189 +++++++++++++++++++-------- 4 files changed, 152 insertions(+), 66 deletions(-) create mode 100644 public/screenshots/cli-selection.png diff --git a/README.md b/README.md index ad33afe..fcb505c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

-A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), Anthropic's official CLI for AI-assisted coding. You can use it locally or remotely to view your active projects and sessions in claude code and make changes to them the same way you would do it in claude code CLI. This gives you a proper interface that works everywhere. +A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), and [Cursor CLI](https://docs.cursor.com/en/cli/overview). You can use it locally or remotely to view your active projects and sessions in Claude Code or Cursor and make changes to them from everywhere (mobile or desktop). This gives you a proper interface that works everywhere. Supports models including **Claude Sonnet 4**, **Opus 4.1**, and **GPT-5** ## Screenshots @@ -25,6 +25,14 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla Responsive mobile design with touch navigation + + +

CLI Selection

+CLI Selection +
+Select between Claude Code and Cursor CLI + + @@ -34,11 +42,12 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla ## Features - **Responsive Design** - Works seamlessly across desktop, tablet, and mobile so you can also use Claude Code from mobile -- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code -- **Integrated Shell Terminal** - Direct access to Claude Code CLI through built-in shell functionality +- **Interactive Chat Interface** - Built-in chat interface for seamless communication with Claude Code or Cursor +- **Integrated Shell Terminal** - Direct access to Claude Code or Cursor CLI through built-in shell functionality - **File Explorer** - Interactive file tree with syntax highlighting and live editing - **Git Explorer** - View, stage and commit your changes. You can also switch branches - **Session Management** - Resume conversations, manage multiple sessions, and track history +- **Model Compatibility** - Works with Claude Sonnet 4, Opus 4.1, and GPT-5 ## Quick Start @@ -46,7 +55,8 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla ### Prerequisites - [Node.js](https://nodejs.org/) v20 or higher -- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured +- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or +- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured ### Installation @@ -108,9 +118,10 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a - **Visual Project Browser** - All available projects with metadata and session counts - **Project Actions** - Rename, delete, and organize projects - **Smart Navigation** - Quick access to recent projects and sessions +- **MCP support** - Add your own MCP servers through the UI #### Chat Interface -- **Use responsive chat or Claude Code CLI** - You can either use the adapted chat interface or use the shell button to connect to Claude Code CLI. +- **Use responsive chat or Claude Code/Cursor CLI** - You can either use the adapted chat interface or use the shell button to connect to your selected CLI. - **Real-time Communication** - Stream responses from Claude with WebSocket connection - **Session Management** - Resume previous conversations or start fresh sessions - **Message History** - Complete conversation history with timestamps and metadata @@ -152,7 +163,7 @@ The UI automatically discovers Claude Code projects from `~/.claude/projects/` a ### Backend (Node.js + Express) - **Express Server** - RESTful API with static file serving - **WebSocket Server** - Communication for chats and project refresh -- **Claude CLI Integration** - Process spawning and management +- **CLI Integration (Claude Code / Cursor)** - Process spawning and management - **Session Management** - JSONL parsing and conversation persistence - **File System API** - Exposing file browser for projects diff --git a/public/screenshots/cli-selection.png b/public/screenshots/cli-selection.png new file mode 100644 index 0000000000000000000000000000000000000000..507cfcee6ea183690bbab7145130cbde04d9bdfd GIT binary patch literal 174946 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 literal 0 HcmV?d00001 diff --git a/server/cursor-cli.js b/server/cursor-cli.js index dcb14ca..be471f9 100644 --- a/server/cursor-cli.js +++ b/server/cursor-cli.js @@ -26,8 +26,8 @@ async function spawnCursor(command, options = {}, ws) { const args = []; // Build flags allowing both resume and prompt together (reply in existing session) - if (resume && sessionId) { - // Resume existing session + // Treat presence of sessionId as intention to resume, regardless of resume flag + if (sessionId) { args.push('--resume=' + sessionId); } @@ -36,7 +36,7 @@ async function spawnCursor(command, options = {}, ws) { args.push('-p', command); // Add model flag if specified (only meaningful for new sessions; harmless on resume) - if (!resume && model) { + if (!sessionId && model) { args.push('--model', model); } diff --git a/src/components/ChatInterface.jsx b/src/components/ChatInterface.jsx index 57081fb..e8cec93 100644 --- a/src/components/ChatInterface.jsx +++ b/src/components/ChatInterface.jsx @@ -1151,6 +1151,9 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const messagesEndRef = useRef(null); const textareaRef = useRef(null); const scrollContainerRef = useRef(null); + // Streaming throttle buffers + const streamBufferRef = useRef(''); + const streamTimerRef = useRef(null); const [debouncedInput, setDebouncedInput] = useState(''); const [showFileDropdown, setShowFileDropdown] = useState(false); const [fileList, setFileList] = useState([]); @@ -1839,7 +1842,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess }; loadMessages(); - }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange, isLoading]); + }, [selectedSession, selectedProject, loadCursorSessionMessages, scrollToBottom, isSystemSessionChange]); // Update chatMessages when convertedMessages changes useEffect(() => { @@ -1910,27 +1913,56 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess // Handle Cursor streaming format (content_block_delta / content_block_stop) if (messageData && typeof messageData === 'object' && messageData.type) { if (messageData.type === 'content_block_delta' && messageData.delta?.text) { - setChatMessages(prev => { - // Check if the last message is an assistant message we can append to - if (prev.length > 0 && prev[prev.length - 1].type === 'assistant' && !prev[prev.length - 1].isToolUse) { - // Append to the last assistant message - const updatedMessages = [...prev]; - const lastMessage = updatedMessages[updatedMessages.length - 1]; - lastMessage.content = (lastMessage.content || '') + messageData.delta.text; - return updatedMessages; - } else { - // Create a new assistant message for the first delta - return [...prev, { - type: 'assistant', - content: messageData.delta.text, - timestamp: new Date() - }]; - } - }); + // Buffer deltas and flush periodically to reduce rerenders + streamBufferRef.current += messageData.delta.text; + if (!streamTimerRef.current) { + streamTimerRef.current = setTimeout(() => { + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + streamTimerRef.current = null; + if (!chunk) return; + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = (last.content || '') + chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + }, 100); + } return; } if (messageData.type === 'content_block_stop') { - // Nothing specific to do; leave as-is + // Flush any buffered text and mark streaming message complete + if (streamTimerRef.current) { + clearTimeout(streamTimerRef.current); + streamTimerRef.current = null; + } + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + if (chunk) { + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = (last.content || '') + chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + } + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && last.isStreaming) { + last.isStreaming = false; + } + return updated; + }); return; } } @@ -2075,11 +2107,30 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess break; case 'claude-output': - setChatMessages(prev => [...prev, { - type: 'assistant', - content: latestMessage.data, - timestamp: new Date() - }]); + { + const cleaned = String(latestMessage.data || ''); + if (cleaned.trim()) { + streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned); + if (!streamTimerRef.current) { + streamTimerRef.current = setTimeout(() => { + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + streamTimerRef.current = null; + if (!chunk) return; + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = last.content ? `${last.content}\n${chunk}` : chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + }, 100); + } + } + } break; case 'claude-interactive-prompt': // Handle interactive prompts from CLI @@ -2163,13 +2214,28 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess try { const r = latestMessage.data || {}; const textResult = typeof r.result === 'string' ? r.result : ''; - if (textResult && textResult.trim()) { - setChatMessages(prev => [...prev, { - type: r.is_error ? 'error' : 'assistant', - content: textResult, - timestamp: new Date() - }]); + // Flush buffered deltas before finalizing + if (streamTimerRef.current) { + clearTimeout(streamTimerRef.current); + streamTimerRef.current = null; } + const pendingChunk = streamBufferRef.current; + streamBufferRef.current = ''; + + setChatMessages(prev => { + const updated = [...prev]; + // Try to consolidate into the last streaming assistant message + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + // Replace streaming content with the final content so deltas don't remain + const finalContent = textResult && textResult.trim() ? textResult : (last.content || '') + (pendingChunk || ''); + last.content = finalContent; + last.isStreaming = false; + } else if (textResult && textResult.trim()) { + updated.push({ type: r.is_error ? 'error' : 'assistant', content: textResult, timestamp: new Date(), isStreaming: false }); + } + return updated; + }); } catch (e) { console.warn('Error handling cursor-result message:', e); } @@ -2198,23 +2264,25 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess const raw = String(latestMessage.data ?? ''); const cleaned = raw.replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '').trim(); if (cleaned) { - setChatMessages(prev => { - // If the last message is from assistant and not a tool use, append to it - if (prev.length > 0 && prev[prev.length - 1].type === 'assistant' && !prev[prev.length - 1].isToolUse) { - const updatedMessages = [...prev]; - const lastMessage = updatedMessages[updatedMessages.length - 1]; - // Append with a newline if there's already content - lastMessage.content = lastMessage.content ? `${lastMessage.content}\n${cleaned}` : cleaned; - return updatedMessages; - } else { - // Otherwise create a new assistant message - return [...prev, { - type: 'assistant', - content: cleaned, - timestamp: new Date() - }]; - } - }); + streamBufferRef.current += (streamBufferRef.current ? `\n${cleaned}` : cleaned); + if (!streamTimerRef.current) { + streamTimerRef.current = setTimeout(() => { + const chunk = streamBufferRef.current; + streamBufferRef.current = ''; + streamTimerRef.current = null; + if (!chunk) return; + setChatMessages(prev => { + const updated = [...prev]; + const last = updated[updated.length - 1]; + if (last && last.type === 'assistant' && !last.isToolUse && last.isStreaming) { + last.content = last.content ? `${last.content}\n${chunk}` : chunk; + } else { + updated.push({ type: 'assistant', content: chunk, timestamp: new Date(), isStreaming: true }); + } + return updated; + }); + }, 100); + } } } catch (e) { console.warn('Error handling cursor-output message:', e); @@ -2636,12 +2704,12 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess setIsUserScrolledUp(false); // Reset scroll state so auto-scroll works for Claude's response setTimeout(() => scrollToBottom(), 100); // Longer delay to ensure message is rendered + // Determine effective session id for replies to avoid race on state updates + const effectiveSessionId = currentSessionId || selectedSession?.id || sessionStorage.getItem('cursorSessionId'); + // Session Protection: Mark session as active to prevent automatic project updates during conversation - // This is crucial for maintaining chat state integrity. We handle two cases: - // 1. Existing sessions: Use the real currentSessionId - // 2. New sessions: Generate temporary identifier "new-session-{timestamp}" since real ID comes via WebSocket later - // This ensures no gap in protection between message send and session creation - const sessionToActivate = currentSessionId || `new-session-${Date.now()}`; + // Use existing session if available; otherwise a temporary placeholder until backend provides real ID + const sessionToActivate = effectiveSessionId || `new-session-${Date.now()}`; if (onSessionActive) { onSessionActive(sessionToActivate); } @@ -2672,13 +2740,13 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess sendMessage({ type: 'cursor-command', command: input, - sessionId: currentSessionId, + sessionId: effectiveSessionId, options: { // Prefer fullPath (actual cwd for project), fallback to path cwd: selectedProject.fullPath || selectedProject.path, projectPath: selectedProject.fullPath || selectedProject.path, - sessionId: currentSessionId, - resume: !!currentSessionId, + sessionId: effectiveSessionId, + resume: !!effectiveSessionId, model: cursorModel, skipPermissions: toolsSettings?.skipPermissions || false, toolsSettings: toolsSettings @@ -3073,7 +3141,7 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess

-
+
{(localStorage.getItem('selected-provider') || 'claude') === 'cursor' ? ( ) : ( @@ -3104,7 +3172,14 @@ function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, mess isInputFocused ? 'pb-2 sm:pb-4 md:pb-6' : 'pb-16 sm:pb-4 md:pb-6' }`}> - +
+ +
{/* Permission Mode Selector with scroll to bottom button - Above input, clickable for mobile */}