From 0535b150e4c57a313c5c76d529238295611c152b Mon Sep 17 00:00:00 2001 From: shookapic Date: Tue, 8 Sep 2026 14:42:04 +0200 Subject: [PATCH 01/66] feat(desktop): establish Tauri and React application foundation --- .gitignore | 7 +- desktop/main.tsx | 3 + docs/electron-package.json | 103 + docs/tauri-migration.md | 38 + index.html | 2 + package-lock.json | 7187 +++++++++--------------------- package.json | 121 +- src-tauri/Cargo.toml | 29 + src-tauri/build.rs | 1 + src-tauri/capabilities/main.json | 1 + src-tauri/src/lib.rs | 5 + src-tauri/src/main.rs | 2 + src-tauri/tauri.conf.json | 12 + tsconfig.json | 10 + vite.config.ts | 8 + vitest.config.ts | 3 + 16 files changed, 2446 insertions(+), 5086 deletions(-) create mode 100644 desktop/main.tsx create mode 100644 docs/electron-package.json create mode 100644 docs/tauri-migration.md create mode 100644 index.html create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/main.json create mode 100644 src-tauri/src/lib.rs create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 tsconfig.json create mode 100644 vite.config.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore index 9bec827..7e928a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,9 @@ node_modules .claw-cache .claw-logs ctrace-report.txt -bundle.js \ No newline at end of file +bundle.js +src-tauri/target/ +src-tauri/gen/ +coverage/ +*.tsbuildinfo + diff --git a/desktop/main.tsx b/desktop/main.tsx new file mode 100644 index 0000000..a858176 --- /dev/null +++ b/desktop/main.tsx @@ -0,0 +1,3 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +createRoot(document.getElementById('root')!).render(
CoreTrace Desktop
); diff --git a/docs/electron-package.json b/docs/electron-package.json new file mode 100644 index 0000000..603a480 --- /dev/null +++ b/docs/electron-package.json @@ -0,0 +1,103 @@ +{ + "name": "ctrace-gui", + "version": "5.1.0", + "description": "A modern Electron-based GUI for CTrace static analysis tool", + "main": "src/main.js", + "scripts": { + "build:renderer": "node scripts/build-renderer-bundle.js", + "build:renderer:dev": "node scripts/build-renderer-bundle.js --dev", + "prestart": "npm run build:renderer:dev", + "predist": "npm run build:renderer", + "predist:publish": "npm run build:renderer", + "predist:win": "npm run build:renderer", + "predist:mac": "npm run build:renderer", + "predist:linux": "npm run build:renderer", + "test": "node --test", + "test:wsl": "node src/main/utils/wslTest.js", + "start": "electron .", + "release:ship": "bash ./scripts/release.sh", + "dist": "electron-builder", + "dist:publish": "electron-builder --publish always", + "dist:win": "electron-builder --win", + "dist:mac": "electron-builder --mac", + "dist:linux": "electron-builder --linux", + "build:verify": "./scripts/build-verify.sh", + "docs": "jsdoc -c jsdoc.conf.json", + "docs:watch": "jsdoc -c jsdoc.conf.json --watch", + "postinstall": "node scripts/patch-node-pty-spectre.js" + }, + "keywords": [ + "ctrace", + "static-analysis", + "cpp", + "c", + "development", + "gui", + "electron" + ], + "author": "CTrace GUI Team", + "license": "Apache-2.0", + "dependencies": { + "chokidar": "^5.0.0", + "electron-updater": "^6.8.9", + "ignore": "^7.0.5", + "monaco-editor": "^0.52.2", + "node-llama-cpp": "^3.18.1", + "node-pty": "^1.1.0", + "tar": "^7.5.22", + "uuid": "^14.0.0", + "@coretrace/cli-core": "file:../coretrace-cli/packages/cli-core" + }, + "devDependencies": { + "electron": "^41.10.7", + "electron-builder": "^26.8.1", + "electron-reload": "^2.0.0-alpha.1", + "esbuild": "^0.28.0", + "jsdoc": "^4.0.4" + }, + "build": { + "publish": [ + { + "provider": "github", + "owner": "CoreTrace", + "repo": "coretrace-gui", + "releaseType": "release" + } + ], + "asar": true, + "extraResources": [ + { + "from": "bin/ctrace", + "to": "bin/ctrace" + } + ], + "asarUnpack": [], + "afterPack": "./scripts/remove-locales.js", + "appId": "com.coretrace.ctracegui", + "compression": "normal", + "productName": "CtraceGUI", + "win": { + "target": "nsis", + "icon": "assets/ctrace.ico" + }, + "mac": { + "target": [ + "dmg", + "zip" + ], + "icon": "assets/ctrace.icns", + "category": "public.app-category.developer-tools", + "identity": null, + "hardenedRuntime": false, + "gatekeeperAssess": false, + "notarize": false + }, + "linux": { + "target": "AppImage", + "icon": "assets/ctrace.png", + "category": "Development", + "synopsis": "Static analysis tool for C/C++ code", + "description": "A modern Electron-based GUI application that provides an intuitive interface for running CTrace static analysis on C/C++ code." + } + } +} diff --git a/docs/tauri-migration.md b/docs/tauri-migration.md new file mode 100644 index 0000000..517939d --- /dev/null +++ b/docs/tauri-migration.md @@ -0,0 +1,38 @@ +# Tauri desktop migration + +## Scope and acceptance + +Replace the default Electron application with Tauri 2 (Rust) and React/TypeScript. +Deliver a desktop sidebar, personal and organisation dashboards using real platform data, +device sign-in, organisation switching, GitHub repository loading, a local folder explorer +and editable Monaco documents, and local/cloud analysis execution with visible results. + +API ownership remains in Coretrace-Entreprise/coretrace-control. Contracts inspected: +`openapi/openapi.yaml`, coretrace-cli device authentication and coretrace-web dashboard +and analysis helpers. Remote repository analyses use the SCM endpoint; local analyses +use an explicitly selected installed ctrace executable. Repository code is never executed +on opening or cloning. Cloud actions that spend CTU require an explicit user action. + +## Migration and rollback + +Base: `807e2c9`. The Electron source and tests remain as migration reference; its package +manifest is saved alongside this document. Tauri uses `desktop/` and `src-tauri/`, its own +application identifier and OS credential entry. Existing Electron settings and encrypted +credentials are not rewritten or implicitly imported. Sign in once in the new application. +Rollback by checking out the base revision in a separate clean worktree and installing its +dependencies. Never reset over user changes. The pre-existing `native/` directory and ATP +archive modification belong to the user and are excluded from migration commits. + +## Stages + +1. Tauri/React build foundation and migration contract. +2. Native workspace, GitHub, authentication and analysis services. +3. Desktop UI, dashboards, IDE and analysis flows. +4. Regression checks, desktop build, CI and operating documentation. + +The previous terminal, WSL manager, local LLM integrations, extension experiments and +Electron updater are not part of the requested replacement. They remain in the old source. +No production deployment, paid analysis or GitHub publication is performed during validation. + +References: https://v2.tauri.app/start/frontend/vite/ and +https://v2.tauri.app/plugin/dialog/. diff --git a/index.html b/index.html new file mode 100644 index 0000000..9ee82f2 --- /dev/null +++ b/index.html @@ -0,0 +1,2 @@ + +CoreTrace
diff --git a/package-lock.json b/package-lock.json index 57424e3..287d074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,36 +1,38 @@ { "name": "ctrace-gui", - "version": "5.1.0", + "version": "6.0.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ctrace-gui", - "version": "5.1.0", - "hasInstallScript": true, + "version": "6.0.0-beta.1", "license": "Apache-2.0", "dependencies": { - "@coretrace/cli-core": "file:../coretrace-cli/packages/cli-core", - "chokidar": "^5.0.0", - "electron-updater": "^6.8.9", - "ignore": "^7.0.5", + "@monaco-editor/react": "^4.7.0", + "@tauri-apps/api": "^2.8.0", + "lucide-react": "^0.468.0", "monaco-editor": "^0.52.2", - "node-llama-cpp": "^3.18.1", - "node-pty": "^1.1.0", - "tar": "^7.5.22", - "uuid": "^14.0.0" + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { - "electron": "^41.10.7", - "electron-builder": "^26.8.1", - "electron-reload": "^2.0.0-alpha.1", - "esbuild": "^0.28.0", - "jsdoc": "^4.0.4" + "@tauri-apps/cli": "^2.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.7.0", + "jsdom": "^26.1.0", + "typescript": "~5.9.2", + "vite": "^6.4.1", + "vitest": "^3.2.4" } }, "../coretrace-cli/packages/cli-core": { "name": "@coretrace/cli-core", - "version": "0.1.0", + "version": "0.2.0", + "extraneous": true, "license": "UNLICENSED", "dependencies": { "ignore": "^7.0.8", @@ -47,398 +49,572 @@ "node": ">=22.15" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", "bin": { - "parser": "bin/babel-parser.js" + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@coretrace/cli-core": { - "resolved": "../coretrace-cli/packages/cli-core", - "link": true + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" }, - "node_modules/@electron-internal/extract-zip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", - "integrity": "sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==", + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "engines": { - "node": ">=22.12.0" + "node": ">=6.9.0" } }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, - "bin": { - "asar": "bin/asar.js" + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { - "node": ">=10.12.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@electron/asar/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@electron/asar/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, - "license": "ISC", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": "*" + "node": ">=6.9.0" } }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" + "@babel/types": "^7.29.8" }, "bin": { - "electron-fuses": "dist/bin.js" + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "dev": true, "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@electron/get": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-5.1.0.tgz", - "integrity": "sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==", + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "env-paths": "^3.0.0", - "graceful-fs": "^4.2.11", - "progress": "^2.0.3", - "semver": "^7.6.3", - "sumchecker": "^3.0.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { - "node": ">=22.12.0" + "node": ">=6.9.0" }, - "optionalDependencies": { - "undici": "^7.24.4" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@electron/get/node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "dev": true, "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { - "node": ">= 10.0.0" + "node": ">=6.9.0" } }, - "node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" }, "engines": { - "node": ">=10" + "node": ">=6.9.0" } }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { - "node": ">=12.0.0" + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" } }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "engines": { - "node": ">= 8.0.0" + "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@electron/rebuild": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.2.0.tgz", - "integrity": "sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^12.2.0", - "read-binary-file-arch": "^1.0.6" - }, - "bin": { - "electron-rebuild": "lib/cli.js" + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" }, "engines": { - "node": ">=22.12.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, "engines": { - "node": ">=16.4" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "node_modules/@electron/universal/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, - "license": "MIT" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", - "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=6.0.0" } }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz", - "integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==", - "dev": true, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "@monaco-editor/loader": "^1.5.0" }, - "engines": { - "node": ">=14.14" + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", "cpu": [ - "ppc64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "aix" + "linux" ], "engines": { - "node": ">=18" + "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", "cpu": [ "arm" ], @@ -447,15 +623,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", "cpu": [ "arm64" ], @@ -464,100 +637,82 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "android" - ], - "engines": { - "node": ">=18" - } + "darwin" + ] }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } + "freebsd" + ] }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", "cpu": [ "arm" ], @@ -566,15 +721,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", "cpu": [ "arm64" ], @@ -583,32 +735,26 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", "cpu": [ "loong64" ], @@ -617,32 +763,26 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", "cpu": [ - "mips64el" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", "cpu": [ "ppc64" ], @@ -651,83 +791,68 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", "cpu": [ - "riscv64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", "cpu": [ - "s390x" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", "cpu": [ - "x64" + "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", "cpu": [ "x64" ], @@ -735,33 +860,27 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", "cpu": [ "x64" ], @@ -770,15 +889,12 @@ "optional": true, "os": [ "openbsd" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", "cpu": [ "arm64" ], @@ -787,66 +903,54 @@ "optional": true, "os": [ "openharmony" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } + "win32" + ] }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", "cpu": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", "cpu": [ "x64" ], @@ -855,608 +959,360 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@huggingface/jinja": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.6.tgz", - "integrity": "sha512-MyMWyLnjqo+KRJYSH7oWNbsOn5onuIvfXYPcc0WOGxU0eHUV7oAYUoQTl2BMdu7ml+ea/bu11UM+EshbeHwtIA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@jsdoc/salty": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.12.tgz", - "integrity": "sha512-TuB0x50EoAvEX/UEWITd8Mkn3WhiTjSvbTMCLj0BhsQEl5iUzjXdA0bETEVpTk+5TGTLR6QktI9H4hLviVeaAQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "lodash": "^4.18.1" - }, - "engines": { - "node": ">=v12.0.0" - } - }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - } - }, - "node_modules/@kwsites/promise-deferred": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", - "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", - "license": "MIT" - }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } + ] }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" + "node_modules/@tauri-apps/api": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.1.tgz", + "integrity": "sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" } }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@noble/hashes": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz", - "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20.19.0" + "node": ">= 10" }, "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@node-llama-cpp/linux-arm64": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-arm64/-/linux-arm64-3.18.1.tgz", - "integrity": "sha512-rXMgZxUay78FOJV/fJ67apYP9eElH5jd4df5YRKPlLhLHHchuOSyDn+qtyW/L/EnPzpogoLkmULqCkdXU39XsQ==", + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", "cpu": [ - "arm64", - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/linux-armv7l": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-armv7l/-/linux-armv7l-3.18.1.tgz", - "integrity": "sha512-BrJL2cGo0pN5xd5nw+CzTn2rFMpz9MJyZZPUY81ptGkF2uIuXT2hdCVh56i9ImQrTwBfq1YcZL/l/Qe/1+HR/Q==", + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", "cpu": [ - "arm", "x64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/linux-x64": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64/-/linux-x64-3.18.1.tgz", - "integrity": "sha512-tRmWcsyvAcqJHQHXHsaOkx6muGbcirA9nRdNgH6n7bjGUw4VuoBD3dChyNF3/Ktt7ohB9kz+XhhyZjbDHpXyMA==", + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", "cpu": [ - "x64" + "arm" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/linux-x64-cuda": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda/-/linux-x64-cuda-3.18.1.tgz", - "integrity": "sha512-qOaYP4uwsUoBHQ/7xSOvyJIuXapS57Al+Sudgi00f96ldNZLKe1vuSGptAi5LTM2lIj66PKm6h8PlRWctwsZ2g==", + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/linux-x64-cuda-ext": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-cuda-ext/-/linux-x64-cuda-ext-3.18.1.tgz", - "integrity": "sha512-VqyKhAVHPCpFzh0f1koCBgpThL+04QOXwv0oDQ8s8YcpfMMOXQlBhTB0plgTh0HrPExoObfTS4ohkrbyGgmztQ==", + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", "cpu": [ - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/linux-x64-vulkan": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/linux-x64-vulkan/-/linux-x64-vulkan-3.18.1.tgz", - "integrity": "sha512-SIaNTK5pUPhwJD0gmiQfHa8OrRctVMmnqu+slJrz2Mzgg/XrwFndJlS9hvc+jSjTXCouwf7sYeQaaJWvQgBh/A==", + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", "cpu": [ - "x64" + "riscv64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/mac-arm64-metal": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-arm64-metal/-/mac-arm64-metal-3.18.1.tgz", - "integrity": "sha512-cyZTdsUMlvuRlGmkkoBbN3v/DT6NuruEqoQYd9CqIrPyLa1xLNBTSKIZ9SgRnw23iCOj4URfITvRP+2pu63LuQ==", + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", "cpu": [ - "arm64", "x64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/mac-x64": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/mac-x64/-/mac-x64-3.18.1.tgz", - "integrity": "sha512-GfCPgdltaIpBhEnQ7WfsrRXrZO9r9pBtDUAQMXRuJwOPP5q7xKrQZUXI6J6mpc8tAG0//CTIuGn4hTKoD/8V8w==", + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/win-arm64": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-arm64/-/win-arm64-3.18.1.tgz", - "integrity": "sha512-S05YUzBMVSRS5KNbOS26cDYugeQHqogI3uewtTUBVC0tPbTHRSKjsdicmgWru1eNAry399LWWhzOf/3St/qsAw==", + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", "cpu": [ - "arm64", - "x64" + "arm64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/win-x64": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64/-/win-x64-3.18.1.tgz", - "integrity": "sha512-QLDVphPl+YDI+x/VYYgIV1N9g0GMXk3PqcoopOUG3cBRUtce7FO+YX903YdRJezs4oKbIp8YaO+xYBgeUSqhpA==", + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", "cpu": [ - "x64" + "ia32" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/win-x64-cuda": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda/-/win-x64-cuda-3.18.1.tgz", - "integrity": "sha512-drgJmBhnxGQtB/SLo4sf4PPSuxRv3MdNP0FF6rKPY9TtzEOV293bRQyYEu/JYwvXfVApAIsRaJUTGvCkA9Qobw==", + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", "cpu": [ "x64" ], - "license": "MIT", + "dev": true, + "license": "Apache-2.0 OR MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=20.0.0" + "node": ">= 10" } }, - "node_modules/@node-llama-cpp/win-x64-cuda-ext": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-cuda-ext/-/win-x64-cuda-ext-3.18.1.tgz", - "integrity": "sha512-u0FzJBQsJA355ksKERxwPJhlcWl3ZJSNkU2ZUwDEiKNOCbv3ybvSCIEyDvB63wdtkfVUuCRJWijZnpDZxrCGqg==", - "cpu": [ - "x64" - ], + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, "engines": { - "node": ">=20.0.0" + "node": ">=18" } }, - "node_modules/@node-llama-cpp/win-x64-vulkan": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/@node-llama-cpp/win-x64-vulkan/-/win-x64-vulkan-3.18.1.tgz", - "integrity": "sha512-PjmxrnPToi7y0zlP7l+hRIhvOmuEv94P6xZ11vjqICEJu8XdAJpvTfPKgDW4W0p0v4+So8ZiZYLUuwIHcsseyQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@peculiar/asn1-schema": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.9.4.tgz", - "integrity": "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==", - "dev": true, + "node_modules/@testing-library/react": { + "version": "16.3.3", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.3.tgz", + "integrity": "sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==", + "dev": true, "license": "MIT", "dependencies": { - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": ">=14" + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@peculiar/json-schema": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", - "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "node_modules/@testing-library/user-event": { + "version": "14.6.7", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.7.tgz", + "integrity": "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==", "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, "engines": { - "node": ">=8.0.0" + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@peculiar/utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", - "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } + "peer": true }, - "node_modules/@peculiar/webcrypto": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.7.1.tgz", - "integrity": "sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==", + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/json-schema": "^1.1.12", - "@peculiar/utils": "^2.0.2", - "tslib": "^2.8.1", - "webcrypto-core": "^1.9.2" - }, - "engines": { - "node": ">=14.18.0" - } - }, - "node_modules/@reflink/reflink": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink/-/reflink-0.1.19.tgz", - "integrity": "sha512-DmCG8GzysnCZ15bres3N5AHCmwBwYgp0As6xjhQ47rAUTUXxJiK+lLUxaGsX3hd/30qUpVElh05PbGuxRPgJwA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@reflink/reflink-darwin-arm64": "0.1.19", - "@reflink/reflink-darwin-x64": "0.1.19", - "@reflink/reflink-linux-arm64-gnu": "0.1.19", - "@reflink/reflink-linux-arm64-musl": "0.1.19", - "@reflink/reflink-linux-x64-gnu": "0.1.19", - "@reflink/reflink-linux-x64-musl": "0.1.19", - "@reflink/reflink-win32-arm64-msvc": "0.1.19", - "@reflink/reflink-win32-x64-msvc": "0.1.19" - } - }, - "node_modules/@reflink/reflink-darwin-arm64": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-arm64/-/reflink-darwin-arm64-0.1.19.tgz", - "integrity": "sha512-ruy44Lpepdk1FqDz38vExBY/PVUsjxZA+chd9wozjUH9JjuDT/HEaQYA6wYN9mf041l0yLVar6BCZuWABJvHSA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-darwin-x64": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-darwin-x64/-/reflink-darwin-x64-0.1.19.tgz", - "integrity": "sha512-By85MSWrMZa+c26TcnAy8SDk0sTUkYlNnwknSchkhHpGXOtjNDUOxJE9oByBnGbeuIE1PiQsxDG3Ud+IVV9yuA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-linux-arm64-gnu": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-gnu/-/reflink-linux-arm64-gnu-0.1.19.tgz", - "integrity": "sha512-7P+er8+rP9iNeN+bfmccM4hTAaLP6PQJPKWSA4iSk2bNvo6KU6RyPgYeHxXmzNKzPVRcypZQTpFgstHam6maVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-linux-arm64-musl": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-arm64-musl/-/reflink-linux-arm64-musl-0.1.19.tgz", - "integrity": "sha512-37iO/Dp6m5DDaC2sf3zPtx/hl9FV3Xze4xoYidrxxS9bgP3S8ALroxRK6xBG/1TtfXKTvolvp+IjrUU6ujIGmA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-linux-x64-gnu": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-gnu/-/reflink-linux-x64-gnu-0.1.19.tgz", - "integrity": "sha512-jbI8jvuYCaA3MVUdu8vLoLAFqC+iNMpiSuLbxlAgg7x3K5bsS8nOpTRnkLF7vISJ+rVR8W+7ThXlXlUQ93ulkw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-linux-x64-musl": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-linux-x64-musl/-/reflink-linux-x64-musl-0.1.19.tgz", - "integrity": "sha512-e9FBWDe+lv7QKAwtKOt6A2W/fyy/aEEfr0g6j/hWzvQcrzHCsz07BNQYlNOjTfeytrtLU7k449H1PI95jA4OjQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-win32-arm64-msvc": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-arm64-msvc/-/reflink-win32-arm64-msvc-0.1.19.tgz", - "integrity": "sha512-09PxnVIQcd+UOn4WAW73WU6PXL7DwGS6wPlkMhMg2zlHHG65F3vHepOw06HFCq+N42qkaNAc8AKIabWvtk6cIQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@reflink/reflink-win32-x64-msvc": { - "version": "0.1.19", - "resolved": "https://registry.npmjs.org/@reflink/reflink-win32-x64-msvc/-/reflink-win32-x64-msvc-0.1.19.tgz", - "integrity": "sha512-E//yT4ni2SyhwP8JRjVGWr3cbnhWDiPLgnQ66qqaanjjnMiu3O/2tjCPQXlcGc/DEYofpDc9fvhv6tALQsMV9w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "node_modules/@simple-git/args-pathspec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@simple-git/args-pathspec/-/args-pathspec-1.0.3.tgz", - "integrity": "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==", - "license": "MIT" - }, - "node_modules/@simple-git/argv-parser": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@simple-git/argv-parser/-/argv-parser-1.1.1.tgz", - "integrity": "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw==", + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, "license": "MIT", "dependencies": { - "@simple-git/args-pathspec": "^1.0.3" + "@babel/types": "^7.0.0" } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tinyhttp/content-disposition": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.4.tgz", - "integrity": "sha512-5Kc5CM2Ysn3vTTArBs2vESUt0AQiWZA86yc1TI3B+lxXmtEq133C1nxXNOgnzhrivdPZIh3zLj5gDnZjoLL5GA==", - "license": "MIT", - "engines": { - "node": ">=12.17.0" - }, - "funding": { - "type": "individual", - "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + "@babel/types": "^7.28.2" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/debug": { @@ -1465,59 +1321,23 @@ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "@types/ms": "*" } }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1526,7 +1346,9 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@types/node": { "version": "24.12.2", @@ -1534,4595 +1356,1976 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "undici-types": "~7.16.0" } }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "dev": true, "license": "MIT", "dependencies": { - "@types/node": "*" + "csstype": "^3.2.2" } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.15", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz", - "integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==", + "node_modules/@types/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10.0.0" + "peerDependencies": { + "@types/react": "^19.2.0" } }, - "node_modules/abbrev": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", - "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 14" + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/ajv": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", - "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", "dev": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ansi-escapes": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", - "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=14.16" + "dependencies": { + "tinyrainbow": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "tinyspy": "^4.0.3" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://opencollective.com/vitest" } }, - "node_modules/app-builder-lib": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.15.3.tgz", - "integrity": "sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==", + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, "license": "MIT", - "dependencies": { - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.4", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@noble/hashes": "^2.2.0", - "@peculiar/webcrypto": "^1.7.1", - "@types/fs-extra": "9.0.13", - "ajv": "^8.18.0", - "asn1js": "^3.0.10", - "async-exit-hook": "^2.0.1", - "builder-util": "26.15.3", - "builder-util-runtime": "9.7.0", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.15.3", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.2.5", - "pkijs": "^3.4.0", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.7", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "unzipper": "^0.12.3", - "which": "^5.0.0" - }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.15.3", - "electron-builder-squirrel-windows": "26.15.3" + "node": ">= 14" } }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "peer": true, "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" + "dequal": "^2.0.3" } }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, "engines": { - "node": ">=6 <7 || >=8" + "node": ">=12" } }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", "dev": true, - "license": "ISC", + "license": "Apache-2.0", "bin": { - "semver": "bin/semver.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", "dev": true, "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, { "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, "engines": { - "node": ">=8" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/app-builder-lib/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=8" } }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/app-builder-lib/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">= 16" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" }, - "node_modules/asn1js": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", - "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.5", - "tslib": "^2.8.1" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true, "license": "MIT" }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, "engines": { - "node": ">=0.12.0" + "node": ">=18" } }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { - "retry": "0.13.1" + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true, "license": "MIT" }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">= 4.0.0" + "node": ">=6" } }, - "node_modules/aws4": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", - "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", + "peer": true, "engines": { - "node": "18 || 20 || >=22" + "node": ">=6" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" + "license": "MIT", + "peer": true }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "node_modules/electron-to-chromium": { + "version": "1.5.423", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.423.tgz", + "integrity": "sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "ISC" }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, - "node_modules/boolean": { + "node_modules/escalade": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, "engines": { - "node": "20 || >=22" + "node": ">=6" } }, - "node_modules/braces": { + "node_modules/estree-walker": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" + "@types/estree": "^1.0.0" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.15.3.tgz", - "integrity": "sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "builder-util-runtime": "9.7.0", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", - "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, + "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/bytestreamjs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz", - "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "BSD-3-Clause", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.0.0" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10.6.0" + "node": ">=6.9.0" } }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "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", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 0.4" + "node": ">= 14" } }, - "node_modules/catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "license": "MIT", "dependencies": { - "lodash": "^4.17.15" + "agent-base": "^7.1.2", + "debug": "4" }, "engines": { - "node": ">= 10" + "node": ">= 14" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/chmodrp": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chmodrp/-/chmodrp-1.0.2.tgz", - "integrity": "sha512-TdngOlFV1FLTzU0o1w8MB6/BFywhtLC0SzRTGJU7T9lmdjlCWeMRt1iVo0Ki+ldwNk0BqNiKoc8xpLZEQ8mY1w==", + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, "license": "MIT" }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" + "optional": true, + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, "engines": { - "node": ">=8" + "node": ">=6" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.468.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", + "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, - "node_modules/cliui/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==", + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "peer": true, + "bin": { + "lz-string": "bin/bin.js" } }, - "node_modules/cliui/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==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "node_modules/monaco-editor": { + "version": "0.52.2", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", + "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "node_modules/cmake-js": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/cmake-js/-/cmake-js-8.0.0.tgz", - "integrity": "sha512-YbUP88RDwCvoQkZhRtGURYm9RIpWdtvZuhT87fKNoLjk8kIFIFeARpKfuZQGdwfH99GZpUmqSfcDrK62X7lTgg==", + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "fs-extra": "^11.3.3", - "node-api-headers": "^1.8.0", - "rc": "1.2.8", - "semver": "^7.7.3", - "tar": "^7.5.6", - "url-join": "^4.0.1", - "which": "^6.0.0", - "yargs": "^17.7.2" - }, - "bin": { - "cmake-js": "bin/cmake-js" - }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" } }, - "node_modules/cmake-js/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "node_modules/nwsapi": { + "version": "2.2.27", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.27.tgz", + "integrity": "sha512-gQPNF78qebCQ6tvVFBYrvJdBNOrYZm90ZlXgpIFm06p6qHDHq/XC4TnJftN6OMbxVE0UTBAoRgcsDeJBBooITw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "entities": "^6.0.0" }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/cmake-js/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/cmake-js/node_modules/which": { + "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" + "node": ">=0.12" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, "engines": { - "node": ">= 0.8" + "node": ">= 14.16" } }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } + "license": "ISC" }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, - "license": "MIT" - }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">= 8" + "node": "^10 || ^12 || >=14" } }, - "node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "bin": { - "node-which": "bin/node-which" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">= 8" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "ms": "^2.1.3" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "node_modules/pretty-format/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==", "dev": true, "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, + "peer": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4.0.0" + "node": ">=6" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", - "optional": true, "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" + "scheduler": "^0.27.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "react": "^19.2.8" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "peer": true }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.4.0" + "node": ">=0.10.0" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", + "node_modules/rollup": { + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", "dev": true, "license": "MIT", "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", + "fsevents": "~2.3.2" } }, - "node_modules/dir-compare/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, "license": "MIT" }, - "node_modules/dir-compare/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } + "license": "MIT" }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "xmlchars": "^2.2.0" }, "engines": { - "node": "*" + "node": ">=v12.22.7" } }, - "node_modules/dmg-builder": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.15.3.tgz", - "integrity": "sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==", + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0" - } + "license": "ISC" }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-3-Clause", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=0.10.0" } }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dotenv": "^16.4.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } + "license": "MIT" }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" + "js-tokens": "^9.0.1" }, - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "readable-stream": "^2.0.2" - } + "license": "MIT" }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/electron": { - "version": "41.10.7", - "resolved": "https://registry.npmjs.org/electron/-/electron-41.10.7.tgz", - "integrity": "sha512-AqIiefddlf5i+HYCGatr8VlBuEbWJpad4/yloBFYcMB2r57j7xhW1ogiJ+BfA5kxQyfmSs5yjwy54Me+wJn1jw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, - "hasInstallScript": true, "license": "MIT", "dependencies": { - "@electron-internal/extract-zip": "^1.0.1", - "@electron/get": "^5.0.0", - "@types/node": "^24.9.0" - }, - "bin": { - "electron": "cli.js" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">= 22.12.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/electron-builder": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.15.3.tgz", - "integrity": "sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==", + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", "dev": true, "license": "MIT", - "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", - "builder-util-runtime": "9.7.0", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.15.3", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, "engines": { - "node": ">=14.0.0" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz", - "integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==", + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.15.3", - "builder-util": "26.15.3", - "electron-winstaller": "5.4.0" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/electron-publish": { - "version": "26.15.3", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz", - "integrity": "sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==", + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", "dev": true, "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "aws4": "^1.13.2", - "builder-util": "26.15.3", - "builder-util-runtime": "9.7.0", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/electron-reload": { - "version": "2.0.0-alpha.1", - "resolved": "https://registry.npmjs.org/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz", - "integrity": "sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g==", + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", "dev": true, "license": "MIT", "dependencies": { - "chokidar": "^3.5.2" + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/electron-reload/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "dev": true, - "license": "MIT", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "tldts": "^6.1.32" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">=16" } }, - "node_modules/electron-reload/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8.6" + "dependencies": { + "punycode": "^2.3.1" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=18" } }, - "node_modules/electron-reload/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" }, "engines": { - "node": ">=8.10.0" + "node": ">=14.17" } }, - "node_modules/electron-updater": { - "version": "6.8.9", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz", - "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==", + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "builder-util-runtime": "9.7.0", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "~7.7.3", - "tiny-typed-emitter": "^2.1.0" - } - }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, "bin": { - "semver": "bin/semver.js" + "update-browserslist-db": "cli.js" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" }, "engines": { - "node": ">=8.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" }, "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } } }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" }, "engines": { - "node": ">=6 <7 || >=8" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/electron-winstaller/node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "peer": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/electron-winstaller/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "peer": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">= 4.0.0" + "node": ">=18" } }, - "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" - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=18" } }, - "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==", + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/env-var": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/env-var/-/env-var-7.5.0.tgz", - "integrity": "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA==", + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" + "node": ">=18" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "license": "MIT" - }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" ], - "license": "BSD-3-Clause" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filelist": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", - "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", - "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/filename-reserved-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-3.0.0.tgz", - "integrity": "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/filenamify": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-6.0.0.tgz", - "integrity": "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==", - "license": "MIT", - "dependencies": { - "filename-reserved-regex": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-east-asian-width": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", - "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "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/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "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" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "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==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "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.", - "dev": true, - "license": "ISC", - "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", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ipull": { - "version": "3.9.5", - "resolved": "https://registry.npmjs.org/ipull/-/ipull-3.9.5.tgz", - "integrity": "sha512-5w/yZB5lXmTfsvNawmvkCjYo4SJNuKQz/av8TC1UiOyfOHyaM+DReqbpU2XpWYfmY+NIUbRRH8PUAWsxaS+IfA==", - "license": "MIT", - "dependencies": { - "@tinyhttp/content-disposition": "^2.2.0", - "async-retry": "^1.3.3", - "chalk": "^5.3.0", - "ci-info": "^4.0.0", - "cli-spinners": "^2.9.2", - "commander": "^10.0.0", - "eventemitter3": "^5.0.1", - "filenamify": "^6.0.0", - "fs-extra": "^11.1.1", - "is-unicode-supported": "^2.0.0", - "lifecycle-utils": "^2.0.1", - "lodash.debounce": "^4.0.8", - "lowdb": "^7.0.1", - "pretty-bytes": "^6.1.0", - "pretty-ms": "^8.0.0", - "sleep-promise": "^9.1.0", - "slice-ansi": "^7.1.0", - "stdout-update": "^4.0.1", - "strip-ansi": "^7.1.0" - }, - "bin": { - "ipull": "dist/cli/cli.js" - }, - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/ido-pluto/ipull?sponsor=1" - }, - "optionalDependencies": { - "@reflink/reflink": "^0.1.16" - } - }, - "node_modules/ipull/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ipull/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/ipull/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/ipull/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/ipull/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/lifecycle-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-2.1.0.tgz", - "integrity": "sha512-AnrXnE2/OF9PHCyFg0RSqsnQTzV991XaZA/buhFDoc58xU7rhSCDgCz/09Lqpsn4MpoPHt7TRAXV1kWZypFVsA==", - "license": "MIT" - }, - "node_modules/ipull/node_modules/parse-ms": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-3.0.0.tgz", - "integrity": "sha512-Tpb8Z7r7XbbtBTrM9UhpkzzaMrqA2VXMT3YChzYltwV3P3pM6t8wl7TvpMnSTosz1aQAdVib7kdoys7vYOPerw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/pretty-ms": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-8.0.0.tgz", - "integrity": "sha512-ASJqOugUF1bbzI35STMBUpZqdfYKlJugy6JBziGi2EE+AL5JPJGSzvpeVXojxrr0ViUYoToUjb5kjSEGf7Y83Q==", - "license": "MIT", - "dependencies": { - "parse-ms": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ipull/node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/js-yaml": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", - "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/js2xmlparser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", - "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "xmlcreate": "^2.0.4" - } - }, - "node_modules/jsdoc": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.5.tgz", - "integrity": "sha512-P4C6MWP9yIlMiK8nwoZvxN84vb6MsnXcHuy7XzVOvQoCizWX5JFCBsWIIWKXBltpoRZXddUOVQmCTOZt9yDj9g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@babel/parser": "^7.20.15", - "@jsdoc/salty": "^0.2.1", - "@types/markdown-it": "^14.1.1", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.2", - "klaw": "^3.0.0", - "markdown-it": "^14.1.0", - "markdown-it-anchor": "^8.6.7", - "marked": "^4.0.10", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "underscore": "~1.13.2" - }, - "bin": { - "jsdoc": "jsdoc.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", - "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.9" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" - }, - "node_modules/lifecycle-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lifecycle-utils/-/lifecycle-utils-3.1.1.tgz", - "integrity": "sha512-gNd3OvhFNjHykJE3uGntz7UuPzWlK9phrIdXxU9Adis0+ExkwnZibfxCJWiWWZ+a6VbKiZrb+9D9hCQWd4vjTg==", - "license": "MIT" - }, - "node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lowdb": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", - "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", - "license": "MIT", - "dependencies": { - "steno": "^4.0.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/markdown-it": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", - "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/markdown-it-anchor": { - "version": "8.6.7", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz", - "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==", - "dev": true, - "license": "Unlicense", - "peerDependencies": { - "@types/markdown-it": "*", - "markdown-it": "*" - } - }, - "node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "dev": true, - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/matcher/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-function": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", - "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/monaco-editor": { - "version": "0.52.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", - "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "5.1.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", - "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^18 || >=20" - } - }, - "node_modules/node-abi": { - "version": "4.35.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.35.0.tgz", - "integrity": "sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-api-headers": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/node-api-headers/-/node-api-headers-1.8.0.tgz", - "integrity": "sha512-jfnmiKWjRAGbdD1yQS28bknFM1tbHC1oucyuMPjmkEs+kpiu76aRs40WlTmBmyEgzDM76ge1DQ7XJ3R5deiVjQ==", - "license": "MIT" - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-gyp": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", - "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.4", - "tinyglobby": "^0.2.12", - "undici": "^6.25.0", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-gyp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-gyp/node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, - "node_modules/node-gyp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-llama-cpp": { - "version": "3.18.1", - "resolved": "https://registry.npmjs.org/node-llama-cpp/-/node-llama-cpp-3.18.1.tgz", - "integrity": "sha512-w0zfuy/IKS2fhrbed5SylZDXJHTVz4HnkwZ4UrFPgSNwJab3QIPwIl4lyCKHHy9flLrtxsAuV5kXfH3HZ6bb8w==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@huggingface/jinja": "^0.5.6", - "async-retry": "^1.3.3", - "bytes": "^3.1.2", - "chalk": "^5.6.2", - "chmodrp": "^1.0.2", - "cmake-js": "^8.0.0", - "cross-spawn": "^7.0.6", - "env-var": "^7.5.0", - "filenamify": "^6.0.0", - "fs-extra": "^11.3.4", - "ignore": "^7.0.4", - "ipull": "^3.9.5", - "is-unicode-supported": "^2.1.0", - "lifecycle-utils": "^3.1.1", - "log-symbols": "^7.0.1", - "nanoid": "^5.1.6", - "node-addon-api": "^8.6.0", - "ora": "^9.3.0", - "pretty-ms": "^9.3.0", - "proper-lockfile": "^4.1.2", - "semver": "^7.7.1", - "simple-git": "^3.33.0", - "slice-ansi": "^8.0.0", - "stdout-update": "^4.0.1", - "strip-ansi": "^7.2.0", - "validate-npm-package-name": "^7.0.2", - "which": "^6.0.1", - "yargs": "^17.7.2" - }, - "bin": { - "nlc": "dist/cli/cli.js", - "node-llama-cpp": "dist/cli/cli.js" - }, - "engines": { - "node": ">=20.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/giladgd" - }, - "optionalDependencies": { - "@node-llama-cpp/linux-arm64": "3.18.1", - "@node-llama-cpp/linux-armv7l": "3.18.1", - "@node-llama-cpp/linux-x64": "3.18.1", - "@node-llama-cpp/linux-x64-cuda": "3.18.1", - "@node-llama-cpp/linux-x64-cuda-ext": "3.18.1", - "@node-llama-cpp/linux-x64-vulkan": "3.18.1", - "@node-llama-cpp/mac-arm64-metal": "3.18.1", - "@node-llama-cpp/mac-x64": "3.18.1", - "@node-llama-cpp/win-arm64": "3.18.1", - "@node-llama-cpp/win-x64": "3.18.1", - "@node-llama-cpp/win-x64-cuda": "3.18.1", - "@node-llama-cpp/win-x64-cuda-ext": "3.18.1", - "@node-llama-cpp/win-x64-vulkan": "3.18.1" - }, - "peerDependencies": { - "typescript": ">=5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/node-llama-cpp/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/node-llama-cpp/node_modules/is-fullwidth-code-point": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/isexe": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", - "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=20" - } - }, - "node_modules/node-llama-cpp/node_modules/node-addon-api": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-llama-cpp/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/ora": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-9.3.0.tgz", - "integrity": "sha512-lBX72MWFduWEf7v7uWf5DHp9Jn5BI8bNPGuFgtXMmr2uDz2Gz2749y3am3agSDdkhHPHYmmxEGSKH85ZLGzgXw==", - "license": "MIT", - "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.1", - "string-width": "^8.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/node-llama-cpp/node_modules/slice-ansi": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", - "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.3", - "is-fullwidth-code-point": "^5.1.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/node-llama-cpp/node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/node-llama-cpp/node_modules/which": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", - "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", - "license": "ISC", - "dependencies": { - "isexe": "^4.0.0" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/node-pty": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0.tgz", - "integrity": "sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^7.1.0" - } - }, - "node_modules/node-pty/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/nopt": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", - "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dev": true, - "license": "MIT", - "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", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", - "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkijs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz", - "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@noble/hashes": "1.4.0", - "asn1js": "^3.0.6", - "bytestreamjs": "^2.0.1", - "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/pkijs/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/proc-log": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", - "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/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", - "engines": { - "node": ">= 4" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pvtsutils": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", - "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } - }, - "node_modules/pvutils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.2.0.tgz", - "integrity": "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/requizzle": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz", - "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.21" - } - }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.4.tgz", - "integrity": "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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" - }, - "node_modules/simple-git": { - "version": "3.36.0", - "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.36.0.tgz", - "integrity": "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==", - "license": "MIT", - "dependencies": { - "@kwsites/file-exists": "^1.1.1", - "@kwsites/promise-deferred": "^1.1.1", - "@simple-git/args-pathspec": "^1.0.3", - "@simple-git/argv-parser": "^1.1.0", - "debug": "^4.4.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/git-js?sponsor=1" - } - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sleep-promise": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/sleep-promise/-/sleep-promise-9.1.0.tgz", - "integrity": "sha512-UHYzVpz9Xn8b+jikYSD6bqvf754xL2uBUzDFwiU6NcdZeifPr6UfgU43xpkPu67VMS88+TI2PSI7Eohgqf2fKA==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "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==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/stdin-discarder": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", - "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stdout-update": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/stdout-update/-/stdout-update-4.0.1.tgz", - "integrity": "sha512-wiS21Jthlvl1to+oorePvcyrIkiG/6M3D3VTmDUlJm7Cy6SbFhKkAvX+YBuHLxck/tO3mrdpC/cNesigQc3+UQ==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^6.2.0", - "ansi-styles": "^6.2.1", - "string-width": "^7.1.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/stdout-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/stdout-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/stdout-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/steno": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", - "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "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", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/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", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/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", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tar": { - "version": "7.5.22", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz", - "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp/node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, + "linux" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tmp": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", - "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.14" + "node": ">=18" } }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "(MIT OR CC0-1.0)", + "license": "MIT", "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/underscore": { - "version": "1.13.8", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", - "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=20.18.1" + "node": ">=18" } }, - "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 10.0.0" + "node": ">=18" } }, - "node_modules/unzipper": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.5.tgz", - "integrity": "sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==", + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "bluebird": "~3.7.2", - "duplexer2": "~0.1.4", - "fs-extra": "11.3.1", - "graceful-fs": "^4.2.2", - "node-int64": "^0.4.0" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/unzipper/node_modules/fs-extra": { - "version": "11.3.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", - "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==", + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=14.14" + "node": ">=18" } }, - "node_modules/url-join": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", - "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", - "license": "MIT" - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" ], + "dev": true, "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/validate-npm-package-name": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-7.0.2.tgz", - "integrity": "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A==", - "license": "ISC", + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": ">=18" } }, - "node_modules/webcrypto-core": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz", - "integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==", + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@peculiar/asn1-schema": "^2.7.0", - "@peculiar/json-schema": "^1.1.12", - "@peculiar/utils": "^2.0.2", - "asn1js": "^3.0.10", - "tslib": "^2.8.1" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" }, "bin": { - "node-which": "bin/which.js" + "vitest": "vitest.mjs" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/wrap-ansi/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", + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/wrap-ansi/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==", + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "iconv-lite": "0.6.3" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.0" + "node": ">=18" } }, - "node_modules/xmlcreate": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", - "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, - "license": "Apache-2.0" - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, - "license": "ISC" - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" }, "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" + "node": ">=10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "node_modules/yoctocolors": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", - "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", - "license": "MIT", + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 603a480..9e75d30 100644 --- a/package.json +++ b/package.json @@ -1,103 +1,38 @@ { "name": "ctrace-gui", - "version": "5.1.0", - "description": "A modern Electron-based GUI for CTrace static analysis tool", - "main": "src/main.js", + "version": "6.0.0-beta.1", + "private": true, + "description": "CoreTrace desktop workspace built with Tauri and React", + "license": "Apache-2.0", "scripts": { - "build:renderer": "node scripts/build-renderer-bundle.js", - "build:renderer:dev": "node scripts/build-renderer-bundle.js --dev", - "prestart": "npm run build:renderer:dev", - "predist": "npm run build:renderer", - "predist:publish": "npm run build:renderer", - "predist:win": "npm run build:renderer", - "predist:mac": "npm run build:renderer", - "predist:linux": "npm run build:renderer", - "test": "node --test", - "test:wsl": "node src/main/utils/wslTest.js", - "start": "electron .", - "release:ship": "bash ./scripts/release.sh", - "dist": "electron-builder", - "dist:publish": "electron-builder --publish always", - "dist:win": "electron-builder --win", - "dist:mac": "electron-builder --mac", - "dist:linux": "electron-builder --linux", - "build:verify": "./scripts/build-verify.sh", - "docs": "jsdoc -c jsdoc.conf.json", - "docs:watch": "jsdoc -c jsdoc.conf.json --watch", - "postinstall": "node scripts/patch-node-pty-spectre.js" + "start": "tauri dev", + "dev": "vite --host 127.0.0.1", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --host 127.0.0.1", + "tauri": "tauri", + "dist": "tauri build", + "test": "vitest run", + "test:native": "cargo test --manifest-path src-tauri/Cargo.toml", + "check": "tsc --noEmit" }, - "keywords": [ - "ctrace", - "static-analysis", - "cpp", - "c", - "development", - "gui", - "electron" - ], - "author": "CTrace GUI Team", - "license": "Apache-2.0", "dependencies": { - "chokidar": "^5.0.0", - "electron-updater": "^6.8.9", - "ignore": "^7.0.5", + "@monaco-editor/react": "^4.7.0", + "@tauri-apps/api": "^2.8.0", + "lucide-react": "^0.468.0", "monaco-editor": "^0.52.2", - "node-llama-cpp": "^3.18.1", - "node-pty": "^1.1.0", - "tar": "^7.5.22", - "uuid": "^14.0.0", - "@coretrace/cli-core": "file:../coretrace-cli/packages/cli-core" + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { - "electron": "^41.10.7", - "electron-builder": "^26.8.1", - "electron-reload": "^2.0.0-alpha.1", - "esbuild": "^0.28.0", - "jsdoc": "^4.0.4" - }, - "build": { - "publish": [ - { - "provider": "github", - "owner": "CoreTrace", - "repo": "coretrace-gui", - "releaseType": "release" - } - ], - "asar": true, - "extraResources": [ - { - "from": "bin/ctrace", - "to": "bin/ctrace" - } - ], - "asarUnpack": [], - "afterPack": "./scripts/remove-locales.js", - "appId": "com.coretrace.ctracegui", - "compression": "normal", - "productName": "CtraceGUI", - "win": { - "target": "nsis", - "icon": "assets/ctrace.ico" - }, - "mac": { - "target": [ - "dmg", - "zip" - ], - "icon": "assets/ctrace.icns", - "category": "public.app-category.developer-tools", - "identity": null, - "hardenedRuntime": false, - "gatekeeperAssess": false, - "notarize": false - }, - "linux": { - "target": "AppImage", - "icon": "assets/ctrace.png", - "category": "Development", - "synopsis": "Static analysis tool for C/C++ code", - "description": "A modern Electron-based GUI application that provides an intuitive interface for running CTrace static analysis on C/C++ code." - } + "@tauri-apps/cli": "^2.8.0", + "@types/react": "^19.1.0", + "@types/react-dom": "^19.1.0", + "@vitejs/plugin-react": "^4.7.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "jsdom": "^26.1.0", + "typescript": "~5.9.2", + "vite": "^6.4.1", + "vitest": "^3.2.4" } } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..0401251 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "coretrace-desktop" +version = "6.0.0-beta.1" +edition = "2021" +rust-version = "1.85" +license = "Apache-2.0" + +[lib] +name = "coretrace_desktop" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +tokio = { version = "1", features = ["process", "time", "io-util", "sync"] } +keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } +url = "2" +uuid = { version = "1", features = ["v4"] } +sha2 = "0.10" +open = "5" + +[dev-dependencies] +tempfile = "3" diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..2846c57 --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1 @@ +fn main() { tauri_build::build() } diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json new file mode 100644 index 0000000..98eb3cd --- /dev/null +++ b/src-tauri/capabilities/main.json @@ -0,0 +1 @@ +{"$schema":"../gen/schemas/desktop-schema.json","identifier":"main","description":"Local application window only; native commands own workspace and network access.","windows":["main"],"permissions":["core:default"]} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..0848562 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,5 @@ +pub fn run() { + tauri::Builder::default() + .run(tauri::generate_context!()) + .expect("failed to start CoreTrace desktop"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..3317636 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,2 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +fn main() { coretrace_desktop::run() } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..ae668fb --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "CoreTrace", + "version": "6.0.0-beta.1", + "identifier": "fr.coretrace.desktop", + "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://127.0.0.1:1420", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" }, + "app": { + "windows": [{ "label": "main", "title": "CoreTrace", "width": 1440, "height": 940, "minWidth": 880, "minHeight": 600 }], + "security": { "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost; worker-src 'self' blob:" } + }, + "bundle": { "active": true, "targets": "all", "icon": ["../assets/ctrace.png", "../assets/ctrace.ico", "../assets/ctrace.icns"] } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..89a89ad --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", + "strict": true, "noEmit": true, "skipLibCheck": true, + "allowImportingTsExtensions": true, "resolveJsonModule": true, + "esModuleInterop": true, "types": ["vite/client"] + }, + "include": ["desktop", "vite.config.ts", "vitest.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..67c624c --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ + plugins: [react()], + server: { port: 1420, strictPort: true, watch: { ignored: ['**/src-tauri/**', '**/native/**'] } }, + build: { outDir: 'dist', target: ['es2022', 'chrome105', 'safari15'] }, + clearScreen: false, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..ac24363 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], test: { include: ['desktop/**/*.test.{ts,tsx}'], environment: 'jsdom' } }); From 451c6e3a805628a23c987a10766ba5c8c18fc0bf Mon Sep 17 00:00:00 2001 From: shookapic Date: Tue, 8 Sep 2026 14:56:01 +0200 Subject: [PATCH 02/66] feat(desktop): add native workspace and platform services --- src-tauri/Cargo.lock | 5159 ++++++++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 3 +- src-tauri/src/analysis.rs | 125 + src-tauri/src/cloud.rs | 488 ++++ src-tauri/src/github.rs | 104 + src-tauri/src/lib.rs | 29 + src-tauri/src/main.rs | 4 +- src-tauri/src/workspace.rs | 205 ++ 8 files changed, 6115 insertions(+), 2 deletions(-) create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/src/analysis.rs create mode 100644 src-tauri/src/cloud.rs create mode 100644 src-tauri/src/github.rs create mode 100644 src-tauri/src/workspace.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..8696d36 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5159 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.20", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "coretrace-desktop" +version = "6.0.0-beta.1" +dependencies = [ + "keyring", + "open", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tempfile", + "tokio", + "url", + "uuid", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.5", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror 2.0.20", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.5+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link 0.2.1", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bd39932ad231f10513df9ab62661fead8719872150b7ad02a2df79f4e141e" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c603ab8300cf18bc3b14146b19fe3dfcc4843ae5a400cd0e7a30b95aa366634" +dependencies = [ + "is-wsl", + "libc", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896bade328c13f7042a297ea5ac5b0951f6cf989dea5f32c2fd98da398195cb" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.20", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935177bb8c0cd8ca1a4e6d1a2ac8988bea69cab4f9d3a31311e012ad27868ea4" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d607aa01a3cb0ad757d6fd216136910db3c97b102fe686585689615a02dbcdc" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45c444e496845d3f2a351146bff59aae4975b2280238df1dfaa0c7d1846f38e" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.20", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.20", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61854a36651aa48381e5e209f69a01273b77f3f9f91f0c430b1b98d33bd47229" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.20", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de22eef34fd78c0da050e748710edd50bf127e651d02ea1b2bfada1523cc5c51" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.20", + "toml 1.1.5+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.20", + "toml 1.1.5+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.5+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c0ba9680044b4ce98d391a62094047eada0d64860b80166c39f4a6b5640785" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.20", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.20", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.20", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0401251..2390039 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,12 +18,13 @@ tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } -tokio = { version = "1", features = ["process", "time", "io-util", "sync"] } +tokio = { version = "1", features = ["process", "time", "io-util", "sync", "macros"] } keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service"] } url = "2" uuid = { version = "1", features = ["v4"] } sha2 = "0.10" open = "5" +tempfile = "3" [dev-dependencies] tempfile = "3" diff --git a/src-tauri/src/analysis.rs b/src-tauri/src/analysis.rs new file mode 100644 index 0000000..cb8b26a --- /dev/null +++ b/src-tauri/src/analysis.rs @@ -0,0 +1,125 @@ +use crate::workspace::{resolve, root, WorkspaceState}; +use serde::Serialize; +use std::{path::PathBuf, sync::Mutex, time::Duration}; +use tauri_plugin_dialog::DialogExt; +use tokio::{ + io::{AsyncRead, AsyncReadExt}, + process::Command, + sync::oneshot, +}; + +#[derive(Default)] +pub struct AnalysisState { + executable: Mutex>, + cancel: Mutex>>, +} +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ResultView { + exit_code: Option, + stdout: String, + stderr: String, + report: Option, + cancelled: bool, +} + +#[tauri::command] +pub async fn choose_analyser( + app: tauri::AppHandle, + state: tauri::State<'_, AnalysisState>, +) -> Result, String> { + let Some(file) = app + .dialog() + .file() + .set_title("Select the installed ctrace executable") + .blocking_pick_file() + else { + return Ok(None); + }; + let path = file + .into_path() + .map_err(|e| e.to_string())? + .canonicalize() + .map_err(|e| e.to_string())?; + if !path.is_file() { + return Err("Choose an installed ctrace executable".into()); + } + let display = path.display().to_string(); + *state.executable.lock().map_err(|e| e.to_string())? = Some(path); + Ok(Some(display)) +} +async fn capture(mut reader: impl AsyncRead + Unpin) -> String { + let mut out = Vec::new(); + let mut chunk = [0; 8192]; + let mut truncated = false; + while let Ok(count) = reader.read(&mut chunk).await { + if count == 0 { + break; + } + let remaining = (1024 * 1024_usize).saturating_sub(out.len()); + out.extend_from_slice(&chunk[..count.min(remaining)]); + truncated |= count > remaining; + } + let mut text = String::from_utf8_lossy(&out).into_owned(); + if truncated { + text.push_str("\n[Output truncated after 1 MiB]"); + } + text +} +#[tauri::command] +pub async fn analyse_local( + state: tauri::State<'_, AnalysisState>, + workspace: tauri::State<'_, WorkspaceState>, + workspace_id: String, + path: String, +) -> Result { + let root = root(&workspace, &workspace_id)?; + let input = resolve(&root, &path)?; + if !input.is_file() || input.to_string_lossy().contains(',') { + return Err("Choose one source file without a comma in its path".into()); + } + let executable = state + .executable + .lock() + .map_err(|e| e.to_string())? + .clone() + .ok_or("Choose the installed ctrace executable in Settings first")?; + let (sender, mut receiver) = oneshot::channel(); + { + let mut active = state.cancel.lock().map_err(|e| e.to_string())?; + if active.is_some() { + return Err("An analysis is already running".into()); + } + *active = Some(sender); + } + let result = async { + let temp = tempfile::tempdir().map_err(|e| e.to_string())?; + let report_path = temp.path().join("report.sarif"); + let mut command = Command::new(executable); + command.arg("--input").arg(input).args(["--static", "--sarif-format", "--report-file"]).arg(&report_path) + .arg("--output-file").arg(temp.path().join("ctrace.out")) + .current_dir(&root).stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()).kill_on_drop(true); + #[cfg(windows)] command.creation_flags(0x08000000); + let mut child = command.spawn().map_err(|e| format!("Cannot start ctrace: {e}"))?; + let stdout = tokio::spawn(capture(child.stdout.take().ok_or("Missing stdout")?)); + let stderr = tokio::spawn(capture(child.stderr.take().ok_or("Missing stderr")?)); + let (status, cancelled) = tokio::select! { + status = child.wait() => (status.map_err(|e| e.to_string())?, false), + _ = &mut receiver => { child.kill().await.map_err(|e| e.to_string())?; (child.wait().await.map_err(|e| e.to_string())?, true) }, + _ = tokio::time::sleep(Duration::from_secs(900)) => { let _ = child.kill().await; stdout.abort(); stderr.abort(); return Err("Analysis exceeded 15 minutes and was stopped".into()); } + }; + let read_output = async { (stdout.await.unwrap_or_default(), stderr.await.unwrap_or_default()) }; + let (stdout, stderr) = tokio::time::timeout(Duration::from_secs(5), read_output).await.unwrap_or_else(|_| (String::new(), "Output pipe did not close".into())); + let report = std::fs::metadata(&report_path).ok().filter(|m| m.len() <= 10 * 1024 * 1024).and_then(|_| std::fs::read_to_string(report_path).ok()); + Ok(ResultView { exit_code: status.code(), stdout, stderr, report, cancelled }) + }.await; + *state.cancel.lock().map_err(|e| e.to_string())? = None; + result +} +#[tauri::command] +pub fn cancel_local(state: tauri::State<'_, AnalysisState>) -> Result<(), String> { + if let Some(sender) = state.cancel.lock().map_err(|e| e.to_string())?.take() { + let _ = sender.send(()); + } + Ok(()) +} diff --git a/src-tauri/src/cloud.rs b/src-tauri/src/cloud.rs new file mode 100644 index 0000000..e962db6 --- /dev/null +++ b/src-tauri/src/cloud.rs @@ -0,0 +1,488 @@ +use reqwest::{Client, Method}; +use serde::Serialize; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::time::{Duration, Instant}; +use tokio::sync::Mutex; + +pub struct Cloud(pub Mutex); +pub struct Session { + client: Client, + base: String, + access: Option, + refresh: Option, + device: Option, +} +struct Device { + code: String, + uri: String, + deadline: Instant, + next_poll: Instant, + interval: Duration, +} +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Status { + signed_in: bool, + base_url: String, +} + +pub fn validate_base(value: &str) -> Result { + let parsed = url::Url::parse(value).map_err(|_| "Invalid API URL")?; + let local = matches!(parsed.host_str(), Some("localhost" | "127.0.0.1" | "[::1]")); + if (parsed.scheme() != "https" && !(parsed.scheme() == "http" && local)) + || !parsed.username().is_empty() + || parsed.password().is_some() + || parsed.query().is_some() + || parsed.fragment().is_some() + { + return Err("The API must use HTTPS (HTTP is allowed only on localhost), without credentials or query parameters".into()); + } + let base = value.trim_end_matches('/'); + Ok(if base.ends_with("/v1") { + base.into() + } else { + format!("{base}/v1") + }) +} +impl Cloud { + pub fn new() -> Result { + let base = validate_base( + &std::env::var("CORETRACE_BASE_URL").unwrap_or_else(|_| "https://coretrace.fr".into()), + )?; + let client = Client::builder() + .user_agent("coretrace-desktop/6.0.0-beta.1") + .timeout(Duration::from_secs(30)) + .redirect(reqwest::redirect::Policy::none()) + .build() + .map_err(|e| e.to_string())?; + Ok(Self(Mutex::new(Session { + client, + base, + access: None, + refresh: None, + device: None, + }))) + } +} +impl Session { + fn credential(&self) -> Result { + keyring::Entry::new("fr.coretrace.desktop", &self.base) + .map_err(|e| format!("Credential store: {e}")) + } + async fn send( + &self, + method: Method, + path: &str, + org: Option<&str>, + body: Option, + auth: bool, + ) -> Result<(u16, Value), String> { + let mut request = self + .client + .request(method, format!("{}{path}", self.base)) + .header("Accept", "application/json"); + if auth { + if let Some(token) = &self.access { + request = request.bearer_auth(token); + } + } + if let Some(org) = org { + request = request.header("X-Org", org); + } + if let Some(body) = body { + request = request.json(&body); + } + let response = request + .send() + .await + .map_err(|e| format!("Platform unavailable: {e}"))?; + let status = response.status().as_u16(); + let bytes = response.bytes().await.map_err(|e| e.to_string())?; + let value = if bytes.is_empty() { + Value::Null + } else { + serde_json::from_slice(&bytes) + .map_err(|_| format!("Platform returned an invalid response (HTTP {status})"))? + }; + Ok((status, value)) + } + fn accept_tokens(&mut self, value: Value) -> Result<(), String> { + let access = value["access_token"] + .as_str() + .ok_or("Missing access token")? + .to_owned(); + let refresh = value["refresh_token"] + .as_str() + .map(str::to_owned) + .or_else(|| self.refresh.clone()); + if let Some(token) = &refresh { + self.credential()? + .set_password(token) + .map_err(|e| format!("Could not save session in the OS credential store: {e}"))?; + } + self.access = Some(access); + self.refresh = refresh; + Ok(()) + } + async fn refresh(&mut self) -> Result<(), String> { + if self.refresh.is_none() { + self.refresh = match self.credential()?.get_password() { + Ok(token) => Some(token), + Err(keyring::Error::NoEntry) => None, + Err(e) => return Err(format!("Credential store unavailable: {e}")), + }; + } + let token = self.refresh.as_ref().ok_or("Sign in to CoreTrace first")?; + let response = self + .send( + Method::POST, + "/auth/refresh", + None, + Some(json!({"refresh_token":token})), + false, + ) + .await?; + self.accept_tokens(success(response)?) + } + async fn request( + &mut self, + method: Method, + path: &str, + org: Option<&str>, + body: Option, + ) -> Result { + if self.access.is_none() { + self.refresh().await?; + } + let response = self + .send(method.clone(), path, org, body.clone(), true) + .await?; + if response.0 != 401 { + return success(response); + } + self.access = None; + self.refresh().await?; + success(self.send(method, path, org, body, true).await?) + } +} +fn success((status, value): (u16, Value)) -> Result { + if (200..300).contains(&status) { + return Ok(value); + } + Err(format!( + "{} (HTTP {status}){}", + value["title"] + .as_str() + .or(value["error"].as_str()) + .unwrap_or("Platform request failed"), + value["request_id"] + .as_str() + .map(|s| format!(" · request {s}")) + .unwrap_or_default() + )) +} +fn segment(value: &str) -> Result<&str, String> { + if value.is_empty() + || value == "." + || value == ".." + || !value + .chars() + .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) + { + return Err("Invalid platform identifier".into()); + } + Ok(value) +} +#[tauri::command] +pub async fn cloud_status(cloud: tauri::State<'_, Cloud>) -> Result { + let mut session = cloud.0.lock().await; + let signed_in = session.access.is_some() || session.refresh().await.is_ok(); + Ok(Status { + signed_in, + base_url: session.base.clone(), + }) +} +#[tauri::command] +pub async fn login_start(cloud: tauri::State<'_, Cloud>) -> Result { + let mut s = cloud.0.lock().await; + let value = success( + s.send(Method::POST, "/auth/device", None, None, false) + .await?, + )?; + let code = value["device_code"] + .as_str() + .ok_or("Missing device code")? + .into(); + let uri = value["verification_uri"] + .as_str() + .ok_or("Missing verification URI")? + .to_owned(); + validate_external(&uri)?; + let interval = Duration::from_secs(value["interval"].as_u64().unwrap_or(5).max(1)); + let expires = value["expires_in"].as_u64().unwrap_or(600); + s.device = Some(Device { + code, + uri: uri.clone(), + deadline: Instant::now() + Duration::from_secs(expires), + next_poll: Instant::now() + interval, + interval, + }); + Ok( + json!({"userCode":value["user_code"], "verificationUri":uri, "interval":interval.as_secs(), "expiresIn":expires}), + ) +} +#[tauri::command] +pub async fn login_poll(cloud: tauri::State<'_, Cloud>) -> Result { + let mut s = cloud.0.lock().await; + let d = s.device.as_mut().ok_or("No sign-in in progress")?; + if Instant::now() > d.deadline { + s.device = None; + return Err("Sign-in code expired; start again".into()); + } + if Instant::now() < d.next_poll { + return Ok(false); + } + d.next_poll = Instant::now() + d.interval; + let code = d.code.clone(); + let response = s + .send( + Method::POST, + "/auth/device/token", + None, + Some(json!({"device_code":code})), + false, + ) + .await?; + match response.1["error"].as_str() { + Some("authorization_pending") => return Ok(false), + Some("slow_down") => { + if let Some(d) = &mut s.device { + d.interval += Duration::from_secs(5); + d.next_poll = Instant::now() + d.interval; + } + return Ok(false); + } + _ => {} + } + s.device = None; + s.accept_tokens(success(response)?)?; + Ok(true) +} +#[tauri::command] +pub async fn login_cancel(cloud: tauri::State<'_, Cloud>) -> Result<(), String> { + cloud.0.lock().await.device = None; + Ok(()) +} +#[tauri::command] +pub async fn logout(cloud: tauri::State<'_, Cloud>) -> Result<(), String> { + let mut s = cloud.0.lock().await; + // Clear local authority even when the server cannot be reached. + let _ = s.send(Method::POST, "/auth/logout", None, None, true).await; + s.access = None; + s.refresh = None; + s.device = None; + match s.credential()?.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(format!("Could not remove stored session: {e}")), + } +} +#[tauri::command] +pub async fn cloud_read( + cloud: tauri::State<'_, Cloud>, + resource: String, + org: Option, + id: Option, + run: Option, +) -> Result { + let org_path = || -> Result { + Ok(format!( + "/orgs/{}", + segment(org.as_deref().ok_or("Choose an organisation")?)? + )) + }; + let id = id.as_deref().map(segment).transpose()?; + let path = match resource.as_str() { + "me" => "/me".into(), + "tools" => "/tools".into(), + "jobs" => "/jobs?limit=200".into(), + "limits" => format!("{}/limits", org_path()?), + "organisation" => org_path()?, + "members" => format!("{}/members", org_path()?), + "installations" => format!("{}/scm/installations", org_path()?), + "repositories" => format!( + "{}/scm/installations/{}/repos", + org_path()?, + id.ok_or("Missing installation")? + ), + "job" => format!("/jobs/{}", id.ok_or("Missing job")?), + "report" => format!( + "/jobs/{}/runs/{}/report", + id.ok_or("Missing job")?, + segment(run.as_deref().ok_or("Missing run")?)? + ), + _ => return Err("Unknown desktop resource".into()), + }; + cloud + .0 + .lock() + .await + .request(Method::GET, &path, org.as_deref(), None) + .await +} +#[tauri::command] +pub async fn cloud_analyse( + cloud: tauri::State<'_, Cloud>, + org: String, + installation: String, + repository: String, + reference: String, + rerun: bool, + request_id: String, +) -> Result { + if reference.trim().is_empty() || reference.len() > 250 { + return Err("Choose a branch, tag or commit".into()); + } + let path = format!( + "/orgs/{}/scm/installations/{}/repos/{}/analyses", + segment(&org)?, + segment(&installation)?, + segment(&repository)? + ); + cloud + .0 + .lock() + .await + .request( + Method::POST, + &path, + Some(&org), + Some(json!({"ref":reference,"rerun":rerun,"idempotency_key":segment(&request_id)?})), + ) + .await +} +#[tauri::command] +pub async fn cloud_cancel( + cloud: tauri::State<'_, Cloud>, + org: String, + id: String, +) -> Result { + cloud + .0 + .lock() + .await + .request( + Method::POST, + &format!("/jobs/{}/cancel", segment(&id)?), + Some(segment(&org)?), + None, + ) + .await +} +#[tauri::command] +pub async fn cloud_report( + cloud: tauri::State<'_, Cloud>, + org: String, + id: String, + run: String, +) -> Result { + let mut s = cloud.0.lock().await; + let link = s + .request( + Method::GET, + &format!("/jobs/{}/runs/{}/report", segment(&id)?, segment(&run)?), + Some(segment(&org)?), + None, + ) + .await?; + let uri = link["url"].as_str().ok_or("Missing report download link")?; + validate_external(uri)?; + if link["size"].as_u64().unwrap_or(u64::MAX) > 10 * 1024 * 1024 { + return Err("Report exceeds the desktop limit of 10 MiB".into()); + } + // Signed artifact links never receive the platform bearer or X-Org header. + let mut response = s + .client + .get(uri) + .send() + .await + .map_err(|_| "Could not download report")?; + if !response.status().is_success() { + return Err(format!( + "Report download failed (HTTP {})", + response.status() + )); + } + let mut bytes = Vec::new(); + while let Some(chunk) = response + .chunk() + .await + .map_err(|_| "Report download interrupted")? + { + if bytes.len() + chunk.len() > 10 * 1024 * 1024 { + return Err("Report exceeds 10 MiB".into()); + } + bytes.extend_from_slice(&chunk); + } + if format!("{:x}", Sha256::digest(&bytes)) + != link["sha256"].as_str().ok_or("Missing report checksum")? + { + return Err("Report checksum mismatch".into()); + } + String::from_utf8(bytes).map_err(|_| "Report is not UTF-8".into()) +} +fn validate_external(value: &str) -> Result<(), String> { + let url = url::Url::parse(value).map_err(|_| "Invalid link")?; + if url.scheme() != "https" + || url.host_str().is_none() + || !url.username().is_empty() + || url.password().is_some() + { + return Err("Only HTTPS links may be opened".into()); + } + Ok(()) +} +#[tauri::command] +pub async fn open_account(cloud: tauri::State<'_, Cloud>, page: String) -> Result<(), String> { + let s = cloud.0.lock().await; + let url = match page.as_str() { + "device" => s + .device + .as_ref() + .ok_or("No sign-in in progress")? + .uri + .clone(), + "dashboard" => "https://coretrace.fr/app".into(), + "repositories" => "https://coretrace.fr/app/repositories".into(), + "settings" => "https://coretrace.fr/app/settings".into(), + _ => return Err("Unknown account page".into()), + }; + validate_external(&url)?; + open::that_detached(url).map_err(|e| e.to_string()) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn validates_platform_origin_and_identifiers() { + assert_eq!( + validate_base("https://coretrace.fr").unwrap(), + "https://coretrace.fr/v1" + ); + assert_eq!( + validate_base("http://127.0.0.1:8080/v1/").unwrap(), + "http://127.0.0.1:8080/v1" + ); + for bad in [ + "http://evil.test", + "https://user:pass@host", + "https://host?token=x", + "file:///tmp", + ] { + assert!(validate_base(bad).is_err()); + } + for bad in ["..", "org/jobs", "org?x=y", "org\r\nX-Org:x"] { + assert!(segment(bad).is_err()); + } + } +} diff --git a/src-tauri/src/github.rs b/src-tauri/src/github.rs new file mode 100644 index 0000000..40943df --- /dev/null +++ b/src-tauri/src/github.rs @@ -0,0 +1,104 @@ +use crate::workspace::{activate, Workspace, WorkspaceState}; +use std::time::Duration; +use tauri_plugin_dialog::DialogExt; +use tokio::process::Command; + +pub fn repository_url(value: &str) -> Result<(String, String), String> { + let value = value.trim().trim_end_matches('/').trim_end_matches(".git"); + let name = value.strip_prefix("https://github.com/").unwrap_or(value); + let parts: Vec<_> = name.split('/').collect(); + if parts.len() != 2 + || parts.iter().any(|p| { + p.is_empty() + || p.starts_with('.') + || p.starts_with('-') + || !p + .chars() + .all(|c| c.is_ascii_alphanumeric() || "-_.".contains(c)) + }) + { + return Err("Use owner/repository or https://github.com/owner/repository".into()); + } + Ok((format!("https://github.com/{name}.git"), parts[1].into())) +} +#[tauri::command] +pub async fn clone_repository( + app: tauri::AppHandle, + state: tauri::State<'_, WorkspaceState>, + repository: String, +) -> Result, String> { + let (url, name) = repository_url(&repository)?; + let Some(parent) = app + .dialog() + .file() + .set_title("Choose where to clone the repository") + .blocking_pick_folder() + else { + return Ok(None); + }; + let parent = parent + .into_path() + .map_err(|e| e.to_string())? + .canonicalize() + .map_err(|e| e.to_string())?; + let destination = parent.join(name); + if destination.exists() { + return Err( + "Destination already exists. Open it as a folder or choose another location.".into(), + ); + } + let mut command = Command::new("git"); + command + .args([ + "-c", + "core.hooksPath=", + "-c", + "protocol.file.allow=never", + "clone", + "--", + &url, + ]) + .arg(&destination) + .current_dir(&parent) + .env("GIT_TERMINAL_PROMPT", "0") + .env("GCM_INTERACTIVE", "never") + .kill_on_drop(true); + #[cfg(windows)] + command.creation_flags(0x08000000); + let output = tokio::time::timeout(Duration::from_secs(300), command.output()) + .await + .map_err(|_| { + "Clone timed out. An incomplete folder may remain; inspect it before retrying." + })? + .map_err(|e| format!("Git is required: {e}"))?; + if !output.status.success() { + return Err(format!("Git clone failed. For private repositories, sign in with Git Credential Manager or gh auth setup-git first. {}", String::from_utf8_lossy(&output.stderr))); + } + Ok(Some(activate(&state, destination)?)) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn accepts_only_github_repository_names() { + assert_eq!( + repository_url("https://github.com/CoreTrace/coretrace-gui.git") + .unwrap() + .0, + "https://github.com/CoreTrace/coretrace-gui.git" + ); + for bad in [ + "--upload-pack=sh", + "https://evil.test/a/b", + "a/../b", + "a/b?token=x", + "a/-b", + "a/b\n", + ] { + if bad.ends_with('\n') { + continue; + } + assert!(repository_url(bad).is_err()); + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0848562..eb6d8ad 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,34 @@ +mod analysis; +mod cloud; +mod github; +mod workspace; + pub fn run() { tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .manage(workspace::WorkspaceState::default()) + .manage(analysis::AnalysisState::default()) + .manage(cloud::Cloud::new().expect("invalid CoreTrace platform configuration")) + .invoke_handler(tauri::generate_handler![ + workspace::choose_workspace, + workspace::list_files, + workspace::read_file, + workspace::save_file, + github::clone_repository, + analysis::choose_analyser, + analysis::analyse_local, + analysis::cancel_local, + cloud::cloud_status, + cloud::login_start, + cloud::login_poll, + cloud::login_cancel, + cloud::logout, + cloud::cloud_read, + cloud::cloud_analyse, + cloud::cloud_cancel, + cloud::cloud_report, + cloud::open_account, + ]) .run(tauri::generate_context!()) .expect("failed to start CoreTrace desktop"); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3317636..fabc064 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,2 +1,4 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -fn main() { coretrace_desktop::run() } +fn main() { + coretrace_desktop::run() +} diff --git a/src-tauri/src/workspace.rs b/src-tauri/src/workspace.rs new file mode 100644 index 0000000..f309e3c --- /dev/null +++ b/src-tauri/src/workspace.rs @@ -0,0 +1,205 @@ +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{ + fs, + path::{Component, Path, PathBuf}, + sync::Mutex, +}; +use tauri_plugin_dialog::DialogExt; + +const MAX_FILE: u64 = 4 * 1024 * 1024; +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Workspace { + pub id: String, + pub name: String, + pub path: PathBuf, +} +#[derive(Default)] +pub struct WorkspaceState(pub Mutex>); +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Entry { + name: String, + path: String, + directory: bool, +} +#[derive(Serialize)] +pub struct Document { + content: String, + revision: String, +} + +pub fn activate(state: &WorkspaceState, path: PathBuf) -> Result { + let path = path.canonicalize().map_err(|e| e.to_string())?; + if !path.is_dir() { + return Err("Choose a folder".into()); + } + let workspace = Workspace { + id: uuid::Uuid::new_v4().to_string(), + name: path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .into(), + path, + }; + *state.0.lock().map_err(|e| e.to_string())? = Some(workspace.clone()); + Ok(workspace) +} +pub fn root(state: &WorkspaceState, id: &str) -> Result { + state + .0 + .lock() + .map_err(|e| e.to_string())? + .as_ref() + .filter(|w| w.id == id) + .map(|w| w.path.clone()) + .ok_or("Workspace changed; reopen the file".into()) +} +pub fn resolve(root: &Path, relative: &str) -> Result { + let path = Path::new(relative); + if path + .components() + .any(|c| !matches!(c, Component::Normal(_))) + && !relative.is_empty() + { + return Err("Only workspace-relative paths are allowed".into()); + } + if path.components().any(|c| { + c.as_os_str().to_string_lossy().eq_ignore_ascii_case(".git") + || c.as_os_str().to_string_lossy().contains(':') + }) { + return Err("Git metadata and alternate streams cannot be edited".into()); + } + let resolved = root.join(path).canonicalize().map_err(|e| e.to_string())?; + if !resolved.starts_with(root) { + return Err("Path leaves the selected workspace".into()); + } + Ok(resolved) +} +fn read(path: &Path) -> Result { + if !path.is_file() || path.metadata().map_err(|e| e.to_string())?.len() > MAX_FILE { + return Err("Choose a text file smaller than 4 MiB".into()); + } + let content = fs::read_to_string(path).map_err(|_| "This file is not UTF-8 text")?; + if content.contains('\0') { + return Err("Binary files cannot be edited".into()); + } + let revision = format!("{:x}", Sha256::digest(content.as_bytes())); + Ok(Document { content, revision }) +} +#[tauri::command] +pub async fn choose_workspace( + app: tauri::AppHandle, + state: tauri::State<'_, WorkspaceState>, +) -> Result, String> { + match app.dialog().file().blocking_pick_folder() { + Some(path) => Ok(Some(activate( + &state, + path.into_path().map_err(|e| e.to_string())?, + )?)), + None => Ok(None), + } +} +#[tauri::command] +pub fn list_files( + state: tauri::State<'_, WorkspaceState>, + workspace_id: String, + path: String, +) -> Result, String> { + let root = root(&state, &workspace_id)?; + let dir = resolve(&root, &path)?; + let mut entries = Vec::new(); + for entry in fs::read_dir(dir).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().into_owned(); + let kind = entry.file_type().map_err(|e| e.to_string())?; + if kind.is_symlink() + || matches!( + name.as_str(), + ".git" | "node_modules" | "target" | ".next" | ".venv" + ) + { + continue; + } + if entries.len() >= 5000 { + return Err("Folder contains more than 5,000 entries; open a smaller folder".into()); + } + entries.push(Entry { + path: entry + .path() + .strip_prefix(&root) + .map_err(|e| e.to_string())? + .to_string_lossy() + .replace('\\', "/"), + name, + directory: kind.is_dir(), + }); + } + entries.sort_by(|a, b| { + b.directory + .cmp(&a.directory) + .then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) + }); + Ok(entries) +} +#[tauri::command] +pub fn read_file( + state: tauri::State<'_, WorkspaceState>, + workspace_id: String, + path: String, +) -> Result { + read(&resolve(&root(&state, &workspace_id)?, &path)?) +} +#[tauri::command] +pub fn save_file( + state: tauri::State<'_, WorkspaceState>, + workspace_id: String, + path: String, + content: String, + revision: String, +) -> Result { + if content.len() > MAX_FILE as usize || content.contains('\0') { + return Err("Invalid or oversized text".into()); + } + let path = resolve(&root(&state, &workspace_id)?, &path)?; + if read(&path)?.revision != revision { + return Err( + "File changed on disk. Reopen it before saving; your draft has been kept.".into(), + ); + } + fs::write(&path, content).map_err(|e| e.to_string())?; + read(&path) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn rejects_escape_and_metadata() { + let dir = tempfile::tempdir().unwrap(); + let base = dir.path().canonicalize().unwrap(); + fs::write(base.join("code.ts"), "hello").unwrap(); + assert!(resolve(&base, "code.ts").is_ok()); + for path in [ + "../secret", + "/etc/passwd", + "C:\\Windows", + ".git/config", + "code.ts:secret", + ] { + assert!(resolve(&base, path).is_err(), "{path}"); + } + } + #[test] + fn stale_workspace_and_binary_are_rejected() { + let dir = tempfile::tempdir().unwrap(); + let state = WorkspaceState::default(); + let first = activate(&state, dir.path().into()).unwrap(); + activate(&state, dir.path().into()).unwrap(); + assert!(root(&state, &first.id).is_err()); + fs::write(dir.path().join("binary"), b"abc\0def").unwrap(); + assert!(read(&dir.path().join("binary")).is_err()); + } +} From 0bcc73bd01c080af450e270d9c655b935d7101ee Mon Sep 17 00:00:00 2001 From: shookapic Date: Tue, 8 Sep 2026 15:06:38 +0200 Subject: [PATCH 03/66] feat(desktop): build dashboards and integrated analysis workspace --- desktop/App.tsx | 536 ++++++ desktop/bridge.ts | 77 + desktop/components/Dialog.tsx | 81 + desktop/components/Login.tsx | 88 + desktop/features/Analyses.tsx | 561 ++++++ desktop/features/Dashboard.tsx | 236 +++ desktop/features/Repositories.tsx | 95 + desktop/features/Settings.tsx | 127 ++ desktop/features/WorkspaceEditor.tsx | 402 +++++ desktop/main.tsx | 12 +- desktop/model.ts | 118 ++ desktop/platform-schema.d.ts | 2422 ++++++++++++++++++++++++++ desktop/styles.css | 1332 ++++++++++++++ desktop/types.ts | 53 + desktop/useCloud.ts | 131 ++ index.html | 12 +- package-lock.json | 17 + package.json | 5 +- src-tauri/capabilities/main.json | 8 +- src-tauri/tauri.conf.json | 32 +- tsconfig.json | 17 +- vite.config.ts | 12 +- vitest.config.ts | 9 +- 23 files changed, 6360 insertions(+), 23 deletions(-) create mode 100644 desktop/App.tsx create mode 100644 desktop/bridge.ts create mode 100644 desktop/components/Dialog.tsx create mode 100644 desktop/components/Login.tsx create mode 100644 desktop/features/Analyses.tsx create mode 100644 desktop/features/Dashboard.tsx create mode 100644 desktop/features/Repositories.tsx create mode 100644 desktop/features/Settings.tsx create mode 100644 desktop/features/WorkspaceEditor.tsx create mode 100644 desktop/model.ts create mode 100644 desktop/platform-schema.d.ts create mode 100644 desktop/styles.css create mode 100644 desktop/types.ts create mode 100644 desktop/useCloud.ts diff --git a/desktop/App.tsx b/desktop/App.tsx new file mode 100644 index 0000000..e0f9842 --- /dev/null +++ b/desktop/App.tsx @@ -0,0 +1,536 @@ +import { lazy, Suspense, useEffect, useRef, useState } from "react"; +import { + Activity, + ArrowUpRight, + Building2, + Code2, + FolderGit2, + FolderOpen, + Home, + LoaderCircle, + PanelLeftClose, + PanelLeftOpen, + Plus, + RefreshCw, + Settings2, + ShieldCheck, + X, +} from "lucide-react"; +import { desktop, errorMessage, native } from "./bridge"; +import { Dialog, useConfirm } from "./components/Dialog"; +import { Login } from "./components/Login"; +import { Dashboard } from "./features/Dashboard"; +import { Repositories } from "./features/Repositories"; +import { Analyses } from "./features/Analyses"; +import { Settings } from "./features/Settings"; +import type { EditorHandle } from "./features/WorkspaceEditor"; +import { useCloud } from "./useCloud"; +import type { Job, LocalResult, Page, Repository, Workspace } from "./types"; +const WorkspaceEditor = lazy(() => + import("./features/WorkspaceEditor").then((module) => ({ + default: module.WorkspaceEditor, + })), +); + +const pages = { + home: "Accueil", + organisation: "Organisation", + analyses: "Analyses", + repositories: "Dépôts", + workspace: "Espace de code", + settings: "Paramètres", +}; +const navigation = [ + { id: "home", icon: Home }, + { id: "analyses", icon: Activity }, + { id: "repositories", icon: FolderGit2 }, + { id: "workspace", icon: Code2 }, +] as const; +export default function App() { + const cloud = useCloud(); + const [page, setPage] = useState("home"); + const [collapsed, setCollapsed] = useState(false); + const [workspace, setWorkspace] = useState(null); + const [dirty, setDirty] = useState(false); + const [busy, setBusy] = useState(""); + const [message, setMessage] = useState(""); + const [login, setLogin] = useState(false); + const [clone, setClone] = useState(null); + const [selectedJob, setSelectedJob] = useState(null); + const [draftRepo, setDraftRepo] = useState(null); + const [analyser, setAnalyser] = useState(""); + const [local, setLocal] = useState(null); + const [localRunning, setLocalRunning] = useState(false); + const editor = useRef(null); + const confirm = useConfirm(); + const orgRef = useRef(cloud.org); + orgRef.current = cloud.org; + useEffect(() => { + setSelectedJob(null); + setDraftRepo(null); + }, [cloud.org]); + // Native close requests also respect unsaved buffers; beforeunload alone is not sufficient in a WebView. + useEffect(() => { + if (!native) return; + let disposed = false; + let unlisten: (() => void) | undefined; + void import("@tauri-apps/api/window") + .then(async ({ getCurrentWindow }) => { + const window = getCurrentWindow(); + const off = await window.onCloseRequested(async (event) => { + if (!dirty && !localRunning) return; + event.preventDefault(); + if ( + await confirm( + "Fermer CoreTrace ?", + `${dirty ? "Des modifications ne sont pas enregistrées. " : ""}${localRunning ? "Une analyse locale est en cours. " : ""}Voulez-vous quitter ?`, + "Quitter", + ) + ) { + if (localRunning) await desktop.cancelLocal(); + await window.destroy(); + } + }); + if (disposed) off(); + else unlisten = off; + }) + .catch((e) => setMessage(errorMessage(e))); + return () => { + disposed = true; + unlisten?.(); + }; + }, [dirty, localRunning, confirm]); + const canSwitch = async () => { + if (busy || localRunning) { + setMessage( + "Attendez la fin de l’opération en cours avant de changer de dossier.", + ); + return false; + } + return ( + !dirty || + (await confirm( + "Changer de dossier ?", + "Les modifications non enregistrées des fichiers ouverts seront abandonnées.", + "Changer de dossier", + )) + ); + }; + const activate = (next: Workspace | null) => { + if (!next) return; + setWorkspace(next); + setDirty(false); + setLocal(null); + setPage("workspace"); + }; + const openFolder = async () => { + if (!(await canSwitch())) return; + setBusy("Ouverture du dossier…"); + try { + activate(await desktop.chooseWorkspace()); + } catch (e) { + setMessage(errorMessage(e)); + } finally { + setBusy(""); + } + }; + const cloneRepo = async () => { + if (clone === null || !(await canSwitch())) return; + setBusy("Clonage du dépôt GitHub…"); + try { + const next = await desktop.cloneRepository(clone); + activate(next); + if (next) setClone(null); + } catch (e) { + setMessage(errorMessage(e)); + } finally { + setBusy(""); + } + }; + const runLocal = async (path: string) => { + if (!workspace || localRunning) return; + if (!analyser) { + setPage("settings"); + setMessage( + "Sélectionnez le programme ctrace avant de lancer une analyse locale.", + ); + return; + } + if ( + !(await confirm( + "Analyser ce fichier local ?", + `${path} sera analysé avec le programme ctrace sélectionné et ses outils installés.`, + "Lancer ctrace", + )) + ) + return; + setLocalRunning(true); + setLocal(null); + setPage("analyses"); + try { + setLocal(await desktop.analyseLocal(workspace.id, path)); + } catch (e) { + setMessage(errorMessage(e)); + } finally { + setLocalRunning(false); + } + }; + const openFinding = (path: string, line: number) => { + if (!workspace || !editor.current) { + setMessage( + "Ouvrez le dossier correspondant dans l’IDE pour accéder à ce fichier.", + ); + return; + } + setPage("workspace"); + void editor.current + .open(path, line) + .catch((e) => setMessage(errorMessage(e))); + }; + const selectJob = (job: Job | null) => { + if (orgRef.current !== cloud.org) return; + setSelectedJob(job); + setPage("analyses"); + }; + const analyseRepo = (repo: Repository) => { + setDraftRepo(repo); + setSelectedJob(null); + setPage("analyses"); + }; + return ( +
+ +
+
+
+ CoreTrace / + {pages[page]} + {page === "workspace" && workspace && ( + <> + / + {workspace.name} + + )} +
+
+ {cloud.me && ( + + )} + + +
+
+ {!native && ( +
+ Aperçu navigateur · Ouvrez l’application desktop pour accéder aux + dossiers, à GitHub et à votre compte. +
+ )} + {cloud.error && ( +
+ {cloud.error} + +
+ )} + {busy && ( +
+ + {busy} +
+ )} +
+ {(page === "home" || page === "organisation") && ( + void openFolder()} + clone={() => setClone("")} + login={() => setLogin(true)} + selectJob={selectJob} + /> + )} + {page === "repositories" && ( + setClone(name ?? "")} + analyse={analyseRepo} + notify={setMessage} + /> + )} + {page === "analyses" && ( + setDraftRepo(null)} + notify={setMessage} + openFinding={openFinding} + local={local} + localRunning={localRunning} + openWorkspace={() => + workspace ? setPage("workspace") : void openFolder() + } + /> + )} + {page === "settings" && ( + setLogin(true)} + notify={setMessage} + /> + )} + } + > + void runLocal(path)} + busy={localRunning} + notify={setMessage} + /> + + ) : ( +
+ +

Un espace pour votre code.

+

+ Ouvrez un dossier local ou clonez un dépôt GitHub pour + commencer. +

+
+ + +
+
+ )} +
+ +
+ + + {native ? "Desktop prêt" : "Aperçu de l’interface"} + + + {localRunning + ? "Analyse locale en cours" + : workspace + ? workspace.name + : "Aucun dossier ouvert"} + +
+
+ {message && ( +
+ {message} + +
+ )} + {login && ( + setLogin(false)} connected={cloud.reconnect} /> + )} + {clone !== null && ( + { + if (!busy) setClone(null); + }} + > +

+ Indiquez le dépôt, puis choisissez où le conserver sur votre + ordinateur. +

+ +

+ Les dépôts privés utilisent votre connexion Git locale. Si + nécessaire, connectez Git Credential Manager ou exécutez gh auth + setup-git. +

+
+ + +
+
+ )} + + ); +} diff --git a/desktop/bridge.ts b/desktop/bridge.ts new file mode 100644 index 0000000..12bc89e --- /dev/null +++ b/desktop/bridge.ts @@ -0,0 +1,77 @@ +import { invoke, isTauri } from "@tauri-apps/api/core"; +import type { + DeviceCode, + Document, + FileEntry, + Job, + LocalResult, + Workspace, +} from "./types"; + +export const native = isTauri(); +function call(command: string, args?: Record): Promise { + if (!native) + return Promise.reject( + new Error( + "Cette action nécessite l’application desktop. Lancez npm start.", + ), + ); + return invoke(command, args); +} +export const desktop = { + chooseWorkspace: () => call("choose_workspace"), + cloneRepository: (repository: string) => + call("clone_repository", { repository }), + files: (workspaceId: string, path = "") => + call("list_files", { workspaceId, path }), + read: (workspaceId: string, path: string) => + call("read_file", { workspaceId, path }), + save: ( + workspaceId: string, + path: string, + content: string, + revision: string, + ) => call("save_file", { workspaceId, path, content, revision }), + chooseAnalyser: () => call("choose_analyser"), + analyseLocal: (workspaceId: string, path: string) => + call("analyse_local", { workspaceId, path }), + cancelLocal: () => call("cancel_local"), + status: () => + native + ? call<{ signedIn: boolean; baseUrl: string }>("cloud_status") + : Promise.resolve({ + signedIn: false, + baseUrl: "https://coretrace.fr/v1", + }), + login: () => call("login_start"), + pollLogin: () => call("login_poll"), + cancelLogin: () => call("login_cancel"), + logout: () => call("logout"), + readCloud: (resource: string, org?: string, id?: string, run?: string) => + call("cloud_read", { resource, org, id, run }), + analyseCloud: ( + org: string, + installation: string, + repository: string, + reference: string, + rerun: boolean, + requestId: string, + ) => + call("cloud_analyse", { + org, + installation, + repository, + reference, + rerun, + requestId, + }), + cancelCloud: (org: string, id: string) => + call("cloud_cancel", { org, id }), + report: (org: string, id: string, run: string) => + call("cloud_report", { org, id, run }), + openAccount: (page: "device" | "dashboard" | "repositories" | "settings") => + call("open_account", { page }), +}; +export function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/desktop/components/Dialog.tsx b/desktop/components/Dialog.tsx new file mode 100644 index 0000000..e5539ff --- /dev/null +++ b/desktop/components/Dialog.tsx @@ -0,0 +1,81 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; +import { X } from "lucide-react"; + +export function Dialog({ + title, + children, + close, +}: { + title: string; + children: ReactNode; + close: () => void; +}) { + const ref = useRef(null); + useEffect(() => { + const dialog = ref.current!; + dialog.showModal(); + return () => dialog.close(); + }, []); + return ( + { + event.preventDefault(); + close(); + }} + aria-label={title} + > +
+

{title}

+ +
+ {children} +
+ ); +} +type Ask = (title: string, detail: string, action?: string) => Promise; +const ConfirmContext = createContext(async () => false); +export const useConfirm = () => useContext(ConfirmContext); +export function ConfirmProvider({ children }: { children: ReactNode }) { + const [request, setRequest] = useState<{ + title: string; + detail: string; + action: string; + resolve: (answer: boolean) => void; + } | null>(null); + const ask: Ask = useCallback( + (title, detail, action = "Continuer") => + new Promise((resolve) => setRequest({ title, detail, action, resolve })), + [], + ); + const finish = (answer: boolean) => { + request?.resolve(answer); + setRequest(null); + }; + return ( + + {children} + {request && ( + finish(false)}> +

{request.detail}

+
+ + +
+
+ )} +
+ ); +} diff --git a/desktop/components/Login.tsx b/desktop/components/Login.tsx new file mode 100644 index 0000000..b181fc9 --- /dev/null +++ b/desktop/components/Login.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { ExternalLink, LoaderCircle } from "lucide-react"; +import { desktop, errorMessage } from "../bridge"; +import type { DeviceCode } from "../types"; +import { Dialog } from "./Dialog"; + +export function Login({ + close, + connected, +}: { + close: () => void; + connected: () => Promise; +}) { + const [code, setCode] = useState(null); + const [error, setError] = useState(""); + useEffect(() => { + let stopped = false; + let timer: ReturnType; + const poll = async () => { + try { + const signedIn = await desktop.pollLogin(); + if (stopped) return; + if (signedIn) { + await connected(); + if (!stopped) close(); + } else timer = setTimeout(poll, 1000); + } catch (e) { + if (!stopped) setError(errorMessage(e)); + } + }; + void desktop + .login() + .then((value) => { + if (!stopped) { + setCode(value); + timer = setTimeout(poll, value.interval * 1000); + } + }) + .catch((e) => { + if (!stopped) setError(errorMessage(e)); + }); + return () => { + stopped = true; + clearTimeout(timer); + void desktop.cancelLogin().catch(() => {}); + }; + }, []); // One native device flow per dialog; callbacks are read for this attempt. + return ( + +

+ Validez ce code dans votre navigateur. Votre session sera conservée dans + le coffre-fort du système. +

+ {code ? ( + <> +
{code.userCode}
+

+ Code valable {Math.ceil(code.expiresIn / 60)} minutes. +

+ +

{code.verificationUri}

+ {!error && ( +

+ En attente de + validation… +

+ )} + + ) : ( + !error &&

Préparation de la connexion…

+ )} + {error && ( +

+ {error} +

+ )} +
+ ); +} diff --git a/desktop/features/Analyses.tsx b/desktop/features/Analyses.tsx new file mode 100644 index 0000000..ef5356a --- /dev/null +++ b/desktop/features/Analyses.tsx @@ -0,0 +1,561 @@ +import { useEffect, useState } from "react"; +import { + ArrowLeft, + FileCode2, + LoaderCircle, + Play, + Search, + Square, +} from "lucide-react"; +import { desktop, errorMessage } from "../bridge"; +import { useConfirm } from "../components/Dialog"; +import { + billed, + date, + duration, + number, + outcome, + parseFindings, + terminal, +} from "../model"; +import type { CloudModel } from "../useCloud"; +import type { Finding, Job, LocalResult, Repository } from "../types"; +import { JobRows } from "./Dashboard"; + +export function Findings({ + findings, + open, +}: { + findings: Finding[]; + open: (path: string, line: number) => void; +}) { + const [search, setSearch] = useState(""); + const filtered = findings.filter((f) => + `${f.message} ${f.rule} ${f.path}` + .toLowerCase() + .includes(search.toLowerCase()), + ); + return ( +
+
+

+ Résultats {findings.length} +

+ +
+ {filtered.map((f, i) => ( +
+
+ {f.level} + + {f.tool} {f.rule} + +
+

{f.message}

+ +
+ ))} + {!filtered.length && ( +

+ {findings.length + ? "Aucun résultat ne correspond au filtre." + : "Aucun problème signalé dans les rapports chargés."} +

+ )} +
+ ); +} + +function JobDetail({ + initial, + cloud, + close, + notify, + open, + rerun, +}: { + initial: Job; + cloud: CloudModel; + close: () => void; + notify: (message: string) => void; + open: (path: string, line: number) => void; + rerun: (repo: Repository, ref: string) => void; +}) { + const [job, setJob] = useState(initial); + const [findings, setFindings] = useState([]); + const [reportErrors, setReportErrors] = useState([]); + const [loading, setLoading] = useState(false); + const [pollError, setPollError] = useState(""); + const confirm = useConfirm(); + useEffect(() => { + let active = true; + let timer: ReturnType; + const poll = async () => { + try { + const next = await desktop.readCloud("job", cloud.org, initial.id); + if (!active) return; + setJob(next); + setPollError(""); + if (!terminal(next)) timer = setTimeout(poll, 2500); + else void cloud.refresh(); + } catch (e) { + if (active) { + setPollError(errorMessage(e)); + timer = setTimeout(poll, 5000); + } + } + }; + void poll(); + return () => { + active = false; + clearTimeout(timer); + }; + }, [initial.id, cloud.org]); + const finished = terminal(job); + useEffect(() => { + if (!finished) return; + let active = true; + setLoading(true); + setReportErrors([]); + void Promise.allSettled( + job.runs.map(async (run) => + parseFindings( + await desktop.report(cloud.org, job.id, run.id), + run.tool, + ), + ), + ).then((results) => { + if (!active) return; + setFindings( + results.flatMap((r) => (r.status === "fulfilled" ? r.value : [])), + ); + setReportErrors( + results.flatMap((r, i) => + r.status === "rejected" + ? [`${job.runs[i].tool} : ${errorMessage(r.reason)}`] + : [], + ), + ); + setLoading(false); + }); + return () => { + active = false; + }; + }, [finished, job.id, cloud.org]); + const repository = cloud.repositories.find( + (r) => r.full_name === job.source?.repo_full_name, + ); + const cancel = async () => { + if ( + !(await confirm( + "Annuler cette analyse ?", + "Les CTU déjà consommés restent facturés.", + "Arrêter l’analyse", + )) + ) + return; + try { + await desktop.cancelCloud(cloud.org, job.id); + setJob(await desktop.readCloud("job", cloud.org, job.id)); + } catch (e) { + notify(errorMessage(e)); + } + }; + return ( + <> + +
+
+

{job.source?.repo_full_name ?? "Analyse cloud"}

+

+ {job.source?.ref ?? job.source?.commit_sha} · {date(job.created_at)} +

+
+ + {outcome(job)} + +
+
+ {number(billed(job))} CTU facturés + {duration(job)} + + {job.runs.filter((r) => r.finished_at).length} / {job.runs.length}{" "} + outils terminés + + {!finished && ( + + )} + {finished && repository?.enabled && ( + + )} +
+ {pollError && ( +

+ {pollError} · Nouvelle tentative automatique. +

+ )} + {job.rejection_reason && ( +

+ La plateforme a refusé l’analyse : {job.rejection_reason} +

+ )} + {job.conclusion === "capped" && ( +

+ Le plafond CTU a été atteint. Les résultats peuvent être incomplets. +

+ )} + {!finished && ( +
+ + L’analyse est en cours. Les résultats seront chargés à sa fin. +
+ )} + {loading &&

Chargement des rapports…

} + {reportErrors.map((error) => ( +

+ {error} +

+ ))} + {finished && + !loading && + (findings.length > 0 || + (!reportErrors.length && job.runs.length > 0)) && ( + + )} +
+ Exécution des outils · {job.runs.length} + {job.runs.map((run) => ( +
+ {run.tool} + {run.execution_outcome ?? run.execution_status} + {number(run.billed_ctu)} CTU + {run.execution_outcome === "tool_error" && ( +

+ L’outil a échoué. Consultez son rapport lorsqu’il est + disponible. +

+ )} +
+ ))} +
+ + ); +} + +export function Analyses({ + cloud, + selected, + select, + initialRepository, + initialRef, + clearDraft, + notify, + openFinding, + local, + localRunning, + openWorkspace, +}: { + cloud: CloudModel; + selected: Job | null; + select: (job: Job | null) => void; + initialRepository: Repository | null; + initialRef: string; + clearDraft: () => void; + notify: (message: string) => void; + openFinding: (path: string, line: number) => void; + local: LocalResult | null; + localRunning: boolean; + openWorkspace: () => void; +}) { + const [repositoryId, setRepositoryId] = useState(initialRepository?.id ?? ""); + const [reference, setReference] = useState( + initialRef || initialRepository?.default_branch || "", + ); + const [rerun, setRerun] = useState(!!initialRef); + const [requestId, setRequestId] = useState(() => crypto.randomUUID()); + const [running, setRunning] = useState(false); + const [filter, setFilter] = useState(""); + const [status, setStatus] = useState(""); + const confirm = useConfirm(); + useEffect(() => { + if (initialRepository) { + setRepositoryId(initialRepository.id); + setReference(initialRef || initialRepository.default_branch); + setRerun(!!initialRef); + setRequestId(crypto.randomUUID()); + } + }, [initialRepository, initialRef]); + const run = async () => { + const repo = cloud.repositories.find((r) => r.id === repositoryId); + if (!repo || !reference.trim()) return; + if ( + !(await confirm( + rerun + ? "Relancer cette analyse cloud ?" + : "Lancer cette analyse cloud ?", + `${repo.full_name} · ${reference}. La plateforme analysera le commit distant et débitera les CTU de ${cloud.org}, selon sa configuration et ses plafonds. Les modifications locales ne seront pas envoyées.`, + "Lancer l’analyse", + )) + ) + return; + setRunning(true); + try { + const job = await desktop.analyseCloud( + cloud.org, + repo.installation, + repo.id, + reference.trim(), + rerun, + requestId, + ); + select(job); + setRequestId(crypto.randomUUID()); + clearDraft(); + void cloud.refresh(); + } catch (e) { + notify(errorMessage(e)); + } finally { + setRunning(false); + } + }; + const prepareRerun = (repo: Repository, ref: string) => { + setRepositoryId(repo.id); + setReference(ref); + setRerun(true); + setRequestId(crypto.randomUUID()); + select(null); + }; + return ( +
+ {selected ? ( + select(null)} + notify={notify} + open={openFinding} + rerun={prepareRerun} + /> + ) : ( + <> +
+
+
COMPRENDRE & CORRIGER
+

Analyses

+

Exécutez les outils CoreTrace et retrouvez leurs résultats.

+
+ +
+
+
+

Nouvelle analyse cloud

+ {cloud.org || "Connexion requise"} +
+
+ + + +
+ +

+ Les outils sont définis par la configuration du dépôt sur la + plateforme. Chaque exécution peut consommer des CTU. +

+ {!cloud.repositories.some((r) => r.enabled) && ( +

+ Connectez et activez un dépôt depuis la page Dépôts pour lancer + une analyse cloud. +

+ )} +
+ {localRunning && ( +
+ + Analyse locale en cours… + +
+ )} + {local && ( +
+
+

Dernière analyse locale

+ + {local.cancelled + ? "Annulée" + : `Code de sortie ${local.exitCode ?? "inconnu"}`} + +
+ {local.report ? ( + + ) : ( +

+ Aucun rapport structuré produit. Consultez la sortie de + l’outil. +

+ )} +
+ Sortie de ctrace +
+                  {local.stdout}
+                  {"\n"}
+                  {local.stderr}
+                
+
+
+ )} +
+

Historique de l’organisation

+
+ setFilter(e.target.value)} + /> + +
+
+ + (job.source?.repo_full_name ?? "") + .toLowerCase() + .includes(filter.toLowerCase()) && + (!status || + (status === "running" + ? !terminal(job) + : job.status === status)), + )} + select={select} + /> + {cloud.jobs.length >= 200 && ( +

+ Affichage des 200 analyses les plus récentes. +

+ )} + + )} +
+ ); +} diff --git a/desktop/features/Dashboard.tsx b/desktop/features/Dashboard.tsx new file mode 100644 index 0000000..acbc98d --- /dev/null +++ b/desktop/features/Dashboard.tsx @@ -0,0 +1,236 @@ +import { + ArrowRight, + Building2, + FolderOpen, + GitBranch, + ShieldCheck, + Sparkles, +} from "lucide-react"; +import type { CloudModel } from "../useCloud"; +import type { Job, Page, Workspace } from "../types"; +import { billed, date, number, outcome, usage } from "../model"; + +export function JobRows({ + jobs, + select, +}: { + jobs: Job[]; + select: (job: Job) => void; +}) { + if (!jobs.length) + return ( +
+ +

Aucune analyse à afficher.

+ + Vos prochains résultats apparaîtront ici. + +
+ ); + return ( +
+ {jobs.map((job) => ( + + ))} +
+ ); +} +export function Dashboard({ + cloud, + organisation, + workspace, + navigate, + openFolder, + clone, + login, + selectJob, +}: { + cloud: CloudModel; + organisation: boolean; + workspace: Workspace | null; + navigate: (page: Page) => void; + openFolder: () => void; + clone: () => void; + login: () => void; + selectJob: (job: Job) => void; +}) { + const summary = cloud.limits ? usage(cloud.limits) : null; + const allowance = summary?.allowance; + const fraction = + allowance && summary + ? Math.min(100, (summary.remaining / allowance) * 100) + : 0; + const member = cloud.me?.orgs.find((o) => o.slug === cloud.org); + return ( +
+
+ + {organisation ? "ESPACE ORGANISATION" : "VOTRE ESPACE DE TRAVAIL"} +
+
+
+

+ {organisation + ? cloud.org || "Votre organisation" + : `Bonjour${cloud.me?.principal.name ? `, ${cloud.me.principal.name}` : ""}.`} +
+ {!organisation && Que souhaitez-vous analyser ?} +

+

+ {organisation + ? "L’activité, les ressources et les résultats de votre équipe." + : "Ouvrez votre code. Lancez une analyse. Comprenez chaque résultat."} +

+
+
+ +
+
+ {!cloud.me && ( +
+
+ Retrouvez votre espace CoreTrace +

+ Connectez-vous pour accéder aux organisations, aux quotas et aux + analyses cloud. +

+
+ +
+ )} +
+ + + +
+ {cloud.me && ( + <> +
+

+ {organisation + ? "Ressources de l’organisation" + : "Votre organisation"} +

+ +
+
+
+ CTU disponibles + {summary ? number(summary.remaining) : "—"} +
+ +
+ + {allowance != null + ? `sur ${number(allowance)} CTU pour cette période` + : "Allocation de période non communiquée"} + +
+
+ Consommation de la période + + {summary?.used != null ? number(summary.used) : "—"}{" "} + CTU + + + {cloud.limits + ? `Renouvellement le ${new Date(cloud.limits.period_ends_at).toLocaleDateString("fr-FR")}` + : "Connexion à la plateforme nécessaire"} + +
+
+ Plan actuel + {cloud.limits?.plan ?? "—"} + + {" "} + {member + ? `${member.role} · ${member.access_state}` + : "Aucune organisation sélectionnée"} + +
+
+ {summary && + allowance != null && + summary.remaining < allowance / 10 && ( +
+ Votre quota est presque épuisé. Consultez les limites de votre + organisation avant de relancer une analyse. +
+ )} + + )} + {workspace && ( + + )} +
+
+

Analyses récentes

+

+ {cloud.me + ? "Les derniers résultats de l’organisation sélectionnée." + : "Connectez-vous pour retrouver votre historique."} +

+
+ +
+ +
+ ); +} diff --git a/desktop/features/Repositories.tsx b/desktop/features/Repositories.tsx new file mode 100644 index 0000000..dc6fa3c --- /dev/null +++ b/desktop/features/Repositories.tsx @@ -0,0 +1,95 @@ +import { ArrowUpRight, FolderGit2, GitBranch } from "lucide-react"; +import { desktop, errorMessage } from "../bridge"; +import type { CloudModel } from "../useCloud"; +import type { Repository } from "../types"; +export function Repositories({ + cloud, + clone, + analyse, + notify, +}: { + cloud: CloudModel; + clone: (name?: string) => void; + analyse: (repo: Repository) => void; + notify: (message: string) => void; +}) { + return ( +
+
+
+
CODE & SOURCES
+

Dépôts

+

+ Retrouvez les dépôts de votre organisation ou clonez un projet + GitHub. +

+
+ +
+
+

+ Dépôts connectés{" "} + {cloud.org && / {cloud.org}} +

+ +
+ {cloud.repositories.length ? ( +
+ {cloud.repositories.map((repo) => ( +
+
+ + + {repo.enabled ? "Analyse activée" : "Analyse désactivée"} + +
+

{repo.full_name}

+

+ {repo.default_branch} +

+
+ + +
+
+ ))} +
+ ) : ( +
+ +

+ {cloud.me + ? "Aucun dépôt GitHub connecté" + : "Connectez votre compte CoreTrace"} +

+

+ La plateforme liste les dépôts autorisés par l’installation GitHub + de votre organisation. +

+

+ Le clonage manuel reste disponible sans connexion CoreTrace. +

+
+ )} +
+ ); +} diff --git a/desktop/features/Settings.tsx b/desktop/features/Settings.tsx new file mode 100644 index 0000000..9c20718 --- /dev/null +++ b/desktop/features/Settings.tsx @@ -0,0 +1,127 @@ +import { ArrowUpRight, LogOut, Settings2, Users } from "lucide-react"; +import { useEffect, useState } from "react"; +import { desktop, errorMessage } from "../bridge"; +import type { CloudModel } from "../useCloud"; +import type { Member } from "../types"; +export function Settings({ + cloud, + analyser, + setAnalyser, + login, + notify, +}: { + cloud: CloudModel; + analyser: string; + setAnalyser: (value: string) => void; + login: () => void; + notify: (message: string) => void; +}) { + const [members, setMembers] = useState([]); + const [memberError, setMemberError] = useState(""); + useEffect(() => { + let active = true; + setMembers([]); + setMemberError(""); + if (cloud.org) + void desktop + .readCloud("members", cloud.org) + .then((rows) => { + if (active) setMembers(rows); + }) + .catch((e) => { + if (active) setMemberError(errorMessage(e)); + }); + return () => { + active = false; + }; + }, [cloud.org]); + return ( +
+
VOTRE CONFIGURATION
+

Paramètres

+
+

Compte CoreTrace

+
+
+ {cloud.me?.principal.name || "Non connecté"} +

+ {cloud.me?.email || + "Connectez-vous pour retrouver vos organisations."} +

+
+ {cloud.me ? ( + + ) : ( + + )} +
+

+ Plateforme : {cloud.baseUrl || "Chargement…"} +

+ +
+
+
+ +

Analyseur local

+
+

+ Sélectionnez le programme ctrace installé sur votre + machine. Les outils qu’il appelle doivent également être installés. +

+
+ + {analyser || "Aucun exécutable sélectionné"} + + +
+

+ Le lancement est explicite depuis le fichier actif de l’IDE. Le choix + vaut pour cette session. +

+
+ {cloud.me && ( +
+
+ +

Membres de {cloud.org}

+
+ {memberError &&

{memberError}

} + {members.map((member) => ( +
+ {member.user_id} + {member.role} +
+ ))} +
+ )} +

CoreTrace 6.0.0-beta.1 · Tauri + React

+
+ ); +} diff --git a/desktop/features/WorkspaceEditor.tsx b/desktop/features/WorkspaceEditor.tsx new file mode 100644 index 0000000..1f178b0 --- /dev/null +++ b/desktop/features/WorkspaceEditor.tsx @@ -0,0 +1,402 @@ +import { + forwardRef, + useEffect, + useImperativeHandle, + useRef, + useState, +} from "react"; +import Editor, { loader, type OnMount } from "@monaco-editor/react"; +import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; +import "monaco-editor/esm/vs/basic-languages/monaco.contribution"; +import EditorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; +import { + ChevronDown, + ChevronRight, + FileCode2, + Folder, + FolderOpen, + Play, + RefreshCw, + Save, + X, +} from "lucide-react"; +import { desktop, errorMessage } from "../bridge"; +import { useConfirm } from "../components/Dialog"; +import type { Document, FileEntry, Workspace } from "../types"; + +self.MonacoEnvironment = { getWorker: () => new EditorWorker() }; +loader.config({ monaco }); +interface Tab extends Document { + path: string; + draft: string; +} +export interface EditorHandle { + open: (path: string, line?: number) => Promise; +} +interface Props { + workspace: Workspace; + dirtyChanged: (dirty: boolean) => void; + run: (path: string) => void; + busy: boolean; + notify: (message: string) => void; +} +const language = (path: string) => + ({ + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + py: "python", + rs: "rust", + c: "c", + h: "c", + cpp: "cpp", + cc: "cpp", + hpp: "cpp", + go: "go", + java: "java", + json: "json", + md: "markdown", + html: "html", + css: "css", + yaml: "yaml", + yml: "yaml", + sh: "shell", + toml: "ini", + })[path.split(".").pop() ?? ""] ?? "plaintext"; + +function Tree({ + workspace, + path = "", + depth = 0, + select, + selected, + refresh, +}: { + workspace: Workspace; + path?: string; + depth?: number; + select: (path: string) => void; + selected: string; + refresh: number; +}) { + const [entries, setEntries] = useState([]); + const [expanded, setExpanded] = useState>(new Set()); + const [error, setError] = useState(""); + useEffect(() => { + let active = true; + setError(""); + void desktop + .files(workspace.id, path) + .then((files) => { + if (active) setEntries(files); + }) + .catch((e) => { + if (active) setError(errorMessage(e)); + }); + return () => { + active = false; + }; + }, [workspace.id, path, refresh]); + return ( +
+ {error &&

{error}

} + {entries.map((entry) => ( +
+ + {entry.directory && expanded.has(entry.path) && ( + + )} +
+ ))} +
+ ); +} + +export const WorkspaceEditor = forwardRef( + function WorkspaceEditor( + { workspace, dirtyChanged, run, busy, notify }, + ref, + ) { + const [tabs, setTabs] = useState([]); + const [active, setActive] = useState(""); + const [refresh, setRefresh] = useState(0); + const [saving, setSaving] = useState(false); + const editor = useRef(null); + const pendingLine = useRef(null); + const confirm = useConfirm(); + const tab = tabs.find((t) => t.path === active); + const dirty = tabs.some((t) => t.content !== t.draft); + const alive = useRef(true); + useEffect(() => { + alive.current = true; + return () => { + alive.current = false; + }; + }, []); + useEffect(() => { + dirtyChanged(dirty); + }, [dirty, dirtyChanged]); + useEffect(() => { + const prevent = (event: BeforeUnloadEvent) => { + if (dirty) { + event.preventDefault(); + event.returnValue = ""; + } + }; + window.addEventListener("beforeunload", prevent); + return () => window.removeEventListener("beforeunload", prevent); + }, [dirty]); + const reveal = (line: number) => { + editor.current?.revealLineInCenter(line); + editor.current?.setPosition({ lineNumber: line, column: 1 }); + editor.current?.focus(); + }; + const open = async (path: string, line?: number) => { + // Native resolution is authoritative; never accept a report's absolute URI as a workspace path. + if (!tabs.some((t) => t.path === path)) { + const doc = await desktop.read(workspace.id, path); + if (!alive.current) return; + setTabs((previous) => + previous.some((t) => t.path === path) + ? previous + : [...previous, { ...doc, path, draft: doc.content }], + ); + } + setActive(path); + if (line) { + pendingLine.current = line; + if (active === path) reveal(line); + } + }; + useImperativeHandle(ref, () => ({ open })); + useEffect(() => { + if (pendingLine.current) { + const line = pendingLine.current; + pendingLine.current = null; + requestAnimationFrame(() => reveal(line)); + } + }, [active]); + const save = async () => { + if (!tab || saving) return; + setSaving(true); + try { + const result = await desktop.save( + workspace.id, + tab.path, + tab.draft, + tab.revision, + ); + if (alive.current) + setTabs((previous) => + previous.map((t) => + t.path === tab.path + ? { ...t, content: result.content, revision: result.revision } + : t, + ), + ); + } catch (e) { + notify(errorMessage(e)); + } finally { + if (alive.current) setSaving(false); + } + }; + const saveRef = useRef(save); + saveRef.current = save; + const mounted: OnMount = (instance) => { + editor.current = instance; + instance.addCommand( + monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, + () => void saveRef.current(), + ); + if (pendingLine.current) { + reveal(pendingLine.current); + pendingLine.current = null; + } + }; + const close = async (path: string) => { + const closing = tabs.find((t) => t.path === path)!; + if ( + closing.content !== closing.draft && + !(await confirm( + "Abandonner les modifications ?", + `${path} contient des modifications non enregistrées.`, + "Abandonner", + )) + ) + return; + setTabs((previous) => previous.filter((t) => t.path !== path)); + if (active === path) + setActive(tabs.find((t) => t.path !== path)?.path ?? ""); + }; + return ( +
+ +
+
+ {tabs.map((t) => ( +
+ + +
+ ))} +
+ {tab ? ( + <> +
+ {tab.path} +
+ + +
+
+ + setTabs((previous) => + previous.map((t) => + t.path === active ? { ...t, draft: value ?? "" } : t, + ), + ) + } + options={{ + automaticLayout: true, + fontSize: 13, + fontFamily: "'Cascadia Code', 'Consolas', monospace", + minimap: { enabled: false }, + padding: { top: 18 }, + scrollBeyondLastLine: false, + smoothScrolling: true, + }} + /> +
+ {language(tab.path)} · UTF-8 + + Ctrl/Cmd + S : enregistrer · Ctrl/Cmd + F : rechercher + +
+ + ) : ( +
+ +

Votre code, au même endroit.

+

+ Sélectionnez un fichier pour le lire, le modifier ou l’analyser. +

+ {workspace.path} +
+ )} +
+
+ ); + }, +); diff --git a/desktop/main.tsx b/desktop/main.tsx index a858176..907dd8c 100644 --- a/desktop/main.tsx +++ b/desktop/main.tsx @@ -1,3 +1,9 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -createRoot(document.getElementById('root')!).render(
CoreTrace Desktop
); +import { createRoot } from "react-dom/client"; +import App from "./App"; +import { ConfirmProvider } from "./components/Dialog"; +import "./styles.css"; +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/desktop/model.ts b/desktop/model.ts new file mode 100644 index 0000000..0743353 --- /dev/null +++ b/desktop/model.ts @@ -0,0 +1,118 @@ +import type { Finding, Job, Limits } from "./types"; + +export const terminal = (job: Job) => + ["completed", "cancelled", "rejected"].includes(job.status); +export const billed = (job: Job) => + job.runs.reduce((sum, run) => sum + run.billed_ctu, 0); +const labels: Record = { + completed: "Terminée", + clean: "Aucun problème", + findings: "Problèmes détectés", + failed: "Échec", + capped: "Plafond atteint", + rejected: "Refusée", + cancelled: "Annulée", + running: "En cours", + queued: "En attente", + awaiting_confirmation: "À confirmer", + preparing: "Préparation", + metering: "Calcul CTU", + succeeded: "Terminée", +}; +export const outcome = (job: Job) => + labels[job.conclusion ?? job.status] ?? job.status.replaceAll("_", " "); +export const number = (value: number) => + new Intl.NumberFormat("fr-FR").format(value); +export const date = (value: string) => + new Date(value).toLocaleString("fr-FR", { + day: "numeric", + month: "short", + hour: "2-digit", + minute: "2-digit", + }); +export function usage(limits: Limits) { + // Missing period figures are unknown, never reconstructed from a policy ceiling. + return { + remaining: Math.max(0, limits.remaining_budget_ctu), + allowance: limits.period_allowance_ctu, + used: limits.period_used_ctu, + }; +} +export function duration(job: Job): string { + if ( + !job.runs.length || + job.runs.some((run) => !run.started_at || !run.finished_at) + ) + return "—"; + const ms = + Math.max(...job.runs.map((run) => Date.parse(run.finished_at!))) - + Math.min(...job.runs.map((run) => Date.parse(run.started_at!))); + return `${Math.max(0, Math.round(ms / 1000))} s`; +} +function record(value: unknown): value is Record { + return !!value && typeof value === "object"; +} +export function parseFindings(text: string, tool?: string): Finding[] { + const finding = (value: unknown): Finding | null => { + if ( + !record(value) || + !record(value.location) || + typeof value.message !== "string" || + typeof value.location.path !== "string" || + typeof value.location.line !== "number" + ) + return null; + return { + rule: String(value.rule_id ?? ""), + level: String(value.level ?? "note"), + message: value.message, + path: value.location.path, + line: value.location.line, + tool, + }; + }; + let parsed: unknown; + try { + parsed = JSON.parse(text); + } catch { + return text.split("\n").flatMap((line) => { + try { + const f = finding(JSON.parse(line)); + return f ? [f] : []; + } catch { + return []; + } + }); + } + if (!record(parsed)) return []; + if (Array.isArray(parsed.findings)) + return parsed.findings.map(finding).filter((f): f is Finding => f !== null); + if (Array.isArray(parsed.runs)) { + return parsed.runs.flatMap((run) => { + if (!record(run) || !Array.isArray(run.results)) return []; + return run.results.flatMap((result) => { + if ( + !record(result) || + !record(result.message) || + typeof result.message.text !== "string" + ) + return []; + const location = Array.isArray(result.locations) + ? result.locations[0]?.physicalLocation + : undefined; + return [ + { + rule: String(result.ruleId ?? ""), + level: String(result.level ?? "warning"), + message: result.message.text, + path: String(location?.artifactLocation?.uri ?? ""), + line: Number(location?.region?.startLine ?? 1), + tool, + }, + ]; + }); + }); + } + const single = finding(parsed); + return single ? [single] : []; +} diff --git a/desktop/platform-schema.d.ts b/desktop/platform-schema.d.ts new file mode 100644 index 0000000..17f7397 --- /dev/null +++ b/desktop/platform-schema.d.ts @@ -0,0 +1,2422 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/healthz": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["healthz"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/clients/cli/latest": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Latest published command-line client; clients check it at most daily and never self-update. */ + get: operations["getLatestCli"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/plans": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Current revision of every plan in tier order (community, pro, enterprise, then others by name); plans without a revision are omitted. */ + get: operations["listPlans"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/providers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listProviders"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/login/{provider}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["startLogin"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/callback/{provider}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Completes the sign-in. Browser transactions (started through /auth/login) set the refresh cookie and redirect to the server-configured landing address (CONTROL_BROWSER_LANDING_URL), with ?error= on failure; the access token never travels in the redirect and is obtained through /auth/refresh with the cookie. Command-line transactions receive the token pair as JSON. */ + get: operations["finishLogin"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/device": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["startDevice"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/device/token": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["pollDevice"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/device/approve": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["approveDevice"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["refreshSession"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["logout"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/auth/gitlab-job-token": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Exchanges a GitLab CI job token for a short-lived organisation credential bound to the organisation that linked the project. */ + post: operations["exchangeGitLabJobToken"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getMe"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createOrg"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getOrg"]; + put?: never; + post?: never; + delete: operations["deleteOrg"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listMembers"]; + put?: never; + post: operations["upsertMember"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/members/{user}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["removeMember"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/api-keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listApiKeys"]; + put?: never; + post: operations["createApiKey"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/api-keys/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["revokeApiKey"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/uploads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createUpload"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/uploads/{id}/complete": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["completeUpload"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/uploads/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getUpload"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/tools": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listTools"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/estimates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createEstimate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listJobs"]; + put?: never; + post: operations["createJob"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getJob"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["confirmJob"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["cancelJob"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}/runs/{run}/report": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getRunReport"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/jobs/{id}/runs/{run}/log": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getRunLog"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/limits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Effective plan caps and remaining budget the clients need before packing and at the confirmation prompt. */ + get: operations["getOrgLimits"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/policy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getPolicy"]; + put: operations["putPolicy"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/github/link": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["linkGitHubApp"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/scm/github/setup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["completeGitHubSetup"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/installations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listScmInstallations"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/installations/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["deleteScmInstallation"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/installations/{id}/repos": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listScmRepos"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/installations/{id}/repos/sync": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["syncScmRepos"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/installations/{id}/repos/{repo}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch: operations["updateScmRepo"]; + trace?: never; + }; + "/orgs/{org}/scm/installations/{id}/repos/{repo}/branches": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Branches of a linked repository, resolved through the installation token; at most 200. */ + get: operations["listScmBranches"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/installations/{id}/repos/{repo}/analyses": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** @description Analyse a repository at a ref. The ref is resolved to a commit and the job is created as a delivery would create it, with event kind manual. A second request for the same commit answers 409 naming the latest job for it, unless rerun is set, which makes a new analysis. */ + post: operations["createScmAnalysis"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/gitlab/connections": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createGitLabConnection"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/gitlab/connections/{id}/rotate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["rotateGitLabToken"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/scm/gitlab/connections/{id}/webhook-secret/rotate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["rotateGitLabWebhookSecret"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/orgs/{org}/audit": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listOrgAudit"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + Problem: { + /** @enum {string} */ + type: + | "unauthenticated" + | "forbidden_entitlement" + | "forbidden" + | "payment_required" + | "rate_limited" + | "conflict" + | "invalid" + | "not_found" + | "provider_unavailable" + | "internal"; + title: string; + status: number; + detail?: { + [key: string]: unknown; + }; + request_id: string; + }; + Health: { + status: string; + schema_version: string; + }; + Providers: { + providers: string[]; + }; + DeviceStart: { + device_code: string; + user_code: string; + verification_uri: string; + expires_in: number; + interval: number; + }; + DeviceTokenRequest: { + device_code: string; + }; + DeviceApproveRequest: { + user_code: string; + }; + DeviceError: { + /** @enum {string} */ + error: + | "authorization_pending" + | "slow_down" + | "expired_token" + | "invalid_grant"; + }; + Tokens: { + access_token: string; + token_type: string; + /** Format: date-time */ + expires_at: string; + /** @description Returned to CLI clients only; browsers receive it as an HttpOnly cookie. */ + refresh_token?: string; + }; + RefreshRequest: { + refresh_token?: string; + }; + Me: { + /** + * Format: uuid + * @description Present for user principals only. + */ + user_id?: string; + /** @description E-mail of the identity most recently used to sign in; user principals only. */ + email?: string; + principal: components["schemas"]["Principal"]; + orgs: components["schemas"]["OrgMembership"][]; + }; + Principal: { + /** @enum {string} */ + kind: "user" | "api_key"; + /** @description Display name for users; key name for API keys. */ + name?: string; + scopes?: string[]; + }; + Limits: { + plan: string; + /** Format: int64 */ + max_archive_bytes: number; + /** Format: int64 */ + max_expanded_bytes: number; + /** Format: int64 */ + max_files: number; + /** Format: int64 */ + max_file_bytes: number; + /** Format: int64 */ + max_ctu_per_job: number; + /** Format: int64 */ + monthly_budget_ctu: number; + /** + * Format: int64 + * @description Included plus overage minus used and reserved in the current period, further bounded by the policy's monthly budget. + */ + remaining_budget_ctu: number; + /** Format: date-time */ + period_ends_at: string; + /** + * Format: int64 + * @description The CTU the organisation may spend this period, the plan's included CTU plus any allowance granted. + */ + period_allowance_ctu?: number; + /** + * Format: int64 + * @description The CTU settled against this period so far. + */ + period_used_ctu?: number; + }; + Plan: { + name: string; + tier: string; + /** Format: int64 */ + included_ctu: number; + limits: { + [key: string]: number; + }; + entitlements: string[]; + }; + ClientRelease: { + version: string; + release_url: string; + checksums_url: string; + }; + GitLabJobTokenRequest: { + host: string; + job_token: string; + }; + GitLabJobTokenResponse: { + access_token: string; + token_type: string; + /** Format: date-time */ + expires_at: string; + /** Format: uuid */ + org_id: string; + project_id: string; + }; + OrgMembership: { + /** Format: uuid */ + id: string; + slug: string; + role: string; + access_state: string; + }; + CreateOrgRequest: { + slug: string; + }; + Org: { + /** Format: uuid */ + id: string; + slug: string; + access_state: string; + /** Format: date-time */ + created_at: string; + role?: string; + }; + Member: { + /** Format: uuid */ + user_id: string; + role: string; + }; + MemberRequest: { + /** Format: uuid */ + user_id: string; + /** @enum {string} */ + role: "owner" | "admin" | "member" | "viewer" | "billing"; + }; + ApiKey: { + /** Format: uuid */ + id: string; + public_id: string; + name: string; + scopes: string[]; + /** Format: uuid */ + created_by?: string; + /** Format: date-time */ + expires_at?: string; + /** Format: date-time */ + last_used_at?: string; + /** Format: date-time */ + revoked_at?: string; + /** Format: date-time */ + created_at: string; + }; + ApiKeyCreateRequest: { + name: string; + scopes: ("jobs:write" | "jobs:read" | "reports:read" | "uploads:write")[]; + /** Format: date-time */ + expires_at?: string; + ip_allowlist?: string[]; + }; + ApiKeyCreated: { + key: components["schemas"]["ApiKey"]; + /** @description Shown exactly once. */ + secret: string; + }; + UploadRequest: { + declared_sha256: string; + /** Format: int64 */ + declared_size: number; + }; + UploadAuthorized: { + /** Format: uuid */ + upload_id: string; + put_url: string; + /** Format: date-time */ + expires_at: string; + /** Format: int64 */ + max_bytes: number; + }; + UploadStatus: { + /** Format: uuid */ + upload_id: string; + state: string; + reject_reason?: string; + /** Format: uuid */ + input_id?: string; + /** Format: int64 */ + expanded_size?: number; + file_count?: number; + }; + Tool: { + id: string; + version: string; + required_entitlement: string; + languages: string[]; + entitled: boolean; + isolation: string; + }; + EstimateRequest: { + /** Format: uuid */ + input_id: string; + tools?: string[]; + config?: { + [key: string]: unknown; + }; + }; + EstimateAccepted: { + /** Format: uuid */ + job_id: string; + status: string; + }; + JobRequest: { + /** Format: uuid */ + input_id?: string; + quote_id?: string; + tools?: string[]; + config?: { + [key: string]: unknown; + }; + allow_partial?: boolean; + auto_confirm?: boolean; + idempotency_key?: string; + }; + RejectedTool: { + tool: string; + reason: string; + }; + ToolRun: { + /** Format: uuid */ + id: string; + tool: string; + version: string; + execution_status: string; + execution_outcome?: string; + metering_status: string; + /** Format: int64 */ + input_ctu: number; + /** Format: int64 */ + output_ctu: number; + /** Format: int64 */ + billed_ctu: number; + /** Format: int64 */ + reserved_ctu: number; + attempt: number; + /** + * Format: date-time + * @description Absent until the run starts executing. + */ + started_at?: string; + /** + * Format: date-time + * @description Absent until the run finishes. + */ + finished_at?: string; + }; + Job: { + /** Format: uuid */ + id: string; + status: string; + conclusion?: string; + rejection_reason?: string; + source?: components["schemas"]["JobSource"]; + warnings?: { + [key: string]: unknown; + }; + quote_id?: string; + /** Format: date-time */ + confirm_deadline?: string; + /** Format: date-time */ + created_at: string; + accepted?: string[]; + rejected?: components["schemas"]["RejectedTool"][]; + runs: components["schemas"]["ToolRun"][]; + }; + PolicyRequest: { + /** Format: int64 */ + max_ctu_per_job?: number; + /** Format: int64 */ + monthly_budget_ctu?: number; + /** @description Applies to SCM-triggered (automated) jobs only. */ + auto_run_enabled?: boolean; + required_tools?: string[]; + retention_days?: number; + zero_retention?: boolean; + /** + * Format: int64 + * @description Calendar-month budget for automated runs (0 = unlimited). + */ + scm_monthly_budget_ctu?: number; + /** @description Self-hosted provider hosts that may be connected; github.com and gitlab.com are implicit. */ + allowed_scm_hosts?: string[]; + /** @description IANA zone used for calendar-month budgets (default UTC). */ + org_time_zone?: string; + }; + Policy: { + version: number; + /** Format: int64 */ + max_ctu_per_job: number; + /** Format: int64 */ + monthly_budget_ctu: number; + auto_run_enabled?: boolean; + required_tools: string[]; + retention_days: number; + zero_retention: boolean; + /** Format: int64 */ + scm_monthly_budget_ctu?: number; + allowed_scm_hosts?: string[]; + org_time_zone?: string; + }; + JobSource: { + /** @enum {string} */ + kind: "upload" | "scm"; + provider?: string; + provider_host?: string; + /** @description Display only; the platform identifies repositories by provider id. */ + repo_full_name?: string; + commit_sha?: string; + event_kind?: string; + ref?: string; + pr_number?: number; + }; + ScmInstallation: { + /** Format: uuid */ + id: string; + provider: string; + provider_host: string; + account_login: string; + kind: string; + state: string; + /** Format: date-time */ + created_at: string; + repos_enabled: number; + repos_total: number; + }; + ScmRepo: { + /** Format: uuid */ + id: string; + external_repo_id: string; + full_name: string; + default_branch: string; + enabled: boolean; + summary_comments: boolean; + }; + ScmBranch: { + name: string; + commit_sha: string; + is_default: boolean; + }; + ScmAnalysisRequest: { + /** @description Branch name, tag or commit SHA. */ + ref: string; + /** @description Reserved for a future per-request selection; the repository configuration decides today. */ + tools?: string[]; + idempotency_key?: string; + /** @description Analyse the commit again even though an analysis of it exists. Earlier analyses stay in the history. */ + rerun?: boolean; + }; + ScmRepoUpdate: { + enabled: boolean; + }; + ScmLink: { + install_url: string; + /** Format: date-time */ + expires_at: string; + }; + GitLabConnectionRequest: { + provider_host: string; + project_id: string; + /** @description Project access token with read_repository and read_api; never returned. */ + token: string; + legacy_token_allowed?: boolean; + }; + GitLabConnection: { + /** Format: uuid */ + id: string; + provider: string; + provider_host: string; + project_id: string; + state: string; + webhook_url: string; + /** @description Shown once at creation and rotation. */ + webhook_secret?: string; + /** Format: date-time */ + created_at: string; + }; + GitLabTokenRotation: { + token: string; + }; + WebhookSecret: { + webhook_secret: string; + }; + AuditEntry: { + /** Format: uuid */ + id: string; + actor_type: string; + actor_id: string; + action: string; + resource_type: string; + resource_id: string; + outcome: string; + request_id: string; + metadata: { + [key: string]: unknown; + }; + /** Format: date-time */ + at: string; + }; + ArtifactLink: { + /** Format: uuid */ + artifact_id: string; + url: string; + /** Format: date-time */ + expires_at: string; + sha256: string; + /** Format: int64 */ + size: number; + }; + }; + responses: { + /** @description Error */ + Problem: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["Problem"]; + }; + }; + }; + parameters: { + OrgSlug: string; + }; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + healthz: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Service healthy */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Health"]; + }; + }; + }; + }; + getLatestCli: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Latest client release */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ClientRelease"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + listPlans: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Plans */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Plan"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + listProviders: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Configured identity providers */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Providers"]; + }; + }; + }; + }; + startLogin: { + parameters: { + query: { + redirect_uri: string; + }; + header?: never; + path: { + provider: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Redirect to the identity provider */ + 302: { + headers: { + Location?: string; + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + finishLogin: { + parameters: { + query: { + state: string; + code: string; + }; + header?: never; + path: { + provider: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session established for a command-line client */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Tokens"]; + }; + }; + /** @description Browser session established (refresh cookie set) or failed (error query parameter); Location is the server-configured landing address, never a value taken from the request */ + 302: { + headers: { + Location?: string; + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + startDevice: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Device authorisation started */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeviceStart"]; + }; + }; + }; + }; + pollDevice: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeviceTokenRequest"]; + }; + }; + responses: { + /** @description Tokens */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Tokens"]; + }; + }; + /** @description Pending, slow down or expired */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeviceError"]; + }; + }; + }; + }; + approveDevice: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["DeviceApproveRequest"]; + }; + }; + responses: { + /** @description Approved */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + refreshSession: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["RefreshRequest"]; + }; + }; + responses: { + /** @description Rotated tokens */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Tokens"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session family revoked */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + exchangeGitLabJobToken: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GitLabJobTokenRequest"]; + }; + }; + responses: { + /** @description Short-lived credential */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GitLabJobTokenResponse"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getMe: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current user */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Me"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + createOrg: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateOrgRequest"]; + }; + }; + responses: { + /** @description Organisation created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Org"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getOrg: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Organisation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Org"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + deleteOrg: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Soft-deleted */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + listMembers: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Members */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Member"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + upsertMember: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["MemberRequest"]; + }; + }; + responses: { + /** @description Member added or updated */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + removeMember: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + user: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Removed */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + listApiKeys: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Keys */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiKey"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + createApiKey: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ApiKeyCreateRequest"]; + }; + }; + responses: { + /** @description Key created; secret shown once */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiKeyCreated"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + revokeApiKey: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Revoked */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + createUpload: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UploadRequest"]; + }; + }; + responses: { + /** @description Upload authorised */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadAuthorized"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + completeUpload: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Verification scheduled */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadStatus"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getUpload: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Upload state */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UploadStatus"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + listTools: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active tools */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Tool"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + createEstimate: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EstimateRequest"]; + }; + }; + responses: { + /** @description Estimate job created; quote arrives as a job event */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["EstimateAccepted"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + listJobs: { + parameters: { + query?: { + status?: string; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Jobs */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Job"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + createJob: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["JobRequest"]; + }; + }; + responses: { + /** @description Idempotent replay of an existing job */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Job"]; + }; + }; + /** @description Job created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Job"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getJob: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Job */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Job"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + confirmJob: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Confirmed and queued */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Job"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + cancelJob: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Cancelled */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + getRunReport: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + run: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Short-lived download link (audited) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ArtifactLink"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getRunLog: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + run: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Short-lived download link to the run log (audited) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ArtifactLink"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getOrgLimits: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Effective limits */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Limits"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + getPolicy: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current policy revision */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Policy"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + putPolicy: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["PolicyRequest"]; + }; + }; + responses: { + /** @description New policy revision */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Policy"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + linkGitHubApp: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Installation URL carrying a signed state */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmLink"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + completeGitHubSetup: { + parameters: { + query: { + installation_id: string; + setup_action?: string; + state: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Installation linked (JSON when Accept is application/json) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmInstallation"]; + }; + }; + /** @description Redirect to the installation page */ + 302: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + listScmInstallations: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Installations and connections of the organisation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmInstallation"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + deleteScmInstallation: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Installation revoked */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + listScmRepos: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Repositories of the installation */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmRepo"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + syncScmRepos: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Repositories after re-listing from the provider */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmRepo"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + updateScmRepo: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + repo: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ScmRepoUpdate"]; + }; + }; + responses: { + /** @description Repository view */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmRepo"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + listScmBranches: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + repo: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Branches */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ScmBranch"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + createScmAnalysis: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + repo: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ScmAnalysisRequest"]; + }; + }; + responses: { + /** @description Analysis accepted */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Job"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + createGitLabConnection: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GitLabConnectionRequest"]; + }; + }; + responses: { + /** @description Connection created; the webhook secret is shown once */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GitLabConnection"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + rotateGitLabToken: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["GitLabTokenRotation"]; + }; + }; + responses: { + /** @description Token replaced */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + default: components["responses"]["Problem"]; + }; + }; + rotateGitLabWebhookSecret: { + parameters: { + query?: never; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description New webhook secret (shown once; the previous one stays valid 24 h) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebhookSecret"]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; + listOrgAudit: { + parameters: { + query?: { + action?: string; + limit?: number; + before?: string; + }; + header?: never; + path: { + org: components["parameters"]["OrgSlug"]; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Audit entries, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuditEntry"][]; + }; + }; + default: components["responses"]["Problem"]; + }; + }; +} diff --git a/desktop/styles.css b/desktop/styles.css new file mode 100644 index 0000000..7fc0807 --- /dev/null +++ b/desktop/styles.css @@ -0,0 +1,1332 @@ +:root { + font-family: Inter, "Segoe UI", sans-serif; + color: #e5e6e8; + background: #191a1c; + font-synthesis: none; + font-size: 14px; + --bg: #191a1c; + --panel: #202123; + --line: #303134; + --muted: #95989f; + --accent: #b9dfc9; + --sidebar: #131416; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +button, +input, +select { + font: inherit; +} +button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border: 1px solid var(--line); + border-radius: 8px; + color: inherit; + background: #28292c; + padding: 9px 13px; + cursor: pointer; + transition: + background 0.15s, + border-color 0.15s; +} +button:hover:not(:disabled) { + background: #323437; + border-color: #53565b; +} +button:disabled { + opacity: 0.4; + cursor: not-allowed; +} +button:focus-visible, +input:focus-visible, +select:focus-visible, +summary:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +input, +select { + min-width: 0; + background: #191a1c; + border: 1px solid #393b3f; + border-radius: 7px; + color: inherit; + padding: 10px 12px; +} +input::placeholder { + color: #6f737a; +} +input[type="checkbox"] { + accent-color: var(--accent); + width: 16px; + height: 16px; +} +label { + display: flex; + flex-direction: column; + gap: 8px; + color: #bec0c5; + font-size: 12px; +} +h1, +h2, +h3, +p { + margin: 0; +} +h1 { + font-size: 32px; + letter-spacing: -1.1px; + font-weight: 550; + line-height: 1.3; +} +h2 { + font-size: 16px; + font-weight: 550; + letter-spacing: -0.2px; +} +h3 { + font-size: 16px; + font-weight: 550; +} +p { + line-height: 1.65; +} +strong { + font-weight: 550; +} +svg { + flex-shrink: 0; +} +code, +pre { + font-family: "Cascadia Code", Consolas, monospace; +} +pre { + white-space: pre-wrap; + overflow-wrap: anywhere; + max-height: 400px; + overflow: auto; + font-size: 12px; + background: #121315; + padding: 16px; + border-radius: 8px; +} +small, +.small { + font-size: 12px; +} +.muted { + color: var(--muted); +} +.inline { + display: flex; + align-items: center; + gap: 9px; +} +.grow { + flex: 1; + min-width: 0; +} +.primary { + background: var(--accent); + border-color: var(--accent); + color: #1a3225; + font-weight: 600; +} +.primary:hover:not(:disabled) { + background: #ceebda; + border-color: #ceebda; +} +.icon { + padding: 6px; + background: transparent; + border-color: transparent; + flex-shrink: 0; +} +.text-button { + background: transparent; + border-color: transparent; + padding: 5px; + color: #b6b9bf; + font-size: 12px; +} +.status-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: #9ed5b4; + flex-shrink: 0; +} +.status-dot.offline { + background: #888b92; +} +.eyebrow { + font-size: 10px; + letter-spacing: 1.6px; + color: #93989f; + display: flex; + align-items: center; + gap: 7px; + margin-bottom: 16px; +} +.spin { + animation: spin 1.5s linear infinite; +} +@keyframes spin { + to { + transform: rotate(360deg); + } +} +.app-shell { + height: 100vh; + display: grid; + grid-template-columns: 242px minmax(0, 1fr); + overflow: hidden; +} +.sidebar { + background: var(--sidebar); + padding: 24px 13px 12px; + display: flex; + flex-direction: column; + border-right: 1px solid #282a2c; + min-height: 0; +} +.brand { + display: flex; + align-items: center; + gap: 10px; + padding: 0 8px 28px; + font-size: 19px; + font-weight: 600; + letter-spacing: -0.5px; +} +.brand-icon { + width: 33px; + height: 37px; + display: grid; + place-items: center; + color: var(--accent); +} +.brand-caption { + display: block; + color: #646a71; + letter-spacing: 2.4px; + font-size: 8px; + margin-top: 3px; +} +.collapse-button { + margin-left: auto; + color: #72777e; +} +.new-analysis { + margin: 0 1px 27px; + background: #242628; + justify-content: flex-start; + font-size: 12px; + padding: 11px; + border-color: #393c3f; + border-radius: 7px; +} +.nav-label { + padding: 0 11px; + color: #61676f; + letter-spacing: 1.3px; + font-size: 9px; + margin-bottom: 11px; +} +.sidebar nav { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 28px; +} +.sidebar nav button { + border: none; + background: transparent; + color: #9ca1a8; + justify-content: flex-start; + padding: 11px 12px; + font-size: 12px; + gap: 12px; + min-height: 39px; +} +.sidebar nav button.active { + background: #282b2d; + color: #ecf1ed; +} +.sidebar nav button.active svg { + color: var(--accent); +} +.nav-count { + margin-left: auto; + color: #91999e; + font-size: 10px; +} +.sidebar-workspace { + margin-top: 3px; + overflow: hidden; +} +.sidebar-workspace > button { + background: transparent; + border: 0; + width: 100%; + justify-content: flex-start; + color: #aaafb6; + font-size: 12px; +} +.sidebar-workspace > button span:not(.dirty-dot) { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.sidebar-bottom { + margin-top: auto; + padding-top: 20px; +} +.cloud-card { + padding: 14px 12px; + border: 1px solid #2b2e30; + border-radius: 8px; + margin-bottom: 15px; + background: linear-gradient(125deg, #22282a55, transparent); +} +.cloud-card > span { + font-size: 11px; +} +.cloud-card p { + font-size: 10px; + color: #7b838a; + margin-top: 6px; + overflow: hidden; + text-overflow: ellipsis; +} +.profile { + background: transparent; + border: 0; + justify-content: flex-start; + width: 100%; + padding: 8px; + gap: 10px; + text-align: left; +} +.avatar { + display: grid; + place-items: center; + background: #333633; + border: 1px solid #494e48; + color: #c8d2c8; + border-radius: 50%; + width: 32px; + height: 32px; + font-size: 10px; +} +.profile strong { + font-size: 11px; + display: block; +} +.profile small { + color: #727a82; + font-size: 10px; + display: block; + margin-top: 4px; +} +.main-shell { + display: flex; + flex-direction: column; + min-width: 0; + height: 100vh; +} +.topbar { + height: 62px; + flex-shrink: 0; + padding: 0 30px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid #2b2d2f; + gap: 12px; +} +.breadcrumbs { + font-size: 11px; + display: flex; + align-items: center; + gap: 12px; + color: #848991; + white-space: nowrap; + overflow: hidden; +} +.breadcrumbs strong { + font-weight: 400; + color: #d0d3d7; +} +.breadcrumbs > span { + color: #4c5158; +} +.org-picker { + flex-direction: row; + align-items: center; + gap: 4px; +} +.org-picker select { + background: transparent; + border: 0; + font-size: 11px; + max-width: 175px; +} +.main-content { + flex: 1; + overflow: auto; + position: relative; + min-height: 0; +} +.page { + max-width: 1170px; + margin: 0 auto; + padding: 42px 46px 64px; +} +.app-status { + border-top: 1px solid #292c2e; + height: 27px; + flex-shrink: 0; + padding: 0 20px; + display: flex; + align-items: center; + justify-content: space-between; + color: #6f7881; + font-size: 9px; +} +.app-status > span { + display: flex; + align-items: center; + gap: 6px; +} +.preview-notice { + background: #26312f; + color: #b0c9bd; + padding: 7px 25px; + font-size: 11px; + flex-shrink: 0; +} +.operation { + padding: 10px 25px; + font-size: 12px; + display: flex; + align-items: center; + gap: 10px; + background: #27312b; +} +.global-error { + padding: 12px 25px; + color: #e5bcb7; + background: #382724; + font-size: 12px; + display: flex; + align-items: center; + gap: 15px; +} +.global-error button { + margin-left: auto; +} +.page-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + margin-bottom: 30px; +} +.page-heading p { + color: var(--muted); + font-size: 13px; + margin-top: 9px; +} +.section-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + margin: 28px 0 16px; +} +.section-heading p { + margin-top: 6px; +} +.section-heading:first-child { + margin-top: 0; +} +.hero { + display: flex; + justify-content: space-between; + gap: 20px; + align-items: center; + margin: 24px 0 32px; +} +.hero h1 { + font-size: 33px; + font-weight: 500; + line-height: 1.5; + letter-spacing: -1.2px; +} +.hero h1 span { + color: #b4b6ba; + font-weight: 400; +} +.hero p { + color: #858b94; + font-size: 12px; + margin-top: 10px; +} +.hero-mark { + display: grid; + place-items: center; + width: 110px; + height: 110px; + color: #889e94; + background: radial-gradient(ellipse, #34453d44, transparent 68%); + transform: rotate(-8deg); +} +.connection-banner { + padding: 16px 19px; + border: 1px solid #35463d; + border-radius: 10px; + background: #202a2544; + display: flex; + align-items: center; + gap: 20px; + justify-content: space-between; + margin-bottom: 26px; +} +.connection-banner strong { + font-size: 12px; +} +.connection-banner p { + color: #8e9b94; + font-size: 11px; + margin-top: 4px; +} +.connection-banner button { + flex-shrink: 0; + background: #2a3930; + font-size: 11px; + border-color: #3e5145; +} +.action-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 34px; +} +.action-card { + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + padding: 21px 19px 20px; + border: 1px solid #373a3c; + border-radius: 10px; + background: linear-gradient(140deg, #272a2b, #202224); + min-height: 181px; + gap: 12px; +} +.action-card:hover:not(:disabled) { + background: linear-gradient(140deg, #2c3430, #252829); + border-color: #667b6b; +} +.action-icon { + width: 40px; + height: 40px; + display: grid; + place-items: center; + border-radius: 10px; + margin-bottom: 3px; +} +.mint { + color: #a5d3ba; + background: #2d4236; +} +.lilac { + color: #b9acdf; + background: #383241; +} +.peach { + color: #d6b995; + background: #40382d; +} +.action-card strong { + font-size: 13px; +} +.action-card > span:not(.action-icon) { + font-size: 11px; + line-height: 1.7; + color: #8b929a; + max-width: 92%; +} +.action-card > svg:last-child { + position: absolute; + top: 23px; + right: 17px; + color: #727a82; + width: 15px; +} +.metrics { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + border: 1px solid #333639; + border-radius: 10px; + background: #202224; +} +.metrics article { + padding: 22px 20px; + display: flex; + flex-direction: column; + gap: 12px; +} +.metrics article + article { + border-left: 1px solid #333639; +} +.metrics article > span:first-child { + font-size: 11px; +} +.metrics strong { + font-size: 29px; + letter-spacing: -0.8px; +} +.metrics strong small { + font-size: 12px; + font-weight: 400; + color: var(--muted); +} +.metrics .small { + font-size: 10px; + display: flex; + gap: 5px; + align-items: center; +} +.plan-name { + text-transform: capitalize; +} +.meter { + height: 3px; + border-radius: 2px; + background: #3a403c; + overflow: hidden; + margin-top: -3px; +} +.meter > span { + display: block; + height: 100%; + background: #a5ccb3; +} +.job-rows { + border: 1px solid #303437; + border-radius: 10px; + overflow: hidden; + background: #1e2022; +} +.job-row { + display: flex; + width: 100%; + text-align: left; + background: transparent; + border: 0; + border-radius: 0; + gap: 15px; + padding: 17px 18px; +} +.job-row + .job-row { + border-top: 1px solid #2c3033; +} +.job-symbol { + width: 34px; + height: 34px; + border: 1px solid #3a4142; + border-radius: 8px; + display: grid; + place-items: center; + color: #9baaab; + background: #282d2e; +} +.job-row .grow { + display: flex; + flex-direction: column; + gap: 6px; +} +.job-row strong { + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; +} +.job-row .small { + font-size: 10px; +} +.cost { + color: #9ba3aa; + font-size: 11px; + min-width: 65px; + text-align: right; +} +.badge { + display: inline-flex; + align-items: center; + gap: 5px; + width: fit-content; + font-size: 10px; + border: 1px solid #3b4247; + border-radius: 5px; + padding: 4px 7px; + white-space: nowrap; + color: #bbc2c7; + background: #292e33; +} +.badge.clean, +.badge.completed { + background: #23392d; + border-color: #365340; + color: #b0d7be; +} +.badge.findings, +.badge.warning, +.badge.capped { + background: #3c3225; + border-color: #59482c; + color: #d9be91; +} +.badge.failed, +.badge.error, +.badge.rejected { + background: #3c2828; + border-color: #573838; + color: #dfadad; +} +.badge.running, +.badge.queued { + color: #b7bfdc; + background: #2c3044; +} +.resume-card { + width: 100%; + text-align: left; + padding: 17px 20px; + margin-top: 24px; + background: #202525; + border-color: #34403c; +} +.resume-card .grow { + display: flex; + flex-direction: column; + gap: 6px; +} +.empty { + padding: 34px 24px; + text-align: center; + border: 1px dashed #353b3f; + border-radius: 10px; + color: #939ca3; + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} +.empty h2, +.empty h1 { + color: #d0d7dc; +} +.empty p { + max-width: 450px; + font-size: 12px; +} +.empty > svg { + color: #81988d; + margin-bottom: 5px; +} +.centered { + height: 100%; + justify-content: center; + border: 0; + min-height: 300px; +} +.panel { + background: #202224; + border: 1px solid #33373a; + border-radius: 11px; + padding: 23px; + margin: 22px 0; +} +.panel > h2 { + margin-bottom: 15px; +} +.panel > p { + margin: 14px 0; +} +.run-form { + display: grid; + grid-template-columns: 1fr 1fr auto; + gap: 16px; + align-items: end; +} +.checkbox { + flex-direction: row; + align-items: center; + margin: 18px 0 4px; + font-size: 11px; +} +.notice { + background: #333021; + border: 1px solid #514b2b; + border-radius: 7px; + padding: 13px 16px; + color: #c9c199; + font-size: 12px; + margin: 17px 0; +} +.error { + color: #e6aaa5; + font-size: 12px; + padding: 10px 0; + overflow-wrap: anywhere; +} +.repository-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} +.repository-card { + border: 1px solid #383c3f; + border-radius: 10px; + background: #222426; + padding: 22px; +} +.repository-card > .inline { + justify-content: space-between; + color: #a1b6a8; +} +.repository-card h3 { + margin: 25px 0 9px; + overflow-wrap: anywhere; +} +.repository-card footer { + display: flex; + gap: 8px; + margin-top: 24px; +} +.repository-card button { + font-size: 11px; +} +.repository-card p { + display: flex; + align-items: center; + gap: 6px; +} +.analysis-summary { + display: flex; + align-items: center; + gap: 20px; + background: #24282a; + padding: 13px; + border: 1px solid #373d40; + border-radius: 9px; + color: #a9b3bb; + font-size: 12px; + flex-wrap: wrap; + margin-bottom: 25px; +} +.finding { + padding: 18px; + margin-bottom: 12px; + background: #222527; + border: 1px solid #363c40; + border-left: 3px solid #768b9b; + border-radius: 7px; +} +.finding.warning { + border-left-color: #c6a167; +} +.finding.error { + border-left-color: #cb7773; + color: inherit; +} +.finding p { + font-size: 12px; + padding: 12px 0; +} +.finding .text-button { + overflow-wrap: anywhere; + white-space: normal; + text-align: left; +} +.search { + flex-direction: row; + align-items: center; + border: 1px solid #343b3f; + border-radius: 8px; + padding: 0 10px; +} +.search input { + background: transparent; + border: 0; + font-size: 11px; +} +.tool-runs { + border: 1px solid #353d43; + border-radius: 8px; + margin-top: 25px; + padding: 16px; +} +summary { + cursor: pointer; + font-size: 12px; + color: #aeb9c0; +} +.tool-run { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + padding: 15px 0; + border-top: 1px solid #32383e; + margin-top: 12px; + font-size: 12px; +} +.tool-run p { + width: 100%; +} +.settings { + max-width: 920px; +} +.setting-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 18px 0; + border-bottom: 1px solid #35393d; +} +.setting-row p { + font-size: 12px; + margin-top: 5px; +} +.path-value { + font-size: 11px; + overflow-wrap: anywhere; +} +.setting-row code { + font-size: 11px; +} +.settings > .panel > .inline { + margin-bottom: 12px; +} +.workspace-host { + height: 100%; + min-height: 0; +} +.workspace-host[hidden] { + display: none; +} +.ide { + display: grid; + grid-template-columns: 235px minmax(0, 1fr); + height: 100%; + min-height: 0; +} +.file-explorer { + overflow: auto; + background: #181a1c; + border-right: 1px solid #2d3236; + resize: horizontal; + min-width: 150px; + max-width: 400px; +} +.explorer-heading { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px; + color: #aaaeb4; + font-size: 11px; + border-bottom: 1px solid #2d3236; + height: 43px; +} +.explorer-heading > span { + display: flex; + gap: 8px; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.tree-row { + display: flex; + width: 100%; + justify-content: flex-start; + border: 0; + border-radius: 0; + background: transparent; + font-size: 11px; + padding-top: 7px; + padding-bottom: 7px; + gap: 6px; + color: #a7b0b8; +} +.tree-row > span:last-child { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.tree-row.selected { + background: #2c3733; + color: #d8e7db; +} +.tree-indent { + width: 12px; + flex-shrink: 0; +} +.editor-area { + display: flex; + flex-direction: column; + min-width: 0; + overflow: hidden; + background: #1e1e1e; +} +.editor-tabs { + min-height: 42px; + display: flex; + overflow-x: auto; + background: #181a1c; + border-bottom: 1px solid #303538; + flex-shrink: 0; +} +.editor-tab { + display: flex; + border-right: 1px solid #303538; + padding: 0 6px; + font-size: 11px; +} +.editor-tab > button:not(.icon) { + border: 0; + border-radius: 0; + background: transparent; + font-size: 11px; + white-space: nowrap; +} +.editor-tab.active { + background: #24292a; + border-top: 2px solid #aacbb6; +} +.dirty-dot { + width: 6px; + height: 6px; + background: #d3bb8d; + border-radius: 50%; + display: inline-block; + flex-shrink: 0; +} +.editor-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 16px; + border-bottom: 1px solid #303538; + font-size: 10px; + gap: 10px; + flex-shrink: 0; +} +.editor-toolbar > span { + color: #86939b; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.editor-toolbar button { + font-size: 10px; + padding: 6px 9px; +} +.editor-area > section { + min-height: 0; + flex: 1; +} +.editor-status { + height: 26px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 15px; + font-size: 9px; + color: #7f8b94; + border-top: 1px solid #303538; + flex-shrink: 0; +} +.toast { + position: fixed; + bottom: 42px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 15px; + border: 1px solid #625146; + background: #322e29; + padding: 14px 16px; + border-radius: 9px; + box-shadow: 0 10px 35px #0008; + max-width: 700px; + min-width: 320px; + z-index: 30; + font-size: 12px; + line-height: 1.6; +} +.toast > span { + overflow-wrap: anywhere; +} +dialog { + background: #242729; + color: #e0e5e8; + border: 1px solid #454e53; + border-radius: 14px; + padding: 25px; + width: min(510px, 90vw); + box-shadow: 0 25px 90px #0008; +} +dialog::backdrop { + background: #080c10b3; + backdrop-filter: blur(4px); +} +dialog header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; +} +dialog p { + font-size: 12px; + color: #a5b0b8; + margin: 14px 0; +} +dialog footer { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 24px; +} +dialog label { + margin-top: 20px; +} +.device-code { + font: + 600 32px "Cascadia Code", + monospace; + text-align: center; + letter-spacing: 7px; + padding: 25px; + background: #191d20; + border: 1px solid #3e4b52; + border-radius: 9px; + margin-top: 20px; +} +.sidebar-collapsed { + grid-template-columns: 72px minmax(0, 1fr); +} +.sidebar-collapsed .sidebar { + padding-left: 8px; + padding-right: 8px; +} +.sidebar-collapsed .brand { + padding: 0 0 20px; + flex-direction: column; + gap: 3px; +} +.sidebar-collapsed .brand > span, +.sidebar-collapsed .nav-label, +.sidebar-collapsed nav button > span, +.sidebar-collapsed .new-analysis > span, +.sidebar-collapsed .sidebar-workspace, +.sidebar-collapsed .cloud-card, +.sidebar-collapsed .profile > span:not(.avatar) { + display: none; +} +.sidebar-collapsed .collapse-button { + margin: 0; +} +.sidebar-collapsed .new-analysis, +.sidebar-collapsed nav button { + justify-content: center; + padding: 11px; +} +.sidebar-collapsed .profile { + justify-content: center; +} +@media (min-width: 1500px) { + .page { + padding-top: 55px; + } + .dashboard { + max-width: 1150px; + } + .hero { + margin-bottom: 40px; + } + .action-card { + min-height: 194px; + } +} +@media (max-width: 1100px) { + .app-shell { + grid-template-columns: 205px minmax(0, 1fr); + } + .page { + padding: 30px 25px 45px; + } + .topbar { + padding: 0 20px; + } + .hero h1 { + font-size: 28px; + } + .hero-mark { + display: none; + } + .action-card { + padding: 17px 13px; + } + .action-card strong { + font-size: 12px; + } + .action-card > span:not(.action-icon) { + font-size: 10px; + } + .web-link { + display: none; + } + .metrics article { + padding: 18px 13px; + } + .metrics strong { + font-size: 24px; + } + .ide { + grid-template-columns: 190px minmax(0, 1fr); + } + .sidebar-collapsed { + grid-template-columns: 72px minmax(0, 1fr); + } +} +@media (max-width: 800px) { + .app-shell { + grid-template-columns: 72px minmax(0, 1fr); + } + .sidebar { + padding: 20px 8px; + } + .brand { + padding: 0 0 20px; + justify-content: center; + } + .brand > span, + .collapse-button, + .nav-label, + .sidebar nav button > span, + .new-analysis > span, + .sidebar-workspace, + .cloud-card, + .profile > span:not(.avatar) { + display: none; + } + .sidebar nav button, + .new-analysis, + .profile { + justify-content: center; + padding: 11px; + } + .page { + padding: 25px 20px; + } + .action-grid { + grid-template-columns: 1fr; + } + .action-card { + min-height: 120px; + padding-left: 75px; + justify-content: center; + } + .action-icon { + position: absolute; + left: 18px; + top: 25px; + } + .action-card > svg:last-child { + top: 30px; + } + .metrics { + grid-template-columns: 1fr; + } + .metrics article + article { + border-left: 0; + border-top: 1px solid #343a3d; + } + .metrics article { + padding: 18px; + } + .connection-banner { + flex-direction: column; + align-items: flex-start; + } + .repository-grid { + grid-template-columns: 1fr; + } + .run-form { + grid-template-columns: 1fr; + } + .section-heading { + flex-wrap: wrap; + } + .section-heading > .inline { + flex-wrap: wrap; + } + .job-row .cost { + display: none; + } + .job-row { + gap: 9px; + padding: 13px 10px; + } + .job-symbol { + display: none; + } + .topbar { + padding: 0 15px; + } + .breadcrumbs { + font-size: 10px; + } + .page-heading { + align-items: flex-start; + flex-direction: column; + } + .ide { + grid-template-columns: 155px minmax(0, 1fr); + } + .editor-status > span:last-child { + display: none; + } + .setting-row { + flex-wrap: wrap; + } + .toast { + min-width: 0; + width: 85%; + } +} +@media (prefers-reduced-motion: reduce) { + * { + animation: none !important; + transition: none !important; + scroll-behavior: auto !important; + } +} diff --git a/desktop/types.ts b/desktop/types.ts new file mode 100644 index 0000000..06341c4 --- /dev/null +++ b/desktop/types.ts @@ -0,0 +1,53 @@ +import type { components } from "./platform-schema"; + +export type Me = components["schemas"]["Me"]; +export type Limits = components["schemas"]["Limits"]; +export type Job = components["schemas"]["Job"]; +export type Tool = components["schemas"]["Tool"]; +export type Installation = components["schemas"]["ScmInstallation"]; +export type Repository = components["schemas"]["ScmRepo"] & { + installation: string; +}; +export type Member = components["schemas"]["Member"]; +export interface Workspace { + id: string; + name: string; + path: string; +} +export interface FileEntry { + name: string; + path: string; + directory: boolean; +} +export interface Document { + content: string; + revision: string; +} +export interface LocalResult { + exitCode: number | null; + stdout: string; + stderr: string; + report: string | null; + cancelled: boolean; +} +export interface DeviceCode { + userCode: string; + verificationUri: string; + interval: number; + expiresIn: number; +} +export interface Finding { + rule: string; + level: string; + path: string; + line: number; + message: string; + tool?: string; +} +export type Page = + | "home" + | "organisation" + | "analyses" + | "repositories" + | "workspace" + | "settings"; diff --git a/desktop/useCloud.ts b/desktop/useCloud.ts new file mode 100644 index 0000000..1a4c72b --- /dev/null +++ b/desktop/useCloud.ts @@ -0,0 +1,131 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { desktop, errorMessage } from "./bridge"; +import type { Installation, Job, Limits, Me, Repository, Tool } from "./types"; + +interface Data { + limits: Limits | null; + jobs: Job[]; + repositories: Repository[]; + tools: Tool[]; +} +const empty = (): Data => ({ + limits: null, + jobs: [], + repositories: [], + tools: [], +}); +export function useCloud() { + const [me, setMe] = useState(null); + const [org, setOrg] = useState(""); + const [data, setData] = useState(empty); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + const [baseUrl, setBaseUrl] = useState(""); + const epoch = useRef(0); + const reconnect = useCallback(async () => { + const status = await desktop.status(); + setBaseUrl(status.baseUrl); + if (status.signedIn) { + const identity = await desktop.readCloud("me"); + setMe(identity); + setOrg((previous) => + identity.orgs.some((o) => o.slug === previous) + ? previous + : (identity.orgs[0]?.slug ?? ""), + ); + } else { + setMe(null); + setOrg(""); + } + }, []); + useEffect(() => { + void reconnect().catch((e) => setError(errorMessage(e))); + }, [reconnect]); + const refresh = useCallback(async () => { + const current = ++epoch.current; + if (!me || !org) { + setData(empty()); + setLoading(false); + return; + } + setLoading(true); + setError(""); + try { + const results = await Promise.allSettled([ + desktop.readCloud("limits", org), + desktop.readCloud("jobs", org), + desktop + .readCloud("installations", org) + .then(async (installs) => { + const repos = await Promise.all( + installs + .filter( + (i) => + i.provider === "github" && i.provider_host === "github.com", + ) + .map(async (i) => { + const rows = await desktop.readCloud( + "repositories", + org, + i.id, + ); + return rows.map((row) => ({ ...row, installation: i.id })); + }), + ); + return repos.flat(); + }), + desktop.readCloud("tools", org), + ] as const); + if (current !== epoch.current) return; + const [limits, jobs, repositories, tools] = results; + setData({ + limits: limits.status === "fulfilled" ? limits.value : null, + jobs: + jobs.status === "fulfilled" + ? jobs.value.sort( + (a, b) => Date.parse(b.created_at) - Date.parse(a.created_at), + ) + : [], + repositories: + repositories.status === "fulfilled" ? repositories.value : [], + tools: tools.status === "fulfilled" ? tools.value : [], + }); + setError( + results + .flatMap((r) => + r.status === "rejected" ? [errorMessage(r.reason)] : [], + ) + .join(" · "), + ); + } finally { + if (current === epoch.current) setLoading(false); + } + }, [me, org]); + useEffect(() => { + setData(empty()); + void refresh(); + return () => { + epoch.current++; + }; + }, [refresh]); + const signOut = async () => { + epoch.current++; + setMe(null); + setOrg(""); + setData(empty()); + await desktop.logout(); + }; + return { + me, + org, + setOrg, + ...data, + loading, + error, + baseUrl, + refresh, + reconnect, + signOut, + }; +} +export type CloudModel = ReturnType; diff --git a/index.html b/index.html index 9ee82f2..445d905 100644 --- a/index.html +++ b/index.html @@ -1,2 +1,12 @@ -CoreTrace
+ + + + + CoreTrace + + +
+ + + diff --git a/package-lock.json b/package-lock.json index 287d074..de87c35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.7.0", "jsdom": "^26.1.0", + "prettier": "^3.9.6", "typescript": "~5.9.2", "vite": "^6.4.1", "vitest": "^3.2.4" @@ -2161,6 +2162,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/package.json b/package.json index 9e75d30..a9e2134 100644 --- a/package.json +++ b/package.json @@ -25,12 +25,13 @@ }, "devDependencies": { "@tauri-apps/cli": "^2.8.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", "@vitejs/plugin-react": "^4.7.0", - "@testing-library/react": "^16.3.0", - "@testing-library/user-event": "^14.6.1", "jsdom": "^26.1.0", + "prettier": "^3.9.6", "typescript": "~5.9.2", "vite": "^6.4.1", "vitest": "^3.2.4" diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index 98eb3cd..b810312 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -1 +1,7 @@ -{"$schema":"../gen/schemas/desktop-schema.json","identifier":"main","description":"Local application window only; native commands own workspace and network access.","windows":["main"],"permissions":["core:default"]} +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "main", + "description": "Local application window only; native commands own workspace and network access.", + "windows": ["main"], + "permissions": ["core:default", "core:window:allow-destroy"] +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ae668fb..0a04d35 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -3,10 +3,34 @@ "productName": "CoreTrace", "version": "6.0.0-beta.1", "identifier": "fr.coretrace.desktop", - "build": { "beforeDevCommand": "npm run dev", "devUrl": "http://127.0.0.1:1420", "beforeBuildCommand": "npm run build", "frontendDist": "../dist" }, + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://127.0.0.1:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, "app": { - "windows": [{ "label": "main", "title": "CoreTrace", "width": 1440, "height": 940, "minWidth": 880, "minHeight": 600 }], - "security": { "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost; worker-src 'self' blob:" } + "windows": [ + { + "label": "main", + "title": "CoreTrace", + "width": 1440, + "height": 940, + "minWidth": 880, + "minHeight": 600 + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src ipc: http://ipc.localhost; worker-src 'self' blob:" + } }, - "bundle": { "active": true, "targets": "all", "icon": ["../assets/ctrace.png", "../assets/ctrace.ico", "../assets/ctrace.icns"] } + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "../assets/ctrace.png", + "../assets/ctrace.ico", + "../assets/ctrace.icns" + ] + } } diff --git a/tsconfig.json b/tsconfig.json index 89a89ad..b0f95bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,17 @@ { "compilerOptions": { - "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react-jsx", - "strict": true, "noEmit": true, "skipLibCheck": true, - "allowImportingTsExtensions": true, "resolveJsonModule": true, - "esModuleInterop": true, "types": ["vite/client"] + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "types": ["vite/client"] }, "include": ["desktop", "vite.config.ts", "vitest.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 67c624c..455cfc4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,12 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], - server: { port: 1420, strictPort: true, watch: { ignored: ['**/src-tauri/**', '**/native/**'] } }, - build: { outDir: 'dist', target: ['es2022', 'chrome105', 'safari15'] }, + server: { + port: 1420, + strictPort: true, + watch: { ignored: ["**/src-tauri/**", "**/native/**"] }, + }, + build: { outDir: "dist", target: ["es2022", "chrome105", "safari15"] }, clearScreen: false, }); diff --git a/vitest.config.ts b/vitest.config.ts index ac24363..8c7875c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,3 +1,6 @@ -import { defineConfig } from 'vitest/config'; -import react from '@vitejs/plugin-react'; -export default defineConfig({ plugins: [react()], test: { include: ['desktop/**/*.test.{ts,tsx}'], environment: 'jsdom' } }); +import { defineConfig } from "vitest/config"; +import react from "@vitejs/plugin-react"; +export default defineConfig({ + plugins: [react()], + test: { include: ["desktop/**/*.test.{ts,tsx}"], environment: "jsdom" }, +}); From 0956bc03c3278db69d1d3a78a59a11cce2e71644 Mon Sep 17 00:00:00 2001 From: shookapic Date: Tue, 8 Sep 2026 15:24:10 +0200 Subject: [PATCH 04/66] test(desktop): verify migration and document native workflows --- .github/workflows/release.yml | 154 ++++--------- README.md | 255 ++++++++-------------- desktop/App.tsx | 34 ++- desktop/features/Analyses.test.tsx | 121 ++++++++++ desktop/features/Analyses.tsx | 24 +- desktop/features/WorkspaceEditor.test.tsx | 124 +++++++++++ desktop/model.test.ts | 144 ++++++++++++ desktop/model.ts | 41 +++- desktop/useCloud.test.tsx | 76 +++++++ docs/desktop-validation.md | 44 ++++ docs/electron-readme.md | 162 ++++++++++++++ docs/tauri-migration.md | 5 +- package.json | 4 +- scripts/check-desktop-version.cjs | 18 ++ src-tauri/Cargo.toml | 7 +- src-tauri/build.rs | 4 +- src-tauri/src/analysis.rs | 232 +++++++++++++++++--- src-tauri/src/cloud.rs | 106 ++++++++- src-tauri/src/main.rs | 2 +- src-tauri/src/workspace.rs | 76 ++++++- src-tauri/tests/fixtures/ctrace.rs | 14 ++ 21 files changed, 1307 insertions(+), 340 deletions(-) create mode 100644 desktop/features/Analyses.test.tsx create mode 100644 desktop/features/WorkspaceEditor.test.tsx create mode 100644 desktop/model.test.ts create mode 100644 desktop/useCloud.test.tsx create mode 100644 docs/desktop-validation.md create mode 100644 docs/electron-readme.md create mode 100644 scripts/check-desktop-version.cjs create mode 100644 src-tauri/tests/fixtures/ctrace.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8eee046..6099af1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,121 +1,55 @@ -name: Build Electron App - -# Pushes to master and develop only run the test suite. Artifacts are published -# from version tags alone (v5.1.0, v5.2.0-beta.1, ...), so merging a PR never -# republishes a release, and the tag is the single trigger for a build. +name: CoreTrace Desktop on: push: - branches: - - master - - develop - tags: - - "v*" + branches: [master, develop, feat/tauri-react-desktop] + tags: ["v*"] pull_request: - branches: - - master - + branches: [master] + workflow_dispatch: permissions: contents: read - jobs: - test: - runs-on: ubuntu-24.04 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22.15.0 - cache: "npm" - - - name: Install dependencies - run: npm ci - - - name: Build renderer bundle - run: npm run build:renderer - - - name: Run tests - run: npm test - - release: - if: startsWith(github.ref, 'refs/tags/v') - needs: test - # Only this job publishes, so only this job gets a write-capable token. - permissions: - contents: write - runs-on: ${{ matrix.os }} + desktop: strategy: fail-fast: false matrix: - # windows-2022 pinned: windows-latest (now windows-2025, build 26100) - # breaks node-gyp VS detection during the node-pty native rebuild. - # - # macOS ships two jobs rather than one universal build: node-pty and - # node-llama-cpp are native modules, so each architecture is compiled on - # a runner of that architecture. macos-latest is Apple Silicon; - # macos-15-intel is the x64 runner that replaced the retired macos-13 - # (a macos-13 job sits queued forever instead of failing). - # The builds are unsigned — see the note below. - os: [windows-2022, ubuntu-24.04, macos-latest, macos-15-intel] - + os: [windows-2022, ubuntu-24.04, macos-latest] + runs-on: ${{ matrix.os }} steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22.19.0" + cache: npm + - uses: dtolnay/rust-toolchain@1.95.0 + with: + components: rustfmt, clippy + - name: Linux desktop dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev libdbus-1-dev + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + - run: npm ci + - name: Verify release versions + run: node scripts/check-desktop-version.cjs + - run: npm run check + - run: npm run format:check + - run: npm test + - run: cargo fmt --manifest-path src-tauri/Cargo.toml --check + - run: cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings + - run: cargo test --manifest-path src-tauri/Cargo.toml --locked + - name: Build desktop + if: "!startsWith(github.ref, 'refs/tags/v')" + run: npm run tauri -- build --no-bundle + - name: Build release installers + if: startsWith(github.ref, 'refs/tags/v') + run: npm run dist + - uses: actions/upload-artifact@v4 with: - node-version: 22.15.0 - cache: "npm" - - # The tag is what users see; package.json is what electron-builder writes - # into the artifacts and the update manifests. If they disagree, the - # updater serves a version nobody can find, so stop before publishing. - - name: Verify the tag matches package.json - shell: bash - run: | - set -euo pipefail - tag="${GITHUB_REF_NAME#v}" - pkg="$(node -p "require('./package.json').version")" - if [ "$tag" != "$pkg" ]; then - echo "::error::Tag $GITHUB_REF_NAME does not match package.json version $pkg." - exit 1 - fi - echo "Releasing $pkg from tag $GITHUB_REF_NAME." - - # Only `-beta.N` prereleases are recognised: electron-builder names the - # update manifest after the prerelease identifier, and `beta` is the one - # the in-app channel selector reads (see updaterHandlers.js). - - name: Determine release channel - id: channel - shell: bash - run: | - set -euo pipefail - version="${GITHUB_REF_NAME#v}" - if [[ "$version" == *-* ]]; then - if [[ ! "$version" =~ -beta\.[0-9]+$ ]]; then - echo "::error::Prerelease versions must end in -beta.N (got $version)." - exit 1 - fi - echo "prerelease=true" >> "$GITHUB_OUTPUT" - else - echo "prerelease=false" >> "$GITHUB_OUTPUT" - fi - - - name: Install dependencies - run: npm ci - - # macOS artifacts are deliberately unsigned and un-notarized: that needs a - # paid Apple Developer account, and we would rather ship a build testers - # can run today. Gatekeeper blocks the first launch, so users open the app - # once with right-click > Open, or run `xattr -cr /Applications/CtraceGUI.app`. - # To sign later, restore an Apple certificate here and set `identity`, - # `hardenedRuntime` and `notarize` in the `mac` block of package.json. - - name: Publish - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_IDENTITY_AUTO_DISCOVERY: false - EP_PRE_RELEASE: ${{ steps.channel.outputs.prerelease }} - run: npm run dist:publish + name: coretrace-desktop-${{ runner.os }}-${{ runner.arch }} + path: | + src-tauri/target/release/coretrace-desktop.exe + src-tauri/target/release/coretrace-desktop + src-tauri/target/release/bundle/** + if-no-files-found: error diff --git a/README.md b/README.md index 7857a33..b198788 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,93 @@ -# CTrace GUI - -A modern Electron-based GUI application for running CTrace analysis on C/C++ code. - -## Overview - -CTrace GUI provides an intuitive interface for analyzing C/C++ source code using the CTrace static analysis tool. The application features a VS Code-like interface with file management, syntax highlighting, and integrated analysis results. - -## Features - -- **File Management**: Open individual files or entire workspaces -- **File Tree Explorer**: Navigate project structure with refresh and auto-watch capabilities -- **Code Editor**: Syntax-highlighted editor with line numbers and search functionality -- **Tab Management**: Multi-file editing with tab interface -- **CTrace Integration**: Run static analysis directly from the GUI -- **Search**: Global search across workspace files -- **AI Assistant**: Chat with local or cloud LLM models about your code -- **Notifications**: User-friendly notification system -- **Work Loss Prevention**: Automatic session saving and restoration - -## Architecture - -The application follows a modular architecture with separate managers for different concerns: - -- **UIController**: Main coordinator for all UI components -- **FileOperationsManager**: Handles file I/O operations via IPC -- **TabManager**: Manages editor tabs and file switching -- **EditorManager**: Controls the Monaco code editor -- **SearchManager**: Handles search operations (widget and sidebar) -- **NotificationManager**: Manages user notifications -- **StateManager**: Handles session persistence (work loss prevention) -- **DiagnosticsManager**: Manages CTrace analysis results and visualization - -The renderer process communicates with the main process exclusively through a typed IPC bridge defined in `src/preload.js`. All IPC channels are whitelisted — the renderer cannot call anything not on the list. - ---- - -## Getting Started - -### Prerequisites - -| Requirement | Version | Notes | -|---|---|---| -| Node.js | v18 or later | Required for the build toolchain | -| npm | bundled with Node.js | Used to install dependencies | -| CTrace binary | any | Must be placed at `bin/ctrace` (see below) | - -> **Windows users:** CTrace is a Linux binary. It runs inside WSL (Windows Subsystem for Linux). The application will detect WSL on startup and guide you through installation if it is missing. - -### Installation - -```bash -npm install -``` - -This installs all Node.js dependencies including Electron and Monaco Editor. - -### CTrace Binary Setup - -The CTrace binary must be present before you can run analyses. Place it at: - -``` -bin/ctrace # Linux -bin/ctrace # Windows (the binary itself runs inside WSL) -bin/ctrace-darwin-arm64 # macOS, Apple Silicon -bin/ctrace-darwin-x64 # macOS, Intel -``` - -The `bin/` directory is at the root of the repository (same level as `package.json`). Create it if it doesn't exist. - -If the binary is missing, the application will still launch, but the **Run Analysis** button will return an error. - -> **macOS users:** the `ctrace` shipped in the release artifacts is a Linux ELF and cannot run on macOS. The app detects this and tells you so instead of failing with an opaque spawn error. Supply a native Mach-O build either by naming it `ctrace-darwin-` next to the bundled one, or by selecting it from **File → Backend Settings**. Everything else — editor, explorer, search, terminal, assistant — works without it. - -### Running in Development - -```bash -npm start -``` - -This automatically rebuilds the renderer bundle (`src/renderer/bundle.js`) before launching Electron. You must rerun `npm start` (or `npm run build:renderer`) any time you edit files under `src/renderer/`. - -### Security notes - -- The renderer runs sandboxed (`sandbox: true`, `contextIsolation: true`) behind a strict Content-Security-Policy: no inline script, no remote scripts, and no navigation away from `index.html`. New UI code must attach listeners with `addEventListener` (or the `data-action` attributes in `index.html`) and pass every external string (file names, tool output, error messages) through `escapeHtml()` before inserting it as HTML. -- DevTools shortcuts (F12, Ctrl/Cmd+Shift+I) are only active in development. Set `CTRACE_DEVTOOLS=1` to enable them in a packaged build. -- File-system IPC only accepts paths that came from a native dialog, the restored session, or the currently opened workspace. -- The assistant API key is encrypted with the OS credential store and never leaves the main process; keyed requests only go to `https:` endpoints (or `http://localhost`). -- App updates download automatically but are only installed after the user chooses "restart to apply". Builds are not code-signed yet, so keep it that way until signing is in place. - -### Building for Distribution - -| Platform | Command | Output | -|---|---|---| -| Current platform | `npm run dist` | `dist/` | -| Linux (AppImage) | `npm run dist:linux` | `dist/*.AppImage` | -| Windows (NSIS installer) | `npm run dist:win` | `dist/*.exe` | -| macOS (DMG + ZIP) | `npm run dist:mac` | `dist/*.dmg`, `dist/*.zip` | - -macOS builds are **unsigned and un-notarized** (signing requires a paid Apple Developer account). Gatekeeper blocks the first launch, so open the app once with right-click → **Open**, or clear the quarantine attribute: - -```bash -xattr -cr /Applications/CtraceGUI.app -``` - -CI builds macOS twice — `macos-latest` for arm64 and `macos-13` for x64 — because `node-pty` and `node-llama-cpp` are native modules and each architecture is compiled on a runner of that architecture. To sign later, restore an Apple certificate step in `.github/workflows/release.yml` and set `identity`, `hardenedRuntime` and `notarize` in the `mac` block of `package.json` (`build/entitlements.mac.plist` is kept for that purpose). - -### Releases and versioning - -Versions are semver, and the git tag is always `v`: - -| Channel | Version shape | Update manifest | Example | -|---|---|---|---| -| stable (`main` in the app) | `X.Y.Z` | `latest*.yml` | `5.1.0` | -| beta | `X.Y.Z-beta.N` | `beta*.yml` | `5.2.0-beta.1` | - -`-beta.N` is the **only** accepted prerelease form. electron-builder names the update manifest after the prerelease identifier, so a version like `5.0.1-a` produces an `a.yml` that neither channel reads — the release becomes invisible to the updater. `scripts/release.sh` and the release workflow both reject it, and `tests/version.test.js` pins the rule. - -Cut a release from `master` with a clean tree: - -```bash -./scripts/release.sh minor # 5.1.0 -> 5.2.0 -./scripts/release.sh 5.2.0-beta.1 # beta channel -./scripts/release.sh prerelease # 5.2.0-beta.1 -> 5.2.0-beta.2 -``` - -The script bumps `package.json`, tags, and pushes. **Only the tag triggers a build**: pushes to `master` and pull requests run the test suite and nothing else, so merging a PR never republishes a release. CI verifies the tag matches `package.json` before publishing, and marks the GitHub release as a prerelease when the version carries `-beta.N`. - -### Running Tests - -```bash -npm test -``` - -Uses Node.js's built-in test runner (`node --test`). No extra test framework is required. - -### Generating Documentation - -```bash -npm run docs -``` - -Generates JSDoc API docs. Hosted version: https://coretrace.github.io/coretrace-gui/ - ---- - -## Documentation - -Complete API documentation is available [here](https://coretrace.github.io/coretrace-gui/) - -IPC channel reference (all renderer ↔ main channels): [docs/ipc-channels.md](docs/ipc-channels.md) - - ---- - -## License - -Licensed under the Apache License, Version 2.0. - -You may obtain a copy of the License in this repository at [LICENSE](LICENSE) or at: - -http://www.apache.org/licenses/LICENSE-2.0 +# CoreTrace Desktop + +CoreTrace 6 uses **Tauri 2 / Rust**, **React / TypeScript** and an embedded Monaco editor. +A desktop sidebar connects the personal home, organisation dashboard, repositories, +analyses and code workspace. + +## Start + +Install Node.js 22.19+, Rust 1.95+ and the platform's +[Tauri prerequisites](https://v2.tauri.app/start/prerequisites/). +Windows requires Microsoft C++ Build Tools and WebView2 Runtime. + +```sh +npm ci +npm start +``` + +`npm start` launches the native application. `npm run dev` is a browser preview; +filesystem, Git and account actions deliberately require Tauri. No demo data or +production credentials are embedded in the preview. + +## Use + +- **Local code:** Open a folder. Browse the tree, edit files, search with Ctrl/Cmd+F, + save with Ctrl/Cmd+S. Tabs retain drafts while navigating. Closing dirty tabs, + switching folders and closing the desktop asks before discarding. Save conflicts + preserve external edits and your in-memory draft; copy the draft before reopening. +- **GitHub:** Enter `owner/repository` or its HTTPS GitHub URL and choose a parent + folder. Git clones into a new child directory; existing destinations are never + overwritten. Private repositories use Git Credential Manager or `gh auth setup-git`. + CoreTrace does not launch builds or install dependencies while opening or cloning; + Git uses your existing local configuration. +- **Account:** Approve the device code in your browser. Rust keeps access tokens in + memory and refresh tokens in the OS credential manager, separated by API URL. + Credentials never enter React, localStorage or config files. Select the organisation + in the top bar. Dashboards show its quotas, reported period spend, plan and jobs. + Missing figures remain unknown. Account settings list members subject to permissions. +- **Cloud analysis:** Select an enabled connected GitHub repository and branch, tag or + commit, then confirm CTU spending. The repository configuration determines the tools. + Follow progress, cancel, inspect verified reports and open findings in the matching + workspace. Retrying an uncertain submission keeps its idempotency key until inputs + change or submission succeeds. Local edits are not uploaded by this workflow. +- **Local analysis:** Select your installed native `ctrace` executable in Settings, + with its analysis tools installed. Open a source file, save it and run static analysis. + SARIF findings, stdout/stderr and cancellation are available. The existing Linux + `bin/ctrace` cannot run directly on Windows. + +The API defaults to `https://coretrace.fr/v1`. Set `CORETRACE_BASE_URL` before launch +for another deployment; `/v1` is added if absent. HTTPS is required except for loopback +API development addresses. Device-browser and signed report URLs require HTTPS. +Web account management links target coretrace.fr. + +## Build and verify + +```sh +npm run check +npm test +npm run format:check +cargo fmt --manifest-path src-tauri/Cargo.toml --check +cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings +npm run test:native +npm run tauri -- build --no-bundle +npm run dist +``` + +Windows executable: `src-tauri/target/release/coretrace-desktop.exe`. +`npm run dist` builds installers. Builds are unsigned; the CI validates Windows, +macOS and Linux and retains tag installers as artifacts. No automatic publication, +signing or replacement update service is configured by this migration. + +## Architecture and current boundaries + +- `desktop/`: React features, typed native bridge, platform projections and tests. +- `src-tauri/src/`: workspace, GitHub, cloud authentication/API and local process services. +- `desktop/platform-schema.d.ts`: generated contract snapshot from + Coretrace-Entreprise/coretrace-web, matching the control plane OpenAPI schema. +- Editing is confined to the chosen workspace, excludes Git metadata and path escapes, + and accepts UTF-8 text up to 4 MiB. Atomic saves check the on-disk revision. Directory + listing omits links and dependency/build folders, with a 5,000-entry limit per folder. +- Reports are capped at 10 MiB, output at 1 MiB per stream, local runs at 15 minutes, + clones at five minutes and cloud history at 200 jobs. Inspect incomplete clone folders + before retrying. Workspace tabs and the analyser choice last for the session. +- The IDE includes exploration, syntax colouring, tabs, search, editing and findings + navigation. Terminal, debugger, LSP, extensions, Git commit/push UI, WSL bridging and + local LLM integrations are not included. Local analysis targets the active source + file; cloud analysis targets connected GitHub commits. Uploading arbitrary local + folders to the cloud is not implemented. + +See [migration/rollback](docs/tauri-migration.md) and +[validation evidence](docs/desktop-validation.md). The old Electron implementation stays +in `src/` as reference, with its [README](docs/electron-readme.md), +[manifest](docs/electron-package.json) and `tests/`. It is not the new runtime; recover it +in a separate clean worktree at `807e2c9` when needed. Existing user changes are preserved. diff --git a/desktop/App.tsx b/desktop/App.tsx index e0f9842..f7e576f 100644 --- a/desktop/App.tsx +++ b/desktop/App.tsx @@ -25,6 +25,7 @@ import { Analyses } from "./features/Analyses"; import { Settings } from "./features/Settings"; import type { EditorHandle } from "./features/WorkspaceEditor"; import { useCloud } from "./useCloud"; +import { workspaceRelativePath } from "./model"; import type { Job, LocalResult, Page, Repository, Workspace } from "./types"; const WorkspaceEditor = lazy(() => import("./features/WorkspaceEditor").then((module) => ({ @@ -87,8 +88,12 @@ export default function App() { "Quitter", ) ) { - if (localRunning) await desktop.cancelLocal(); - await window.destroy(); + try { + if (localRunning) await desktop.cancelLocal(); + await window.destroy(); + } catch (e) { + setMessage(errorMessage(e)); + } } }); if (disposed) off(); @@ -183,9 +188,14 @@ export default function App() { return; } setPage("workspace"); - void editor.current - .open(path, line) - .catch((e) => setMessage(errorMessage(e))); + try { + const relative = workspaceRelativePath(path, workspace.path); + void editor.current + .open(relative, line) + .catch((e) => setMessage(errorMessage(e))); + } catch (e) { + setMessage(errorMessage(e)); + } }; const selectJob = (job: Job | null) => { if (orgRef.current !== cloud.org) return; @@ -323,7 +333,11 @@ export default function App() { - - - - - - `; - - // Helper to append messages with typing effect - const addMessage = (who, text, options = {}) => { - const container = document.getElementById('assistant-messages'); - if (!container) return; - const wrap = document.createElement('div'); - wrap.style.marginBottom = '12px'; - const bubble = document.createElement('div'); - bubble.style.padding = '10px 12px'; - bubble.style.borderRadius = '8px'; - bubble.style.maxWidth = '90%'; - bubble.style.lineHeight = '1.4'; - - if (who === 'user') { - bubble.style.background = '#0b5fff'; - bubble.style.color = '#fff'; - bubble.style.marginLeft = 'auto'; - bubble.style.whiteSpace = 'pre-wrap'; - bubble.textContent = text; - wrap.appendChild(bubble); - container.appendChild(wrap); - container.scrollTop = container.scrollHeight; - } else { - bubble.style.background = '#111319'; - bubble.style.color = '#e6edf3'; - bubble.style.marginRight = 'auto'; - bubble.style.animation = 'assistantFadeIn 0.2s ease-in'; - wrap.appendChild(bubble); - container.appendChild(wrap); - - // Return the bubble element for typing effects - if (options.typing) { - return bubble; - } - - // Render markdown for assistant messages and return bubble - bubble.innerHTML = renderMarkdown(text); - container.scrollTop = container.scrollHeight; - return bubble; - } - }; - - // Typing effect for assistant messages - const typeMessage = async (bubble, text, speed = 0.1) => { // Changed from 20 to 10ms (faster). Increase for slower, decrease for faster - const container = document.getElementById('assistant-messages'); - let currentText = ''; - - for (let i = 0; i < text.length; i++) { - currentText += text[i]; - bubble.innerHTML = renderMarkdown(currentText); - if (container) container.scrollTop = container.scrollHeight; - await new Promise(resolve => setTimeout(resolve, speed)); - } - }; - - // Animated thinking indicator - const addThinkingMessage = () => { - const bubble = addMessage('assistant', '', { typing: true }); - if (!bubble) return null; - - let dotCount = 0; - const thinkingInterval = setInterval(() => { - dotCount = (dotCount % 3) + 1; - bubble.textContent = 'Thinking' + '.'.repeat(dotCount); - const container = document.getElementById('assistant-messages'); - if (container) container.scrollTop = container.scrollHeight; - }, 400); - - return { bubble, interval: thinkingInterval }; - }; - - // Remove thinking message - const removeThinkingMessage = (thinkingData) => { - if (!thinkingData) return; - clearInterval(thinkingData.interval); - const container = document.getElementById('assistant-messages'); - if (container && thinkingData.bubble && thinkingData.bubble.parentElement) { - container.removeChild(thinkingData.bubble.parentElement); - } - }; - - // Simple markdown renderer - const renderMarkdown = (text) => { - // Trim leading/trailing whitespace to avoid extra newlines - text = text.trim(); - - // Store original code blocks before any processing - const codeBlocks = []; - let codeIndex = 0; - - // Extract and store original code blocks with language info - const textWithPlaceholders = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { - const trimmedCode = code.trim(); - codeBlocks.push({ code: trimmedCode, lang: lang || '' }); - return `__CODE_BLOCK_${codeIndex++}__`; - }); - - // Escape HTML - let html = textWithPlaceholders.replace(/&/g, '&') - .replace(//g, '>'); - - // Replace code block placeholders with rendered HTML - codeIndex = 0; - html = html.replace(/__CODE_BLOCK_(\d+)__/g, (match, index) => { - const blockData = codeBlocks[parseInt(index)]; - const originalCode = blockData.code; - const lang = blockData.lang.toLowerCase(); - - // Store original code in base64 to avoid any escaping issues - const base64Code = btoa(unescape(encodeURIComponent(originalCode))); - - // Apply syntax highlighting for C/C++ code - let displayCode; - if (lang === 'c' || lang === 'cpp' || lang === 'c++') { - // Use syntax highlighter - const { applySyntaxHighlight } = window.syntaxHighlighter; - const fileType = lang === 'c' ? 'C' : 'C++'; - displayCode = applySyntaxHighlight(originalCode, fileType); - } else { - // No highlighting - just escape HTML - displayCode = originalCode.replace(/&/g, '&') - .replace(//g, '>'); - } - - const langLabel = lang ? `${lang}` : ''; - - return `
- ${langLabel} -
- - -
-
${displayCode}
-
`; - }); - - // Inline code (`code`) - html = html.replace(/`([^`]+)`/g, '$1'); - - // Bold (**text** or __text__) - html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); - html = html.replace(/__([^_]+)__/g, '$1'); - - // Italic (*text* or _text_) - html = html.replace(/\*([^*]+)\*/g, '$1'); - html = html.replace(/_([^_]+)_/g, '$1'); - - // Headers (### text) - html = html.replace(/^### (.+)$/gm, '

$1

'); - html = html.replace(/^## (.+)$/gm, '

$1

'); - html = html.replace(/^# (.+)$/gm, '

$1

'); - - // Lists (- item or * item or 1. item) - html = html.replace(/^- (.+)$/gm, '
  • $1
  • '); - html = html.replace(/^\* (.+)$/gm, '
  • $1
  • '); - html = html.replace(/^\d+\. (.+)$/gm, '
  • $1
  • '); - - // Wrap consecutive
  • in
      - html = html.replace(/(]*>.*<\/li>\n?)+/g, (match) => { - return `
        ${match}
      `; - }); - - // Line breaks (preserve double newlines as paragraphs) - html = html.replace(/\n\n/g, '

      '); - - return html; - }; - - // Prefill a small welcome message with typing effect (only first time) - const providerLabel = cfg.provider === 'external' - ? (() => { - const base = cfg.externalProvider === 'ChatGPT5' ? 'OpenAI' : (cfg.externalProvider || 'External'); - return cfg.model ? `${base} — ${cfg.model}` : base; - })() - : cfg.provider; - const welcomeText = `Hi — I'm your assistant. Using: ${providerLabel}. Ask me something or open Settings to change providers.`; - - // Check if welcome message has been shown before - const hasShownWelcome = sessionStorage.getItem('assistantWelcomeShown'); - - if (!hasShownWelcome) { - // First time - show typing effect (faster speed: 8ms per character) - const welcomeBubble = addMessage('assistant', '', { typing: true }); - if (welcomeBubble) { - typeMessage(welcomeBubble, welcomeText, 8); - } - sessionStorage.setItem('assistantWelcomeShown', 'true'); - } else { - // Already shown - display instantly - addMessage('assistant', welcomeText); - } - - // Wire up send button - const sendBtn = document.getElementById('assistant-send'); - const inputEl = document.getElementById('assistant-input'); - const settingsBtn = document.getElementById('assistant-settings'); - const newChatBtn = document.getElementById('assistant-new-chat'); - const contextIndicator = document.getElementById('context-indicator'); - const contextText = document.getElementById('context-text'); - const contextClearBtn = document.getElementById('context-clear'); - - const historyBtn = document.getElementById('assistant-history'); - - // New conversation button — auto-saves current chat then resets - newChatBtn.onclick = async () => { - await this._saveCurrentConversation(); - const messagesEl = document.getElementById('assistant-messages'); - if (messagesEl) messagesEl.innerHTML = ''; - this._conversationHistory = []; - this._currentConvId = null; - this._inputHistory = []; - this._inputHistoryCursor = -1; - this._inputDraft = ''; - }; - - // History button — show past conversations panel - historyBtn.onclick = () => this._showHistoryPanel(); - - // Capture selection before it's lost when user clicks on input - let capturedSelection = ''; - let capturedLineInfo = ''; - let capturedContextType = 'selection'; // 'selection' | 'file' | 'analysis' - - inputEl.addEventListener('focus', () => { - try { - const monacoEditor = this.ui.editorManager.getMonacoInstance ? this.ui.editorManager.getMonacoInstance() : null; - if (monacoEditor) { - const selection = monacoEditor.getSelection(); - const model = monacoEditor.getModel(); - if (selection && model) { - const selectedText = model.getValueInRange(selection); - if (selectedText) { - capturedSelection = selectedText; - const startLine = selection.startLineNumber; - const endLine = selection.endLineNumber; - const activeTab = this.ui.tabManager.getActiveTab(); - const fileName = activeTab && activeTab.fileName ? activeTab.fileName : 'Untitled'; - capturedLineInfo = startLine === endLine ? `${fileName}: ${startLine}` : `${fileName}: ${startLine}-${endLine}`; - contextText.textContent = capturedLineInfo; - contextIndicator.style.display = 'block'; - } - } - } else { - // Fallback for legacy textarea editor - const editor = this.ui.editorManager.editor; - const start = editor.selectionStart; - const end = editor.selectionEnd; - const selection = editor.value.substring(start, end); - if (selection) { - capturedSelection = selection; - const textBeforeStart = editor.value.substring(0, start); - const textBeforeEnd = editor.value.substring(0, end); - const startLine = (textBeforeStart.match(/\n/g) || []).length + 1; - const endLine = (textBeforeEnd.match(/\n/g) || []).length + 1; - const activeTab = this.ui.tabManager.getActiveTab(); - const fileName = activeTab && activeTab.fileName ? activeTab.fileName : 'Untitled'; - if (startLine === endLine) { - capturedLineInfo = `${fileName}: ${startLine}`; - } else { - capturedLineInfo = `${fileName}: ${startLine}-${endLine}`; - } - contextText.textContent = capturedLineInfo; - contextIndicator.style.display = 'block'; - } - } - } catch (err) { - console.warn('Error capturing selection for assistant context:', err); - } - }); - - // Handle Enter key to send message (Shift+Enter for new line) - // Arrow keys navigate input history like a terminal - inputEl.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendBtn.click(); - } else if (e.key === 'ArrowUp') { - if (!this._inputHistory || this._inputHistory.length === 0) return; - if (this._inputHistoryCursor === -1) { - this._inputDraft = inputEl.value; - this._inputHistoryCursor = this._inputHistory.length - 1; - } else if (this._inputHistoryCursor > 0) { - this._inputHistoryCursor--; - } - inputEl.value = this._inputHistory[this._inputHistoryCursor]; - e.preventDefault(); - } else if (e.key === 'ArrowDown') { - if (!this._inputHistory || this._inputHistoryCursor === -1) return; - if (this._inputHistoryCursor < this._inputHistory.length - 1) { - this._inputHistoryCursor++; - inputEl.value = this._inputHistory[this._inputHistoryCursor]; - } else { - this._inputHistoryCursor = -1; - inputEl.value = this._inputDraft || ''; - } - e.preventDefault(); - } - }); - - // Clear captured selection when input loses focus without sending - inputEl.addEventListener('blur', () => { - // Small delay to allow send button click to process - setTimeout(() => { - if (!inputEl.value.trim()) { - capturedSelection = ''; - capturedLineInfo = ''; - capturedContextType = 'selection'; - contextIndicator.style.display = 'none'; - document.getElementById('ctx-file').textContent = '📄 Current file'; - } - }, 200); - }); - - // Clear button handler - contextClearBtn.onclick = () => { - capturedSelection = ''; - capturedLineInfo = ''; - capturedContextType = 'selection'; - contextIndicator.style.display = 'none'; - document.getElementById('ctx-file').textContent = '📄 Current file'; - }; - - // Context shortcut — current file (or current selection if text is highlighted) - document.getElementById('ctx-file').onclick = () => { - try { - const monaco = this.ui.editorManager.getMonacoInstance ? this.ui.editorManager.getMonacoInstance() : null; - const activeTab = this.ui.tabManager.getActiveTab(); - const fileName = activeTab && activeTab.fileName ? activeTab.fileName : 'file'; - const ctxFileBtn = document.getElementById('ctx-file'); - let content = ''; - if (monaco) { - const model = monaco.getModel(); - const sel = monaco.getSelection(); - if (!model) return; - if (sel && !sel.isEmpty()) { - // User has highlighted a range — capture only that - content = model.getValueInRange(sel); - const s = sel.startLineNumber; - const e = sel.endLineNumber; - capturedContextType = 'selection'; - capturedLineInfo = s === e ? `${fileName}:${s}` : `${fileName}:${s}-${e}`; - } else { - // No selection — capture entire file - content = model.getValue(); - const totalLines = model.getLineCount(); - capturedContextType = 'file'; - capturedLineInfo = `${fileName}:1-${totalLines}`; - } - } else { - const editor = this.ui.editorManager.editor; - if (!editor) return; - const start = editor.selectionStart; - const end = editor.selectionEnd; - if (start !== end) { - content = editor.value.substring(start, end); - const before = editor.value.substring(0, start); - const beforeEnd = editor.value.substring(0, end); - const sLine = (before.match(/\n/g) || []).length + 1; - const eLine = (beforeEnd.match(/\n/g) || []).length + 1; - capturedContextType = 'selection'; - capturedLineInfo = sLine === eLine ? `${fileName}:${sLine}` : `${fileName}:${sLine}-${eLine}`; - } else { - content = editor.value; - const totalLines = (content.match(/\n/g) || []).length + 1; - capturedContextType = 'file'; - capturedLineInfo = `${fileName}:1-${totalLines}`; - } - } - if (!content.trim()) return; - capturedSelection = content; - ctxFileBtn.textContent = `📄 ${capturedLineInfo}`; - contextText.textContent = capturedLineInfo; - contextIndicator.style.display = 'block'; - inputEl.focus(); - } catch (e) { - console.warn('Error capturing file context:', e); - } - }; - - // Context shortcut — analysis results - document.getElementById('ctx-analysis').onclick = () => { - try { - const dm = this.ui.diagnosticsManager; - const diags = dm && dm.currentDiagnostics; - const ctxBtn = document.getElementById('ctx-analysis'); - if (!diags || diags.length === 0) { - const orig = ctxBtn.textContent; - ctxBtn.textContent = 'No results'; - setTimeout(() => { ctxBtn.textContent = orig; }, 1500); - return; - } - const lines = [`${diags.length} issue(s) found:`]; - for (const d of diags) { - const loc = d.location || {}; - const file = loc.file ? loc.file.split('/').pop().split('\\').pop() : '?'; - const msg = (d.details && d.details.message) ? d.details.message : ''; - lines.push(`[${d.severity}] ${file}:${loc.startLine} (${d.ruleId}) — ${msg}`); - } - capturedSelection = lines.join('\n'); - capturedContextType = 'analysis'; - capturedLineInfo = `Analysis: ${diags.length} issue(s)`; - contextText.textContent = capturedLineInfo; - contextIndicator.style.display = 'block'; - inputEl.focus(); - } catch (e) { - console.warn('Error capturing analysis context:', e); - } - }; - - const stopBtn = document.getElementById('assistant-stop'); - - const setRequestingState = (requesting) => { - if (requesting) { - sendBtn.style.display = 'none'; - stopBtn.style.display = 'flex'; - } else { - sendBtn.style.display = 'flex'; - stopBtn.style.display = 'none'; - } - }; - - stopBtn.onclick = async () => { - try { await window.api.invoke('assistant-abort'); } catch (_) {} - setRequestingState(false); - }; - - sendBtn.onclick = async () => { - if (sendBtn.style.display === 'none') return; // already requesting - const text = (inputEl.value || '').trim(); - if (!text) return; - - // Save to input history for ArrowUp navigation - this._inputHistory.push(text); - this._inputHistoryCursor = -1; - this._inputDraft = ''; - - // Use captured selection as context - let context = ''; - if (capturedSelection) { - if (capturedContextType === 'analysis') { - context = `\n\n[Context - Analysis Results]:\n${capturedSelection}\n\n`; - } else { - // For both 'file' (full file) and 'selection' (highlighted range), use the line info label - const label = capturedLineInfo || (capturedContextType === 'file' ? 'File' : 'Selected Code'); - context = `\n\n[Context - ${label}]:\n\`\`\`\n${capturedSelection}\n\`\`\`\n\n`; - } - } - - // Clear captured context and hide indicator after using it - capturedSelection = ''; - capturedLineInfo = ''; - capturedContextType = 'selection'; - contextIndicator.style.display = 'none'; - document.getElementById('ctx-file').textContent = '📄 Current file'; - - // Combine user message with context (context goes to LLM, only text shown in UI) - const fullMessage = context ? context + text : text; - - // Display only user's text in UI - addMessage('user', text); - inputEl.value = ''; - - // Get current assistant config - const cfg = this.ui.getAssistantConfig(); - if (!cfg || cfg.provider === 'none' || cfg.skipped) { - addMessage('assistant', 'Assistant not configured. Please click Settings ⚙️ to set up your provider.'); - return; - } - - setRequestingState(true); - - // Show animated thinking indicator - const thinkingData = addThinkingMessage(); - - try { - // All providers go through IPC (main process); send conversation history - const result = await window.api.invoke('assistant-chat', { - provider: cfg.provider, - message: fullMessage, - history: this._conversationHistory, - config: cfg - }); - - // Remove the thinking message - removeThinkingMessage(thinkingData); - - if (result && result.aborted) { - // User cancelled — no message, no history update - return; - } - - if (result && result.success) { - // Update in-memory conversation history for next request - this._conversationHistory.push({ role: 'user', content: fullMessage }); - this._conversationHistory.push({ role: 'assistant', content: result.reply }); - - // Render the full response immediately so code-block buttons are interactive at once - const bubble = addMessage('assistant', result.reply); - if (bubble) { - attachCodeActionListeners(); - } - - // Auto-save conversation after each exchange - this._saveCurrentConversation(); - } else { - const errorMsg = result && result.error ? result.error : 'Unknown error occurred'; - addMessage('assistant', `❌ Error: ${errorMsg}`); - } - } catch (err) { - removeThinkingMessage(thinkingData); - console.error('Assistant chat error:', err); - addMessage('assistant', `❌ Error: ${err.message || 'Failed to communicate with assistant'}`); - } finally { - setRequestingState(false); - } - }; - - // Helper to attach event listeners to code action buttons - const attachCodeActionListeners = () => { - const container = document.getElementById('assistant-messages'); - if (!container) return; - - // Copy button handlers - container.querySelectorAll('.code-copy-btn').forEach(btn => { - btn.onclick = () => { - const base64Code = btn.getAttribute('data-code-b64'); - const code = decodeURIComponent(escape(atob(base64Code))); - - try { - window.api.clipboard.writeText(code); - btn.textContent = 'Copied!'; - setTimeout(() => btn.textContent = 'Copy', 2000); - } catch (_) { - if (navigator && navigator.clipboard && navigator.clipboard.writeText) { - navigator.clipboard.writeText(code) - .then(() => { - btn.textContent = 'Copied!'; - setTimeout(() => btn.textContent = 'Copy', 2000); - }) - .catch(() => alert('Failed to copy code')); - } - } - }; - }); - - // Replace button handlers - container.querySelectorAll('.code-replace-btn').forEach(btn => { - btn.onclick = () => { - const base64Code = btn.getAttribute('data-code-b64'); - const code = decodeURIComponent(escape(atob(base64Code))); - try { - const monacoEditor = this.ui.editorManager.getMonacoInstance ? this.ui.editorManager.getMonacoInstance() : null; - if (monacoEditor) { - const model = monacoEditor.getModel(); - const selection = monacoEditor.getSelection(); - let rangeObj = null; - let actionLabel = 'Inserted!'; - - if (selection && typeof selection.isEmpty === 'function' ? !selection.isEmpty() : (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn)) { - rangeObj = { - startLineNumber: selection.startLineNumber, - startColumn: selection.startColumn, - endLineNumber: selection.endLineNumber, - endColumn: selection.endColumn - }; - actionLabel = 'Replaced!'; - } else { - const pos = monacoEditor.getPosition(); - rangeObj = { startLineNumber: pos.lineNumber, startColumn: pos.column, endLineNumber: pos.lineNumber, endColumn: pos.column }; - actionLabel = 'Inserted!'; - } - - monacoEditor.executeEdits('assistant', [{ range: rangeObj, text: code, forceMoveMarkers: true }]); - // Update tab state - if (this.ui.tabManager.activeTabId && model) { - this.ui.tabManager.handleContentChange(this.ui.tabManager.activeTabId, model.getValue()); - } - btn.textContent = actionLabel; - setTimeout(() => btn.textContent = 'Replace', 2000); - monacoEditor.focus(); - } else { - // Fallback to legacy textarea editor - const editor = this.ui.editorManager.editor; - const start = editor.selectionStart; - const end = editor.selectionEnd; - if (start !== end) { - // Replace selected text - const before = editor.value.substring(0, start); - const after = editor.value.substring(end); - editor.value = before + code + after; - if (this.ui.tabManager.activeTabId) { - this.ui.tabManager.handleContentChange(this.ui.tabManager.activeTabId, editor.value); - } - btn.textContent = 'Replaced!'; - setTimeout(() => btn.textContent = 'Replace', 2000); - } else { - const before = editor.value.substring(0, start); - const after = editor.value.substring(start); - editor.value = before + code + after; - if (this.ui.tabManager.activeTabId) { - this.ui.tabManager.handleContentChange(this.ui.tabManager.activeTabId, editor.value); - } - btn.textContent = 'Inserted!'; - setTimeout(() => btn.textContent = 'Replace', 2000); - } - editor.focus(); - } - } catch (err) { - console.error('Error replacing/inserting code from assistant:', err); - btn.textContent = 'Error'; - setTimeout(() => btn.textContent = 'Replace', 2000); - } - }; - }); - }; - - settingsBtn.onclick = () => { - // Open the assistant setup modal for reconfiguration - this.ui.showAssistantSetupGuide((cfg) => { - // Re-render assistant UI to reflect changes - this.ui.renderAssistantUI(); - }); - }; - } - -/** - * Return the in-memory cached assistant configuration (synchronous). - * Call loadAssistantConfig() first to ensure the cache is populated. - * @returns {Object|null} - */ - getAssistantConfig() { - return this._cachedConfig || null; - } - -/** - * Load assistant configuration from secure storage via IPC. - * Migrates any existing plaintext config from localStorage on first run, - * then removes it. Updates the in-memory cache. - * @returns {Promise} - */ - async loadAssistantConfig() { - try { - const result = await window.api.invoke('assistant-config-load'); - if (result.success) { - this._cachedConfig = result.config; - return this._cachedConfig; - } - - // Migration path: lift any legacy plaintext config from localStorage - const raw = localStorage.getItem('assistantConfig'); - if (raw) { - try { - const legacy = JSON.parse(raw); - const saved = await window.api.invoke('assistant-config-save', legacy); - if (!saved.success) throw new Error(saved.error || 'save failed'); - localStorage.removeItem('assistantConfig'); - console.log('[AssistantPanel] Migrated assistant config from localStorage to secure storage'); - return this.loadAssistantConfig(); - } catch (migErr) { - console.warn('[AssistantPanel] Migration from localStorage failed:', migErr); - } - } - - this._cachedConfig = null; - return null; - } catch (err) { - console.error('[AssistantPanel] Failed to load assistant config:', err); - this._cachedConfig = null; - return null; - } - } - -/** - * Persist assistant configuration via secure IPC storage and update cache. - * @param {Object} cfg - * @returns {Promise} - */ - async saveAssistantConfig(cfg) { - try { - const result = await window.api.invoke('assistant-config-save', cfg); - if (!result.success) throw new Error(result.error || 'Unknown error'); - // Re-read from storage so the cache never holds the key, only hasApiKey. - await this.loadAssistantConfig(); - this.ui.notificationManager.showSuccess('Assistant settings saved'); - } catch (err) { - console.error('[AssistantPanel] Failed to save assistant config:', err); - this.ui.notificationManager.showError('Failed to save assistant settings'); - } - } - -/** - * Render a first-time setup modal for Assistant configuration. - * Calls the done callback with saved config or null if cancelled. - */ - showAssistantSetupGuide(done) { - // Use the in-memory cache (should already be loaded by ensureAssistantConfigured) - const existingConfig = this.ui.getAssistantConfig() || {}; - - // Modal overlay - const modal = document.createElement('div'); - modal.style.cssText = ` - position: fixed; top:0; left:0; width:100%; height:100%; - background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index:10001; - `; - - const dialog = document.createElement('div'); - dialog.style.cssText = ` - width: 720px; max-width: 95%; background: #161b22; border-radius:8px; padding:20px; box-shadow:0 8px 30px rgba(0,0,0,0.6); border:1px solid #30363d; - font-family: sans-serif; color: #e6edf3; - `; - - dialog.innerHTML = ` -

      Assistant setup

      -

      Choose how you'd like to connect the Assistant. You can use Ollama, an external API (ChatGPT5, Deepseek, or other), or point to a local GGUF model on your machine.

      -
      - - - -
      - -
      - -
      - - - -
      - `; - - modal.appendChild(dialog); - document.body.appendChild(modal); - - const extra = dialog.querySelector('#assist-extra'); - - const clearExtra = () => { extra.innerHTML = ''; }; - - const makeInputRow = (labelText, inputId, placeholder = '') => { - const row = document.createElement('div'); - row.style.cssText = 'display:flex; flex-direction:column; gap:6px; margin-top:8px;'; - row.innerHTML = ` - - - `; - return row; - }; - - const providerRadios = dialog.querySelectorAll('input[name="assist-provider"]'); - const optOllama = dialog.querySelector('#assist-opt-ollama'); - const optExternal = dialog.querySelector('#assist-opt-external'); - const optLocal = dialog.querySelector('#assist-opt-local'); - - const selectProvider = (value) => { - providerRadios.forEach(r => r.checked = (r.value === value)); - optOllama.style.borderColor = value === 'ollama' ? '#388bfd' : '#30363d'; - optExternal.style.borderColor = value === 'external' ? '#388bfd' : '#30363d'; - optLocal.style.borderColor = value === 'local' ? '#388bfd' : '#30363d'; - - clearExtra(); - if (value === 'ollama') { - extra.appendChild(makeInputRow('Ollama host (include protocol)', 'ollama-host', 'http://localhost:11434')); - extra.appendChild(makeInputRow('System prompt (optional)', 'system-prompt', 'You are a helpful assistant...')); - - // Pre-fill with saved values - setTimeout(() => { - const hostInput = document.getElementById('ollama-host'); - const systemInput = document.getElementById('system-prompt'); - if (hostInput && existingConfig.ollamaHost) { - hostInput.value = existingConfig.ollamaHost; - } - if (systemInput && existingConfig.systemPrompt) { - systemInput.value = existingConfig.systemPrompt; - } - }, 0); - } else if (value === 'external') { - const selRow = document.createElement('div'); - selRow.style.cssText = 'display:flex; gap:8px; align-items:center; margin-top:8px;'; - selRow.innerHTML = ` - - - `; - extra.appendChild(selRow); - extra.appendChild(makeInputRow('API Key', 'external-api-key', 'sk-...')); - - // Model list row — starts as a text input, upgraded to select after fetch - const modelRow = document.createElement('div'); - modelRow.style.cssText = 'display:flex; flex-direction:column; gap:6px; margin-top:8px;'; - modelRow.innerHTML = ` -
      - - -
      - - - `; - extra.appendChild(modelRow); - extra.appendChild(makeInputRow('System prompt (optional)', 'system-prompt', 'You are a helpful assistant...')); - - // Helper: map UI provider name → IPC providerId - const toProviderId = (name) => name === 'Deepseek' ? 'deepseek' : 'openai'; - - // Helper: populate model dropdown from fetched list - const populateModelSelect = (models, savedModel) => { - const sel = document.getElementById('external-model'); - const inp = document.getElementById('external-model-input'); - if (!sel || !inp) return; - sel.innerHTML = models.map(m => - `` - ).join(''); - sel.style.display = 'block'; - inp.style.display = 'none'; - }; - - // Fetch models when API key field loses focus - const attachFetchListener = () => { - const apiKeyInput = document.getElementById('external-api-key'); - const providerSelect = document.getElementById('external-provider'); - const statusEl = document.getElementById('model-fetch-status'); - if (!apiKeyInput) return; - - apiKeyInput.addEventListener('blur', async () => { - const key = apiKeyInput.value.trim(); - if (!key) return; - const providerId = toProviderId(providerSelect ? providerSelect.value : 'ChatGPT5'); - if (statusEl) statusEl.textContent = 'Fetching models…'; - try { - const result = await window.api.invoke('assistant-list-models', { providerId, apiKey: key }); - if (result.success && result.models.length > 0) { - const currentModel = document.getElementById('external-model-input').value.trim(); - populateModelSelect(result.models, currentModel); - if (statusEl) statusEl.textContent = `${result.models.length} models loaded`; - } else { - if (statusEl) statusEl.textContent = result.error ? `Error: ${result.error}` : 'No models returned'; - } - } catch (err) { - if (statusEl) statusEl.textContent = 'Failed to fetch models'; - } - }); - - // Re-fetch when provider changes (if key already present) - if (providerSelect) { - providerSelect.addEventListener('change', async () => { - const key = apiKeyInput.value.trim(); - if (!key) return; - const providerId = toProviderId(providerSelect.value); - if (statusEl) statusEl.textContent = 'Fetching models…'; - try { - const result = await window.api.invoke('assistant-list-models', { providerId, apiKey: key }); - if (result.success && result.models.length > 0) { - const currentModel = document.getElementById('external-model-input')?.value.trim() || ''; - populateModelSelect(result.models, currentModel); - if (statusEl) statusEl.textContent = `${result.models.length} models loaded`; - } else { - if (statusEl) statusEl.textContent = result.error ? `Error: ${result.error}` : 'No models returned'; - } - } catch (err) { - if (statusEl) statusEl.textContent = 'Failed to fetch models'; - } - }); - } - }; - - // Pre-fill with saved values and auto-fetch if key already exists - setTimeout(async () => { - const providerSelect = document.getElementById('external-provider'); - const apiKeyInput = document.getElementById('external-api-key'); - const modelInput = document.getElementById('external-model-input'); - const systemInput = document.getElementById('system-prompt'); - if (providerSelect && existingConfig.externalProvider) { - providerSelect.value = existingConfig.externalProvider; - } - if (apiKeyInput && existingConfig.hasApiKey) { - apiKeyInput.placeholder = 'Saved key on file — leave blank to keep it'; - } - if (modelInput && existingConfig.model) { - modelInput.value = existingConfig.model; - } - if (systemInput && existingConfig.systemPrompt) { - systemInput.value = existingConfig.systemPrompt; - } - - // If a saved API key exists, fetch models immediately (the key itself stays in the main process) - if (existingConfig.hasApiKey) { - const providerId = toProviderId(existingConfig.externalProvider || 'ChatGPT5'); - const statusEl = document.getElementById('model-fetch-status'); - if (statusEl) statusEl.textContent = 'Fetching models…'; - try { - const result = await window.api.invoke('assistant-list-models', { providerId, useStoredKey: true }); - if (result.success && result.models.length > 0) { - populateModelSelect(result.models, existingConfig.model); - if (statusEl) statusEl.textContent = `${result.models.length} models loaded`; - } else { - if (statusEl) statusEl.textContent = result.error ? `Error: ${result.error}` : ''; - } - } catch (_) { /* non-fatal */ } - } - - attachFetchListener(); - }, 0); - } else if (value === 'local') { - const row = document.createElement('div'); - row.style.cssText = 'display:flex; gap:8px; align-items:center; margin-top:8px;'; - row.innerHTML = ` - - - `; - extra.appendChild(row); - - // Browse handler using IPC - setTimeout(() => { - const browseBtn = document.getElementById('local-browse'); - const pathInput = document.getElementById('local-model-path'); - - // Pre-fill with saved value - if (pathInput && existingConfig.localModelPath) { - pathInput.value = existingConfig.localModelPath; - } - - if (browseBtn) { - browseBtn.onclick = async () => { - try { - const result = await window.api.invoke('select-llm-file'); - if (result && result.filePath) { - pathInput.value = result.filePath; - } - } catch (err) { - console.error('Error selecting model file', err); - this.ui.notificationManager.showError('Unable to open file selector'); - } - }; - } - }, 0); - - // Add context size configuration - const contextRow = document.createElement('div'); - contextRow.style.cssText = 'margin-top:12px;'; - contextRow.innerHTML = ` - - -
      Lower values use less VRAM. Recommended: 2048-8192. Default model max: 40960
      - `; - extra.appendChild(contextRow); - - // Add GPU layers configuration - const gpuRow = document.createElement('div'); - gpuRow.style.cssText = 'margin-top:12px;'; - gpuRow.innerHTML = ` - - -
      Higher values offload more layers to GPU for faster inference. Use -1 to offload all layers.
      - `; - extra.appendChild(gpuRow); - - // Add system prompt field for local models too - extra.appendChild(makeInputRow('System prompt (optional)', 'system-prompt', 'You are a helpful assistant...')); - - // Pre-fill all settings - setTimeout(() => { - const systemInput = document.getElementById('system-prompt'); - const gpuLayersInput = document.getElementById('gpu-layers'); - const contextSizeInput = document.getElementById('context-size'); - - if (systemInput && existingConfig.systemPrompt) { - systemInput.value = existingConfig.systemPrompt; - } - if (gpuLayersInput && existingConfig.gpuLayers !== undefined) { - gpuLayersInput.value = existingConfig.gpuLayers; - } - if (contextSizeInput && existingConfig.contextSize !== undefined) { - contextSizeInput.value = existingConfig.contextSize; - } - }, 0); - } - }; - - // Click handlers for the option cards as well - optOllama.onclick = () => selectProvider('ollama'); - optExternal.onclick = () => selectProvider('external'); - optLocal.onclick = () => selectProvider('local'); - - // Pre-select provider based on saved config, or default to external - const savedProvider = existingConfig.provider && existingConfig.provider !== 'none' - ? existingConfig.provider - : 'external'; - selectProvider(savedProvider); - - // Buttons - const btnSave = dialog.querySelector('#assist-save'); - const btnCancel = dialog.querySelector('#assist-cancel'); - const btnSkip = dialog.querySelector('#assist-skip'); - - let escHandlerRef = null; - const closeModal = (result) => { - try { document.body.removeChild(modal); } catch (_) {} - if (escHandlerRef) { document.removeEventListener('keydown', escHandlerRef); escHandlerRef = null; } - if (done) done(result); - }; - - btnCancel.onclick = () => closeModal(null); - btnSkip.onclick = async () => { - // Save a lightweight config indicating user skipped - const cfg = { provider: 'none', skipped: true }; - await this.ui.saveAssistantConfig(cfg); - closeModal(cfg); - }; - - btnSave.onclick = async () => { - const selected = Array.from(providerRadios).find(r => r.checked); - if (!selected) { - this.ui.notificationManager.showError('Please select a provider'); - return; - } - const provider = selected.value; - const cfg = { provider }; - - // Get system prompt if it exists - const systemPromptEl = document.getElementById('system-prompt'); - if (systemPromptEl && systemPromptEl.value.trim()) { - cfg.systemPrompt = systemPromptEl.value.trim(); - } - - if (provider === 'ollama') { - const hostEl = document.getElementById('ollama-host'); - cfg.ollamaHost = hostEl && hostEl.value ? hostEl.value.trim() : 'http://localhost:11434'; - } else if (provider === 'external') { - const prov = document.getElementById('external-provider'); - const key = document.getElementById('external-api-key'); - // Model is either the populated select or the fallback text input - const modelSel = document.getElementById('external-model'); - const modelInp = document.getElementById('external-model-input'); - const modelValue = (modelSel && modelSel.style.display !== 'none') - ? modelSel.value.trim() - : (modelInp ? modelInp.value.trim() : ''); - - cfg.externalProvider = prov ? prov.value : 'ChatGPT5'; - cfg.apiKey = key ? key.value.trim() : ''; - cfg.model = modelValue; - - // Map UI selection to backend provider ID - if (cfg.externalProvider === 'Deepseek') { - cfg.providerId = 'deepseek'; - if (!cfg.model) cfg.model = 'deepseek-chat'; - } else if (cfg.externalProvider === 'ChatGPT5') { - cfg.providerId = 'openai'; - if (!cfg.model) cfg.model = 'gpt-4'; - } else { - // Default to openai for compatibility if not specified - cfg.providerId = 'openai'; - if (!cfg.model) cfg.model = 'gpt-4'; - } - - const hasStoredKey = Boolean((this.ui.getAssistantConfig() || {}).hasApiKey); - if (!cfg.apiKey && !hasStoredKey) { - this.ui.notificationManager.showError('Please enter an API key for the external provider'); - return; - } - } else if (provider === 'local') { - const pathEl = document.getElementById('local-model-path'); - const gpuLayersEl = document.getElementById('gpu-layers'); - const contextSizeEl = document.getElementById('context-size'); - - cfg.localModelPath = pathEl ? pathEl.value : ''; - if (!cfg.localModelPath) { - this.ui.notificationManager.showError('Please choose a local GGUF model file'); - return; - } - - // Save GPU layers setting (default to 0 if not specified) - cfg.gpuLayers = gpuLayersEl && gpuLayersEl.value !== '' ? parseInt(gpuLayersEl.value, 10) : 0; - - // Save context size setting (default to 8192 if not specified) - cfg.contextSize = contextSizeEl && contextSizeEl.value !== '' ? parseInt(contextSizeEl.value, 10) : 8192; - } - - // Persist and close - // Detect system prompt change — clear conversation history so AI gets a fresh context - const prevCfg = this.ui.getAssistantConfig() || {}; - if ((prevCfg.systemPrompt || '') !== (cfg.systemPrompt || '')) { - this._conversationHistory = []; - } - - await this.ui.saveAssistantConfig(cfg); - // Notify main process in case it needs to warm things up - try { window.api.send('assistant-config-updated', cfg); } catch (_) {} - closeModal(cfg); - }; - - // Clicking outside the dialog triggers Save (same as clicking the Save button) - modal.onclick = (e) => { if (e.target === modal) btnSave.click(); }; - // Escape key closes without saving - const escHandler = (e) => { if (e.key === 'Escape') { closeModal(null); } }; - escHandlerRef = escHandler; - document.addEventListener('keydown', escHandler); - } - - // ── Conversation persistence ──────────────────────────────────────────────── - - /** - * Persist the current conversation to disk. No-op if the history is empty. - */ - async _saveCurrentConversation() { - if (this._conversationHistory.length === 0) return; - if (!this._currentConvId) { - this._currentConvId = String(Date.now()); - } - const firstUserMsg = this._conversationHistory.find(m => m.role === 'user'); - const title = firstUserMsg ? firstUserMsg.content.replace(/\n/g, ' ').slice(0, 72) : 'Conversation'; - try { - await window.api.invoke('assistant-conversations-save', { - id: this._currentConvId, - title, - history: this._conversationHistory, - createdAt: new Date().toISOString() - }); - } catch (_) { /* non-critical */ } - } - - /** - * Show the history panel inside the messages area. - * Replaces the messages area with a list of saved conversations. - */ - async _showHistoryPanel() { - const container = document.getElementById('assistant-messages'); - if (!container) return; - - // Save snapshot of current messages so we can restore them on "Back" - const prevHTML = container.innerHTML; - const prevHistory = this._conversationHistory.slice(); - const prevConvId = this._currentConvId; - - const render = async () => { - container.innerHTML = ''; - - // Header row - const header = document.createElement('div'); - header.style.cssText = 'display:flex; align-items:center; gap:8px; margin-bottom:12px;'; - - const backBtn = document.createElement('button'); - backBtn.title = 'Back to chat'; - backBtn.style.cssText = 'background:transparent; border:none; color:#8b949e; cursor:pointer; padding:4px 6px; border-radius:4px; display:flex; align-items:center;'; - backBtn.innerHTML = ''; - backBtn.onmouseover = () => { backBtn.style.color = '#c9d1d9'; }; - backBtn.onmouseout = () => { backBtn.style.color = '#8b949e'; }; - backBtn.onclick = () => { - container.innerHTML = prevHTML; - this._conversationHistory = prevHistory; - this._currentConvId = prevConvId; - }; - - const title = document.createElement('span'); - title.style.cssText = 'font-size:13px; color:#c9d1d9; font-weight:500;'; - title.textContent = 'Past conversations'; - - header.appendChild(backBtn); - header.appendChild(title); - container.appendChild(header); - - // Fetch list - let conversations = []; - try { - const res = await window.api.invoke('assistant-conversations-list'); - if (res && res.success) conversations = res.conversations || []; - } catch (_) {} - - if (conversations.length === 0) { - const empty = document.createElement('div'); - empty.style.cssText = 'color:#555; font-size:12px; text-align:center; margin-top:32px;'; - empty.textContent = 'No saved conversations yet.'; - container.appendChild(empty); - return; - } - - for (const conv of conversations) { - const item = document.createElement('div'); - item.style.cssText = 'display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:6px; cursor:pointer; border:1px solid #1e2530; margin-bottom:6px; background:#0d1117; transition:background 0.15s;'; - item.onmouseover = () => { item.style.background = '#161b22'; }; - item.onmouseout = () => { item.style.background = '#0d1117'; }; - - const info = document.createElement('div'); - info.style.cssText = 'flex:1; min-width:0;'; - - const convTitle = document.createElement('div'); - convTitle.style.cssText = 'font-size:12px; color:#c9d1d9; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;'; - convTitle.textContent = conv.title || 'Untitled'; - - const date = document.createElement('div'); - date.style.cssText = 'font-size:10px; color:#555; margin-top:2px;'; - const d = new Date(conv.updatedAt || conv.createdAt); - date.textContent = isNaN(d) ? '' : d.toLocaleString(); - - info.appendChild(convTitle); - info.appendChild(date); - - const delBtn = document.createElement('button'); - delBtn.title = 'Delete'; - delBtn.style.cssText = 'background:transparent; border:none; color:#555; cursor:pointer; padding:3px 5px; border-radius:4px; flex-shrink:0; display:flex; align-items:center;'; - delBtn.innerHTML = ''; - delBtn.onmouseover = () => { delBtn.style.color = '#e85555'; }; - delBtn.onmouseout = () => { delBtn.style.color = '#555'; }; - delBtn.onclick = async (e) => { - e.stopPropagation(); - try { await window.api.invoke('assistant-conversations-delete', conv.id); } catch (_) {} - await render(); - }; - - item.appendChild(info); - item.appendChild(delBtn); - - // Click item body to load the conversation - item.onclick = async (e) => { - if (e.target === delBtn || delBtn.contains(e.target)) return; - try { - const res = await window.api.invoke('assistant-conversations-load', conv.id); - if (res && res.success && res.conversation) { - const loaded = res.conversation; - this._conversationHistory = loaded.history || []; - this._currentConvId = loaded.id; - // Rebuild message UI from history - container.innerHTML = ''; - for (const msg of this._conversationHistory) { - const wrap = document.createElement('div'); - wrap.style.marginBottom = '12px'; - const bubble = document.createElement('div'); - bubble.style.padding = '10px 12px'; - bubble.style.borderRadius = '8px'; - bubble.style.maxWidth = '90%'; - bubble.style.lineHeight = '1.4'; - if (msg.role === 'user') { - bubble.style.cssText += 'background:#0b5fff; color:#fff; margin-left:auto; white-space:pre-wrap;'; - bubble.textContent = msg.content; - } else { - bubble.style.cssText += 'background:#111319; color:#e6edf3; margin-right:auto;'; - bubble.innerHTML = (typeof window.renderMarkdown === 'function') - ? window.renderMarkdown(msg.content) - : msg.content.replace(/ { + toolsPanel.style.display = 'none'; + }, 200); + } + } + + toggleToolsPanel() { + const toolsPanel = document.getElementById('toolsPanel'); + if (toolsPanel) { + if (toolsPanel.style.display === 'none' || !toolsPanel.classList.contains('active')) { + this.ui.showToolsPanel(); + } else { + this.ui.hideToolsPanel(); + } + } + } + + openAssistantPanel() { + const toolsPanel = document.getElementById('toolsPanel'); + + // If already in assistant mode and panel is visible, hide it + if (this.ui._toolsPanelMode === 'assistant' && toolsPanel && toolsPanel.classList.contains('active')) { + this.ui.hideToolsPanel(); + return; + } + + // Ensure assistant is configured at least once before opening + const ensure = this.ui.ensureAssistantConfigured(); + Promise.resolve(ensure).then(() => { + if (toolsPanel) { + // Mark that we're in assistant mode + this.ui._toolsPanelMode = 'assistant'; + this.ui.showToolsPanel(); + // Inject assistant chat UI into tools panel + this.ui.renderAssistantUI(); + } + }); + } + +/** + * Open CTrace Tools panel with original content + */ + openCtracePanel() { + const toolsPanel = document.getElementById('toolsPanel'); + + // If already in ctrace mode and panel is visible, hide it + if (this.ui._toolsPanelMode === 'ctrace' && toolsPanel && toolsPanel.classList.contains('active')) { + this.ui.hideToolsPanel(); + return; + } + + if (toolsPanel) { + // Mark that we're in ctrace mode + this.ui._toolsPanelMode = 'ctrace'; + // Restore original CTrace content if we have it saved + if (this.ui._toolsPanelOriginal) { + const header = toolsPanel.querySelector('.tools-panel-header'); + const content = toolsPanel.querySelector('.tools-panel-content'); + if (header && this.ui._toolsPanelOriginal.headerHTML) { + header.innerHTML = this.ui._toolsPanelOriginal.headerHTML; + } + if (content && this.ui._toolsPanelOriginal.contentHTML) { + content.innerHTML = this.ui._toolsPanelOriginal.contentHTML; + } + } + this.ui.showToolsPanel(); + } + } + +/** + * Inject a simple chat UI into the tools panel (like VSCode Copilot sidebar) + */ + renderAssistantUI() { + const toolsPanel = document.getElementById('toolsPanel'); + if (!toolsPanel) return; + + // Save original content so we can restore it later + if (!this.ui._toolsPanelOriginal) { + const header = toolsPanel.querySelector('.tools-panel-header'); + const content = toolsPanel.querySelector('.tools-panel-content'); + this.ui._toolsPanelOriginal = { + headerHTML: header ? header.innerHTML : null, + contentHTML: content ? content.innerHTML : null + }; + } + + const header = toolsPanel.querySelector('.tools-panel-header'); + const content = toolsPanel.querySelector('.tools-panel-content'); + if (!header || !content) return; + + // Update header title + const titleSpan = header.querySelector('span'); + if (titleSpan) titleSpan.textContent = 'Assistant'; + + // Build assistant UI + const cfg = this.ui.getAssistantConfig() || { provider: 'none' }; + + // Get display name for the assistant + let displayName = 'Not configured'; + if (cfg.provider === 'local' && cfg.localModelPath) { + // Extract filename from path and remove .gguf extension + const pathParts = cfg.localModelPath.replace(/\\/g, '/').split('/'); + const filename = pathParts[pathParts.length - 1]; + displayName = filename.replace(/\.gguf$/i, ''); + } else if (cfg.provider === 'external') { + const providerLabel = cfg.externalProvider === 'ChatGPT5' ? 'OpenAI' : (cfg.externalProvider || 'External'); + displayName = cfg.model ? `${providerLabel} — ${cfg.model}` : providerLabel; + } else if (cfg.provider === 'ollama') { + displayName = 'Ollama'; + } else if (cfg.provider !== 'none') { + displayName = cfg.provider; + } + + content.innerHTML = ` +
      +
      +
      Assistant — ${escapeHtml(displayName)}
      +
      + + + +
      +
      +
      + +
      +
      + +
      + + +
      +
      + + + +
      +
      +
      + `; + + // Helper to append messages with typing effect + const addMessage = (who, text, options = {}) => { + const container = document.getElementById('assistant-messages'); + if (!container) return; + const wrap = document.createElement('div'); + wrap.style.marginBottom = '12px'; + const bubble = document.createElement('div'); + bubble.style.padding = '10px 12px'; + bubble.style.borderRadius = '8px'; + bubble.style.maxWidth = '90%'; + bubble.style.lineHeight = '1.4'; + + if (who === 'user') { + bubble.style.background = '#0b5fff'; + bubble.style.color = '#fff'; + bubble.style.marginLeft = 'auto'; + bubble.style.whiteSpace = 'pre-wrap'; + bubble.textContent = text; + wrap.appendChild(bubble); + container.appendChild(wrap); + container.scrollTop = container.scrollHeight; + } else { + bubble.style.background = '#111319'; + bubble.style.color = '#e6edf3'; + bubble.style.marginRight = 'auto'; + bubble.style.animation = 'assistantFadeIn 0.2s ease-in'; + wrap.appendChild(bubble); + container.appendChild(wrap); + + // Return the bubble element for typing effects + if (options.typing) { + return bubble; + } + + // Render markdown for assistant messages and return bubble + bubble.innerHTML = renderMarkdown(text); + container.scrollTop = container.scrollHeight; + return bubble; + } + }; + + // Typing effect for assistant messages + const typeMessage = async (bubble, text, speed = 0.1) => { // Changed from 20 to 10ms (faster). Increase for slower, decrease for faster + const container = document.getElementById('assistant-messages'); + let currentText = ''; + + for (let i = 0; i < text.length; i++) { + currentText += text[i]; + bubble.innerHTML = renderMarkdown(currentText); + if (container) container.scrollTop = container.scrollHeight; + await new Promise(resolve => setTimeout(resolve, speed)); + } + }; + + // Animated thinking indicator + const addThinkingMessage = () => { + const bubble = addMessage('assistant', '', { typing: true }); + if (!bubble) return null; + + let dotCount = 0; + const thinkingInterval = setInterval(() => { + dotCount = (dotCount % 3) + 1; + bubble.textContent = 'Thinking' + '.'.repeat(dotCount); + const container = document.getElementById('assistant-messages'); + if (container) container.scrollTop = container.scrollHeight; + }, 400); + + return { bubble, interval: thinkingInterval }; + }; + + // Remove thinking message + const removeThinkingMessage = (thinkingData) => { + if (!thinkingData) return; + clearInterval(thinkingData.interval); + const container = document.getElementById('assistant-messages'); + if (container && thinkingData.bubble && thinkingData.bubble.parentElement) { + container.removeChild(thinkingData.bubble.parentElement); + } + }; + + // Simple markdown renderer + const renderMarkdown = (text) => { + // Trim leading/trailing whitespace to avoid extra newlines + text = text.trim(); + + // Store original code blocks before any processing + const codeBlocks = []; + let codeIndex = 0; + + // Extract and store original code blocks with language info + const textWithPlaceholders = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { + const trimmedCode = code.trim(); + codeBlocks.push({ code: trimmedCode, lang: lang || '' }); + return `__CODE_BLOCK_${codeIndex++}__`; + }); + + // Escape HTML + let html = textWithPlaceholders.replace(/&/g, '&') + .replace(//g, '>'); + + // Replace code block placeholders with rendered HTML + codeIndex = 0; + html = html.replace(/__CODE_BLOCK_(\d+)__/g, (match, index) => { + const blockData = codeBlocks[parseInt(index)]; + const originalCode = blockData.code; + const lang = blockData.lang.toLowerCase(); + + // Store original code in base64 to avoid any escaping issues + const base64Code = btoa(unescape(encodeURIComponent(originalCode))); + + // Apply syntax highlighting for C/C++ code + let displayCode; + if (lang === 'c' || lang === 'cpp' || lang === 'c++') { + // Use syntax highlighter + const { applySyntaxHighlight } = window.syntaxHighlighter; + const fileType = lang === 'c' ? 'C' : 'C++'; + displayCode = applySyntaxHighlight(originalCode, fileType); + } else { + // No highlighting - just escape HTML + displayCode = originalCode.replace(/&/g, '&') + .replace(//g, '>'); + } + + const langLabel = lang ? `${lang}` : ''; + + return `
      + ${langLabel} +
      + + +
      +
      ${displayCode}
      +
      `; + }); + + // Inline code (`code`) + html = html.replace(/`([^`]+)`/g, '$1'); + + // Bold (**text** or __text__) + html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); + html = html.replace(/__([^_]+)__/g, '$1'); + + // Italic (*text* or _text_) + html = html.replace(/\*([^*]+)\*/g, '$1'); + html = html.replace(/_([^_]+)_/g, '$1'); + + // Headers (### text) + html = html.replace(/^### (.+)$/gm, '

      $1

      '); + html = html.replace(/^## (.+)$/gm, '

      $1

      '); + html = html.replace(/^# (.+)$/gm, '

      $1

      '); + + // Lists (- item or * item or 1. item) + html = html.replace(/^- (.+)$/gm, '
    • $1
    • '); + html = html.replace(/^\* (.+)$/gm, '
    • $1
    • '); + html = html.replace(/^\d+\. (.+)$/gm, '
    • $1
    • '); + + // Wrap consecutive
    • in
        + html = html.replace(/(]*>.*<\/li>\n?)+/g, (match) => { + return `
          ${match}
        `; + }); + + // Line breaks (preserve double newlines as paragraphs) + html = html.replace(/\n\n/g, '

        '); + + return html; + }; + + // Prefill a small welcome message with typing effect (only first time) + const providerLabel = cfg.provider === 'external' + ? (() => { + const base = cfg.externalProvider === 'ChatGPT5' ? 'OpenAI' : (cfg.externalProvider || 'External'); + return cfg.model ? `${base} — ${cfg.model}` : base; + })() + : cfg.provider; + const welcomeText = `Hi — I'm your assistant. Using: ${providerLabel}. Ask me something or open Settings to change providers.`; + + // Check if welcome message has been shown before + const hasShownWelcome = sessionStorage.getItem('assistantWelcomeShown'); + + if (!hasShownWelcome) { + // First time - show typing effect (faster speed: 8ms per character) + const welcomeBubble = addMessage('assistant', '', { typing: true }); + if (welcomeBubble) { + typeMessage(welcomeBubble, welcomeText, 8); + } + sessionStorage.setItem('assistantWelcomeShown', 'true'); + } else { + // Already shown - display instantly + addMessage('assistant', welcomeText); + } + + // Wire up send button + const sendBtn = document.getElementById('assistant-send'); + const inputEl = document.getElementById('assistant-input'); + const settingsBtn = document.getElementById('assistant-settings'); + const newChatBtn = document.getElementById('assistant-new-chat'); + const contextIndicator = document.getElementById('context-indicator'); + const contextText = document.getElementById('context-text'); + const contextClearBtn = document.getElementById('context-clear'); + + const historyBtn = document.getElementById('assistant-history'); + + // New conversation button — auto-saves current chat then resets + newChatBtn.onclick = async () => { + await this._saveCurrentConversation(); + const messagesEl = document.getElementById('assistant-messages'); + if (messagesEl) messagesEl.innerHTML = ''; + this._conversationHistory = []; + this._currentConvId = null; + this._inputHistory = []; + this._inputHistoryCursor = -1; + this._inputDraft = ''; + }; + + // History button — show past conversations panel + historyBtn.onclick = () => this._showHistoryPanel(); + + // Capture selection before it's lost when user clicks on input + let capturedSelection = ''; + let capturedLineInfo = ''; + let capturedContextType = 'selection'; // 'selection' | 'file' | 'analysis' + + inputEl.addEventListener('focus', () => { + try { + const monacoEditor = this.ui.editorManager.getMonacoInstance ? this.ui.editorManager.getMonacoInstance() : null; + if (monacoEditor) { + const selection = monacoEditor.getSelection(); + const model = monacoEditor.getModel(); + if (selection && model) { + const selectedText = model.getValueInRange(selection); + if (selectedText) { + capturedSelection = selectedText; + const startLine = selection.startLineNumber; + const endLine = selection.endLineNumber; + const activeTab = this.ui.tabManager.getActiveTab(); + const fileName = activeTab && activeTab.fileName ? activeTab.fileName : 'Untitled'; + capturedLineInfo = startLine === endLine ? `${fileName}: ${startLine}` : `${fileName}: ${startLine}-${endLine}`; + contextText.textContent = capturedLineInfo; + contextIndicator.style.display = 'block'; + } + } + } else { + // Fallback for legacy textarea editor + const editor = this.ui.editorManager.editor; + const start = editor.selectionStart; + const end = editor.selectionEnd; + const selection = editor.value.substring(start, end); + if (selection) { + capturedSelection = selection; + const textBeforeStart = editor.value.substring(0, start); + const textBeforeEnd = editor.value.substring(0, end); + const startLine = (textBeforeStart.match(/\n/g) || []).length + 1; + const endLine = (textBeforeEnd.match(/\n/g) || []).length + 1; + const activeTab = this.ui.tabManager.getActiveTab(); + const fileName = activeTab && activeTab.fileName ? activeTab.fileName : 'Untitled'; + if (startLine === endLine) { + capturedLineInfo = `${fileName}: ${startLine}`; + } else { + capturedLineInfo = `${fileName}: ${startLine}-${endLine}`; + } + contextText.textContent = capturedLineInfo; + contextIndicator.style.display = 'block'; + } + } + } catch (err) { + console.warn('Error capturing selection for assistant context:', err); + } + }); + + // Handle Enter key to send message (Shift+Enter for new line) + // Arrow keys navigate input history like a terminal + inputEl.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendBtn.click(); + } else if (e.key === 'ArrowUp') { + if (!this._inputHistory || this._inputHistory.length === 0) return; + if (this._inputHistoryCursor === -1) { + this._inputDraft = inputEl.value; + this._inputHistoryCursor = this._inputHistory.length - 1; + } else if (this._inputHistoryCursor > 0) { + this._inputHistoryCursor--; + } + inputEl.value = this._inputHistory[this._inputHistoryCursor]; + e.preventDefault(); + } else if (e.key === 'ArrowDown') { + if (!this._inputHistory || this._inputHistoryCursor === -1) return; + if (this._inputHistoryCursor < this._inputHistory.length - 1) { + this._inputHistoryCursor++; + inputEl.value = this._inputHistory[this._inputHistoryCursor]; + } else { + this._inputHistoryCursor = -1; + inputEl.value = this._inputDraft || ''; + } + e.preventDefault(); + } + }); + + // Clear captured selection when input loses focus without sending + inputEl.addEventListener('blur', () => { + // Small delay to allow send button click to process + setTimeout(() => { + if (!inputEl.value.trim()) { + capturedSelection = ''; + capturedLineInfo = ''; + capturedContextType = 'selection'; + contextIndicator.style.display = 'none'; + document.getElementById('ctx-file').textContent = '📄 Current file'; + } + }, 200); + }); + + // Clear button handler + contextClearBtn.onclick = () => { + capturedSelection = ''; + capturedLineInfo = ''; + capturedContextType = 'selection'; + contextIndicator.style.display = 'none'; + document.getElementById('ctx-file').textContent = '📄 Current file'; + }; + + // Context shortcut — current file (or current selection if text is highlighted) + document.getElementById('ctx-file').onclick = () => { + try { + const monaco = this.ui.editorManager.getMonacoInstance ? this.ui.editorManager.getMonacoInstance() : null; + const activeTab = this.ui.tabManager.getActiveTab(); + const fileName = activeTab && activeTab.fileName ? activeTab.fileName : 'file'; + const ctxFileBtn = document.getElementById('ctx-file'); + let content = ''; + if (monaco) { + const model = monaco.getModel(); + const sel = monaco.getSelection(); + if (!model) return; + if (sel && !sel.isEmpty()) { + // User has highlighted a range — capture only that + content = model.getValueInRange(sel); + const s = sel.startLineNumber; + const e = sel.endLineNumber; + capturedContextType = 'selection'; + capturedLineInfo = s === e ? `${fileName}:${s}` : `${fileName}:${s}-${e}`; + } else { + // No selection — capture entire file + content = model.getValue(); + const totalLines = model.getLineCount(); + capturedContextType = 'file'; + capturedLineInfo = `${fileName}:1-${totalLines}`; + } + } else { + const editor = this.ui.editorManager.editor; + if (!editor) return; + const start = editor.selectionStart; + const end = editor.selectionEnd; + if (start !== end) { + content = editor.value.substring(start, end); + const before = editor.value.substring(0, start); + const beforeEnd = editor.value.substring(0, end); + const sLine = (before.match(/\n/g) || []).length + 1; + const eLine = (beforeEnd.match(/\n/g) || []).length + 1; + capturedContextType = 'selection'; + capturedLineInfo = sLine === eLine ? `${fileName}:${sLine}` : `${fileName}:${sLine}-${eLine}`; + } else { + content = editor.value; + const totalLines = (content.match(/\n/g) || []).length + 1; + capturedContextType = 'file'; + capturedLineInfo = `${fileName}:1-${totalLines}`; + } + } + if (!content.trim()) return; + capturedSelection = content; + ctxFileBtn.textContent = `📄 ${capturedLineInfo}`; + contextText.textContent = capturedLineInfo; + contextIndicator.style.display = 'block'; + inputEl.focus(); + } catch (e) { + console.warn('Error capturing file context:', e); + } + }; + + // Context shortcut — analysis results + document.getElementById('ctx-analysis').onclick = () => { + try { + const dm = this.ui.diagnosticsManager; + const diags = dm && dm.currentDiagnostics; + const ctxBtn = document.getElementById('ctx-analysis'); + if (!diags || diags.length === 0) { + const orig = ctxBtn.textContent; + ctxBtn.textContent = 'No results'; + setTimeout(() => { ctxBtn.textContent = orig; }, 1500); + return; + } + const lines = [`${diags.length} issue(s) found:`]; + for (const d of diags) { + const loc = d.location || {}; + const file = loc.file ? loc.file.split('/').pop().split('\\').pop() : '?'; + const msg = (d.details && d.details.message) ? d.details.message : ''; + lines.push(`[${d.severity}] ${file}:${loc.startLine} (${d.ruleId}) — ${msg}`); + } + capturedSelection = lines.join('\n'); + capturedContextType = 'analysis'; + capturedLineInfo = `Analysis: ${diags.length} issue(s)`; + contextText.textContent = capturedLineInfo; + contextIndicator.style.display = 'block'; + inputEl.focus(); + } catch (e) { + console.warn('Error capturing analysis context:', e); + } + }; + + const stopBtn = document.getElementById('assistant-stop'); + + const setRequestingState = (requesting) => { + if (requesting) { + sendBtn.style.display = 'none'; + stopBtn.style.display = 'flex'; + } else { + sendBtn.style.display = 'flex'; + stopBtn.style.display = 'none'; + } + }; + + stopBtn.onclick = async () => { + try { await window.api.invoke('assistant-abort'); } catch (_) {} + setRequestingState(false); + }; + + sendBtn.onclick = async () => { + if (sendBtn.style.display === 'none') return; // already requesting + const text = (inputEl.value || '').trim(); + if (!text) return; + + // Save to input history for ArrowUp navigation + this._inputHistory.push(text); + this._inputHistoryCursor = -1; + this._inputDraft = ''; + + // Use captured selection as context + let context = ''; + if (capturedSelection) { + if (capturedContextType === 'analysis') { + context = `\n\n[Context - Analysis Results]:\n${capturedSelection}\n\n`; + } else { + // For both 'file' (full file) and 'selection' (highlighted range), use the line info label + const label = capturedLineInfo || (capturedContextType === 'file' ? 'File' : 'Selected Code'); + context = `\n\n[Context - ${label}]:\n\`\`\`\n${capturedSelection}\n\`\`\`\n\n`; + } + } + + // Clear captured context and hide indicator after using it + capturedSelection = ''; + capturedLineInfo = ''; + capturedContextType = 'selection'; + contextIndicator.style.display = 'none'; + document.getElementById('ctx-file').textContent = '📄 Current file'; + + // Combine user message with context (context goes to LLM, only text shown in UI) + const fullMessage = context ? context + text : text; + + // Display only user's text in UI + addMessage('user', text); + inputEl.value = ''; + + // Get current assistant config + const cfg = this.ui.getAssistantConfig(); + if (!cfg || cfg.provider === 'none' || cfg.skipped) { + addMessage('assistant', 'Assistant not configured. Please click Settings ⚙️ to set up your provider.'); + return; + } + + setRequestingState(true); + + // Show animated thinking indicator + const thinkingData = addThinkingMessage(); + + try { + // All providers go through IPC (main process); send conversation history + const result = await window.api.invoke('assistant-chat', { + provider: cfg.provider, + message: fullMessage, + history: this._conversationHistory, + config: cfg + }); + + // Remove the thinking message + removeThinkingMessage(thinkingData); + + if (result && result.aborted) { + // User cancelled — no message, no history update + return; + } + + if (result && result.success) { + // Update in-memory conversation history for next request + this._conversationHistory.push({ role: 'user', content: fullMessage }); + this._conversationHistory.push({ role: 'assistant', content: result.reply }); + + // Render the full response immediately so code-block buttons are interactive at once + const bubble = addMessage('assistant', result.reply); + if (bubble) { + attachCodeActionListeners(); + } + + // Auto-save conversation after each exchange + this._saveCurrentConversation(); + } else { + const errorMsg = result && result.error ? result.error : 'Unknown error occurred'; + addMessage('assistant', `❌ Error: ${errorMsg}`); + } + } catch (err) { + removeThinkingMessage(thinkingData); + console.error('Assistant chat error:', err); + addMessage('assistant', `❌ Error: ${err.message || 'Failed to communicate with assistant'}`); + } finally { + setRequestingState(false); + } + }; + + // Helper to attach event listeners to code action buttons + const attachCodeActionListeners = () => { + const container = document.getElementById('assistant-messages'); + if (!container) return; + + // Copy button handlers + container.querySelectorAll('.code-copy-btn').forEach(btn => { + btn.onclick = () => { + const base64Code = btn.getAttribute('data-code-b64'); + const code = decodeURIComponent(escape(atob(base64Code))); + + try { + window.api.clipboard.writeText(code); + btn.textContent = 'Copied!'; + setTimeout(() => btn.textContent = 'Copy', 2000); + } catch (_) { + if (navigator && navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(code) + .then(() => { + btn.textContent = 'Copied!'; + setTimeout(() => btn.textContent = 'Copy', 2000); + }) + .catch(() => alert('Failed to copy code')); + } + } + }; + }); + + // Replace button handlers + container.querySelectorAll('.code-replace-btn').forEach(btn => { + btn.onclick = () => { + const base64Code = btn.getAttribute('data-code-b64'); + const code = decodeURIComponent(escape(atob(base64Code))); + try { + const monacoEditor = this.ui.editorManager.getMonacoInstance ? this.ui.editorManager.getMonacoInstance() : null; + if (monacoEditor) { + const model = monacoEditor.getModel(); + const selection = monacoEditor.getSelection(); + let rangeObj = null; + let actionLabel = 'Inserted!'; + + if (selection && typeof selection.isEmpty === 'function' ? !selection.isEmpty() : (selection.startLineNumber !== selection.endLineNumber || selection.startColumn !== selection.endColumn)) { + rangeObj = { + startLineNumber: selection.startLineNumber, + startColumn: selection.startColumn, + endLineNumber: selection.endLineNumber, + endColumn: selection.endColumn + }; + actionLabel = 'Replaced!'; + } else { + const pos = monacoEditor.getPosition(); + rangeObj = { startLineNumber: pos.lineNumber, startColumn: pos.column, endLineNumber: pos.lineNumber, endColumn: pos.column }; + actionLabel = 'Inserted!'; + } + + monacoEditor.executeEdits('assistant', [{ range: rangeObj, text: code, forceMoveMarkers: true }]); + // Update tab state + if (this.ui.tabManager.activeTabId && model) { + this.ui.tabManager.handleContentChange(this.ui.tabManager.activeTabId, model.getValue()); + } + btn.textContent = actionLabel; + setTimeout(() => btn.textContent = 'Replace', 2000); + monacoEditor.focus(); + } else { + // Fallback to legacy textarea editor + const editor = this.ui.editorManager.editor; + const start = editor.selectionStart; + const end = editor.selectionEnd; + if (start !== end) { + // Replace selected text + const before = editor.value.substring(0, start); + const after = editor.value.substring(end); + editor.value = before + code + after; + if (this.ui.tabManager.activeTabId) { + this.ui.tabManager.handleContentChange(this.ui.tabManager.activeTabId, editor.value); + } + btn.textContent = 'Replaced!'; + setTimeout(() => btn.textContent = 'Replace', 2000); + } else { + const before = editor.value.substring(0, start); + const after = editor.value.substring(start); + editor.value = before + code + after; + if (this.ui.tabManager.activeTabId) { + this.ui.tabManager.handleContentChange(this.ui.tabManager.activeTabId, editor.value); + } + btn.textContent = 'Inserted!'; + setTimeout(() => btn.textContent = 'Replace', 2000); + } + editor.focus(); + } + } catch (err) { + console.error('Error replacing/inserting code from assistant:', err); + btn.textContent = 'Error'; + setTimeout(() => btn.textContent = 'Replace', 2000); + } + }; + }); + }; + + settingsBtn.onclick = () => { + // Open the assistant setup modal for reconfiguration + this.ui.showAssistantSetupGuide((cfg) => { + // Re-render assistant UI to reflect changes + this.ui.renderAssistantUI(); + }); + }; + } + +/** + * Return the in-memory cached assistant configuration (synchronous). + * Call loadAssistantConfig() first to ensure the cache is populated. + * @returns {Object|null} + */ + getAssistantConfig() { + return this._cachedConfig || null; + } + +/** + * Load assistant configuration from secure storage via IPC. + * Migrates any existing plaintext config from localStorage on first run, + * then removes it. Updates the in-memory cache. + * @returns {Promise} + */ + async loadAssistantConfig() { + try { + const result = await window.api.invoke('assistant-config-load'); + if (result.success) { + this._cachedConfig = result.config; + return this._cachedConfig; + } + + // Migration path: lift any legacy plaintext config from localStorage + const raw = localStorage.getItem('assistantConfig'); + if (raw) { + try { + const legacy = JSON.parse(raw); + const saved = await window.api.invoke('assistant-config-save', legacy); + if (!saved.success) throw new Error(saved.error || 'save failed'); + localStorage.removeItem('assistantConfig'); + console.log('[AssistantPanel] Migrated assistant config from localStorage to secure storage'); + return this.loadAssistantConfig(); + } catch (migErr) { + console.warn('[AssistantPanel] Migration from localStorage failed:', migErr); + } + } + + this._cachedConfig = null; + return null; + } catch (err) { + console.error('[AssistantPanel] Failed to load assistant config:', err); + this._cachedConfig = null; + return null; + } + } + +/** + * Persist assistant configuration via secure IPC storage and update cache. + * @param {Object} cfg + * @returns {Promise} + */ + async saveAssistantConfig(cfg) { + try { + const result = await window.api.invoke('assistant-config-save', cfg); + if (!result.success) throw new Error(result.error || 'Unknown error'); + // Re-read from storage so the cache never holds the key, only hasApiKey. + await this.loadAssistantConfig(); + this.ui.notificationManager.showSuccess('Assistant settings saved'); + } catch (err) { + console.error('[AssistantPanel] Failed to save assistant config:', err); + this.ui.notificationManager.showError('Failed to save assistant settings'); + } + } + +/** + * Render a first-time setup modal for Assistant configuration. + * Calls the done callback with saved config or null if cancelled. + */ + showAssistantSetupGuide(done) { + // Use the in-memory cache (should already be loaded by ensureAssistantConfigured) + const existingConfig = this.ui.getAssistantConfig() || {}; + + // Modal overlay + const modal = document.createElement('div'); + modal.style.cssText = ` + position: fixed; top:0; left:0; width:100%; height:100%; + background: rgba(0,0,0,0.6); display:flex; align-items:center; justify-content:center; z-index:10001; + `; + + const dialog = document.createElement('div'); + dialog.style.cssText = ` + width: 720px; max-width: 95%; background: #161b22; border-radius:8px; padding:20px; box-shadow:0 8px 30px rgba(0,0,0,0.6); border:1px solid #30363d; + font-family: sans-serif; color: #e6edf3; + `; + + dialog.innerHTML = ` +

        Assistant setup

        +

        Choose how you'd like to connect the Assistant. You can use Ollama, an external API (ChatGPT5, Deepseek, or other), or point to a local GGUF model on your machine.

        +
        + + + +
        + +
        + +
        + + + +
        + `; + + modal.appendChild(dialog); + document.body.appendChild(modal); + + const extra = dialog.querySelector('#assist-extra'); + + const clearExtra = () => { extra.innerHTML = ''; }; + + const makeInputRow = (labelText, inputId, placeholder = '') => { + const row = document.createElement('div'); + row.style.cssText = 'display:flex; flex-direction:column; gap:6px; margin-top:8px;'; + row.innerHTML = ` + + + `; + return row; + }; + + const providerRadios = dialog.querySelectorAll('input[name="assist-provider"]'); + const optOllama = dialog.querySelector('#assist-opt-ollama'); + const optExternal = dialog.querySelector('#assist-opt-external'); + const optLocal = dialog.querySelector('#assist-opt-local'); + + const selectProvider = (value) => { + providerRadios.forEach(r => r.checked = (r.value === value)); + optOllama.style.borderColor = value === 'ollama' ? '#388bfd' : '#30363d'; + optExternal.style.borderColor = value === 'external' ? '#388bfd' : '#30363d'; + optLocal.style.borderColor = value === 'local' ? '#388bfd' : '#30363d'; + + clearExtra(); + if (value === 'ollama') { + extra.appendChild(makeInputRow('Ollama host (include protocol)', 'ollama-host', 'http://localhost:11434')); + extra.appendChild(makeInputRow('System prompt (optional)', 'system-prompt', 'You are a helpful assistant...')); + + // Pre-fill with saved values + setTimeout(() => { + const hostInput = document.getElementById('ollama-host'); + const systemInput = document.getElementById('system-prompt'); + if (hostInput && existingConfig.ollamaHost) { + hostInput.value = existingConfig.ollamaHost; + } + if (systemInput && existingConfig.systemPrompt) { + systemInput.value = existingConfig.systemPrompt; + } + }, 0); + } else if (value === 'external') { + const selRow = document.createElement('div'); + selRow.style.cssText = 'display:flex; gap:8px; align-items:center; margin-top:8px;'; + selRow.innerHTML = ` + + + `; + extra.appendChild(selRow); + extra.appendChild(makeInputRow('API Key', 'external-api-key', 'sk-...')); + + // Model list row — starts as a text input, upgraded to select after fetch + const modelRow = document.createElement('div'); + modelRow.style.cssText = 'display:flex; flex-direction:column; gap:6px; margin-top:8px;'; + modelRow.innerHTML = ` +
        + + +
        + + + `; + extra.appendChild(modelRow); + extra.appendChild(makeInputRow('System prompt (optional)', 'system-prompt', 'You are a helpful assistant...')); + + // Helper: map UI provider name → IPC providerId + const toProviderId = (name) => name === 'Deepseek' ? 'deepseek' : 'openai'; + + // Helper: populate model dropdown from fetched list + const populateModelSelect = (models, savedModel) => { + const sel = document.getElementById('external-model'); + const inp = document.getElementById('external-model-input'); + if (!sel || !inp) return; + sel.innerHTML = models.map(m => + `` + ).join(''); + sel.style.display = 'block'; + inp.style.display = 'none'; + }; + + // Fetch models when API key field loses focus + const attachFetchListener = () => { + const apiKeyInput = document.getElementById('external-api-key'); + const providerSelect = document.getElementById('external-provider'); + const statusEl = document.getElementById('model-fetch-status'); + if (!apiKeyInput) return; + + apiKeyInput.addEventListener('blur', async () => { + const key = apiKeyInput.value.trim(); + if (!key) return; + const providerId = toProviderId(providerSelect ? providerSelect.value : 'ChatGPT5'); + if (statusEl) statusEl.textContent = 'Fetching models…'; + try { + const result = await window.api.invoke('assistant-list-models', { providerId, apiKey: key }); + if (result.success && result.models.length > 0) { + const currentModel = document.getElementById('external-model-input').value.trim(); + populateModelSelect(result.models, currentModel); + if (statusEl) statusEl.textContent = `${result.models.length} models loaded`; + } else { + if (statusEl) statusEl.textContent = result.error ? `Error: ${result.error}` : 'No models returned'; + } + } catch (err) { + if (statusEl) statusEl.textContent = 'Failed to fetch models'; + } + }); + + // Re-fetch when provider changes (if key already present) + if (providerSelect) { + providerSelect.addEventListener('change', async () => { + const key = apiKeyInput.value.trim(); + if (!key) return; + const providerId = toProviderId(providerSelect.value); + if (statusEl) statusEl.textContent = 'Fetching models…'; + try { + const result = await window.api.invoke('assistant-list-models', { providerId, apiKey: key }); + if (result.success && result.models.length > 0) { + const currentModel = document.getElementById('external-model-input')?.value.trim() || ''; + populateModelSelect(result.models, currentModel); + if (statusEl) statusEl.textContent = `${result.models.length} models loaded`; + } else { + if (statusEl) statusEl.textContent = result.error ? `Error: ${result.error}` : 'No models returned'; + } + } catch (err) { + if (statusEl) statusEl.textContent = 'Failed to fetch models'; + } + }); + } + }; + + // Pre-fill with saved values and auto-fetch if key already exists + setTimeout(async () => { + const providerSelect = document.getElementById('external-provider'); + const apiKeyInput = document.getElementById('external-api-key'); + const modelInput = document.getElementById('external-model-input'); + const systemInput = document.getElementById('system-prompt'); + if (providerSelect && existingConfig.externalProvider) { + providerSelect.value = existingConfig.externalProvider; + } + if (apiKeyInput && existingConfig.hasApiKey) { + apiKeyInput.placeholder = 'Saved key on file — leave blank to keep it'; + } + if (modelInput && existingConfig.model) { + modelInput.value = existingConfig.model; + } + if (systemInput && existingConfig.systemPrompt) { + systemInput.value = existingConfig.systemPrompt; + } + + // If a saved API key exists, fetch models immediately (the key itself stays in the main process) + if (existingConfig.hasApiKey) { + const providerId = toProviderId(existingConfig.externalProvider || 'ChatGPT5'); + const statusEl = document.getElementById('model-fetch-status'); + if (statusEl) statusEl.textContent = 'Fetching models…'; + try { + const result = await window.api.invoke('assistant-list-models', { providerId, useStoredKey: true }); + if (result.success && result.models.length > 0) { + populateModelSelect(result.models, existingConfig.model); + if (statusEl) statusEl.textContent = `${result.models.length} models loaded`; + } else { + if (statusEl) statusEl.textContent = result.error ? `Error: ${result.error}` : ''; + } + } catch (_) { /* non-fatal */ } + } + + attachFetchListener(); + }, 0); + } else if (value === 'local') { + const row = document.createElement('div'); + row.style.cssText = 'display:flex; gap:8px; align-items:center; margin-top:8px;'; + row.innerHTML = ` + + + `; + extra.appendChild(row); + + // Browse handler using IPC + setTimeout(() => { + const browseBtn = document.getElementById('local-browse'); + const pathInput = document.getElementById('local-model-path'); + + // Pre-fill with saved value + if (pathInput && existingConfig.localModelPath) { + pathInput.value = existingConfig.localModelPath; + } + + if (browseBtn) { + browseBtn.onclick = async () => { + try { + const result = await window.api.invoke('select-llm-file'); + if (result && result.filePath) { + pathInput.value = result.filePath; + } + } catch (err) { + console.error('Error selecting model file', err); + this.ui.notificationManager.showError('Unable to open file selector'); + } + }; + } + }, 0); + + // Add context size configuration + const contextRow = document.createElement('div'); + contextRow.style.cssText = 'margin-top:12px;'; + contextRow.innerHTML = ` + + +
        Lower values use less VRAM. Recommended: 2048-8192. Default model max: 40960
        + `; + extra.appendChild(contextRow); + + // Add GPU layers configuration + const gpuRow = document.createElement('div'); + gpuRow.style.cssText = 'margin-top:12px;'; + gpuRow.innerHTML = ` + + +
        Higher values offload more layers to GPU for faster inference. Use -1 to offload all layers.
        + `; + extra.appendChild(gpuRow); + + // Add system prompt field for local models too + extra.appendChild(makeInputRow('System prompt (optional)', 'system-prompt', 'You are a helpful assistant...')); + + // Pre-fill all settings + setTimeout(() => { + const systemInput = document.getElementById('system-prompt'); + const gpuLayersInput = document.getElementById('gpu-layers'); + const contextSizeInput = document.getElementById('context-size'); + + if (systemInput && existingConfig.systemPrompt) { + systemInput.value = existingConfig.systemPrompt; + } + if (gpuLayersInput && existingConfig.gpuLayers !== undefined) { + gpuLayersInput.value = existingConfig.gpuLayers; + } + if (contextSizeInput && existingConfig.contextSize !== undefined) { + contextSizeInput.value = existingConfig.contextSize; + } + }, 0); + } + }; + + // Click handlers for the option cards as well + optOllama.onclick = () => selectProvider('ollama'); + optExternal.onclick = () => selectProvider('external'); + optLocal.onclick = () => selectProvider('local'); + + // Pre-select provider based on saved config, or default to external + const savedProvider = existingConfig.provider && existingConfig.provider !== 'none' + ? existingConfig.provider + : 'external'; + selectProvider(savedProvider); + + // Buttons + const btnSave = dialog.querySelector('#assist-save'); + const btnCancel = dialog.querySelector('#assist-cancel'); + const btnSkip = dialog.querySelector('#assist-skip'); + + let escHandlerRef = null; + const closeModal = (result) => { + try { document.body.removeChild(modal); } catch (_) {} + if (escHandlerRef) { document.removeEventListener('keydown', escHandlerRef); escHandlerRef = null; } + if (done) done(result); + }; + + btnCancel.onclick = () => closeModal(null); + btnSkip.onclick = async () => { + // Save a lightweight config indicating user skipped + const cfg = { provider: 'none', skipped: true }; + await this.ui.saveAssistantConfig(cfg); + closeModal(cfg); + }; + + btnSave.onclick = async () => { + const selected = Array.from(providerRadios).find(r => r.checked); + if (!selected) { + this.ui.notificationManager.showError('Please select a provider'); + return; + } + const provider = selected.value; + const cfg = { provider }; + + // Get system prompt if it exists + const systemPromptEl = document.getElementById('system-prompt'); + if (systemPromptEl && systemPromptEl.value.trim()) { + cfg.systemPrompt = systemPromptEl.value.trim(); + } + + if (provider === 'ollama') { + const hostEl = document.getElementById('ollama-host'); + cfg.ollamaHost = hostEl && hostEl.value ? hostEl.value.trim() : 'http://localhost:11434'; + } else if (provider === 'external') { + const prov = document.getElementById('external-provider'); + const key = document.getElementById('external-api-key'); + // Model is either the populated select or the fallback text input + const modelSel = document.getElementById('external-model'); + const modelInp = document.getElementById('external-model-input'); + const modelValue = (modelSel && modelSel.style.display !== 'none') + ? modelSel.value.trim() + : (modelInp ? modelInp.value.trim() : ''); + + cfg.externalProvider = prov ? prov.value : 'ChatGPT5'; + cfg.apiKey = key ? key.value.trim() : ''; + cfg.model = modelValue; + + // Map UI selection to backend provider ID + if (cfg.externalProvider === 'Deepseek') { + cfg.providerId = 'deepseek'; + if (!cfg.model) cfg.model = 'deepseek-chat'; + } else if (cfg.externalProvider === 'ChatGPT5') { + cfg.providerId = 'openai'; + if (!cfg.model) cfg.model = 'gpt-4'; + } else { + // Default to openai for compatibility if not specified + cfg.providerId = 'openai'; + if (!cfg.model) cfg.model = 'gpt-4'; + } + + const hasStoredKey = Boolean((this.ui.getAssistantConfig() || {}).hasApiKey); + if (!cfg.apiKey && !hasStoredKey) { + this.ui.notificationManager.showError('Please enter an API key for the external provider'); + return; + } + } else if (provider === 'local') { + const pathEl = document.getElementById('local-model-path'); + const gpuLayersEl = document.getElementById('gpu-layers'); + const contextSizeEl = document.getElementById('context-size'); + + cfg.localModelPath = pathEl ? pathEl.value : ''; + if (!cfg.localModelPath) { + this.ui.notificationManager.showError('Please choose a local GGUF model file'); + return; + } + + // Save GPU layers setting (default to 0 if not specified) + cfg.gpuLayers = gpuLayersEl && gpuLayersEl.value !== '' ? parseInt(gpuLayersEl.value, 10) : 0; + + // Save context size setting (default to 8192 if not specified) + cfg.contextSize = contextSizeEl && contextSizeEl.value !== '' ? parseInt(contextSizeEl.value, 10) : 8192; + } + + // Persist and close + // Detect system prompt change — clear conversation history so AI gets a fresh context + const prevCfg = this.ui.getAssistantConfig() || {}; + if ((prevCfg.systemPrompt || '') !== (cfg.systemPrompt || '')) { + this._conversationHistory = []; + } + + await this.ui.saveAssistantConfig(cfg); + // Notify main process in case it needs to warm things up + try { window.api.send('assistant-config-updated', cfg); } catch (_) {} + closeModal(cfg); + }; + + // Clicking outside the dialog triggers Save (same as clicking the Save button) + modal.onclick = (e) => { if (e.target === modal) btnSave.click(); }; + // Escape key closes without saving + const escHandler = (e) => { if (e.key === 'Escape') { closeModal(null); } }; + escHandlerRef = escHandler; + document.addEventListener('keydown', escHandler); + } + + // ── Conversation persistence ──────────────────────────────────────────────── + + /** + * Persist the current conversation to disk. No-op if the history is empty. + */ + async _saveCurrentConversation() { + if (this._conversationHistory.length === 0) return; + if (!this._currentConvId) { + this._currentConvId = String(Date.now()); + } + const firstUserMsg = this._conversationHistory.find(m => m.role === 'user'); + const title = firstUserMsg ? firstUserMsg.content.replace(/\n/g, ' ').slice(0, 72) : 'Conversation'; + try { + await window.api.invoke('assistant-conversations-save', { + id: this._currentConvId, + title, + history: this._conversationHistory, + createdAt: new Date().toISOString() + }); + } catch (_) { /* non-critical */ } + } + + /** + * Show the history panel inside the messages area. + * Replaces the messages area with a list of saved conversations. + */ + async _showHistoryPanel() { + const container = document.getElementById('assistant-messages'); + if (!container) return; + + // Save snapshot of current messages so we can restore them on "Back" + const prevHTML = container.innerHTML; + const prevHistory = this._conversationHistory.slice(); + const prevConvId = this._currentConvId; + + const render = async () => { + container.innerHTML = ''; + + // Header row + const header = document.createElement('div'); + header.style.cssText = 'display:flex; align-items:center; gap:8px; margin-bottom:12px;'; + + const backBtn = document.createElement('button'); + backBtn.title = 'Back to chat'; + backBtn.style.cssText = 'background:transparent; border:none; color:#8b949e; cursor:pointer; padding:4px 6px; border-radius:4px; display:flex; align-items:center;'; + backBtn.innerHTML = ''; + backBtn.onmouseover = () => { backBtn.style.color = '#c9d1d9'; }; + backBtn.onmouseout = () => { backBtn.style.color = '#8b949e'; }; + backBtn.onclick = () => { + container.innerHTML = prevHTML; + this._conversationHistory = prevHistory; + this._currentConvId = prevConvId; + }; + + const title = document.createElement('span'); + title.style.cssText = 'font-size:13px; color:#c9d1d9; font-weight:500;'; + title.textContent = 'Past conversations'; + + header.appendChild(backBtn); + header.appendChild(title); + container.appendChild(header); + + // Fetch list + let conversations = []; + try { + const res = await window.api.invoke('assistant-conversations-list'); + if (res && res.success) conversations = res.conversations || []; + } catch (_) {} + + if (conversations.length === 0) { + const empty = document.createElement('div'); + empty.style.cssText = 'color:#555; font-size:12px; text-align:center; margin-top:32px;'; + empty.textContent = 'No saved conversations yet.'; + container.appendChild(empty); + return; + } + + for (const conv of conversations) { + const item = document.createElement('div'); + item.style.cssText = 'display:flex; align-items:center; gap:8px; padding:8px 10px; border-radius:6px; cursor:pointer; border:1px solid #1e2530; margin-bottom:6px; background:#0d1117; transition:background 0.15s;'; + item.onmouseover = () => { item.style.background = '#161b22'; }; + item.onmouseout = () => { item.style.background = '#0d1117'; }; + + const info = document.createElement('div'); + info.style.cssText = 'flex:1; min-width:0;'; + + const convTitle = document.createElement('div'); + convTitle.style.cssText = 'font-size:12px; color:#c9d1d9; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;'; + convTitle.textContent = conv.title || 'Untitled'; + + const date = document.createElement('div'); + date.style.cssText = 'font-size:10px; color:#555; margin-top:2px;'; + const d = new Date(conv.updatedAt || conv.createdAt); + date.textContent = isNaN(d) ? '' : d.toLocaleString(); + + info.appendChild(convTitle); + info.appendChild(date); + + const delBtn = document.createElement('button'); + delBtn.title = 'Delete'; + delBtn.style.cssText = 'background:transparent; border:none; color:#555; cursor:pointer; padding:3px 5px; border-radius:4px; flex-shrink:0; display:flex; align-items:center;'; + delBtn.innerHTML = ''; + delBtn.onmouseover = () => { delBtn.style.color = '#e85555'; }; + delBtn.onmouseout = () => { delBtn.style.color = '#555'; }; + delBtn.onclick = async (e) => { + e.stopPropagation(); + try { await window.api.invoke('assistant-conversations-delete', conv.id); } catch (_) {} + await render(); + }; + + item.appendChild(info); + item.appendChild(delBtn); + + // Click item body to load the conversation + item.onclick = async (e) => { + if (e.target === delBtn || delBtn.contains(e.target)) return; + try { + const res = await window.api.invoke('assistant-conversations-load', conv.id); + if (res && res.success && res.conversation) { + const loaded = res.conversation; + this._conversationHistory = loaded.history || []; + this._currentConvId = loaded.id; + // Rebuild message UI from history + container.innerHTML = ''; + for (const msg of this._conversationHistory) { + const wrap = document.createElement('div'); + wrap.style.marginBottom = '12px'; + const bubble = document.createElement('div'); + bubble.style.padding = '10px 12px'; + bubble.style.borderRadius = '8px'; + bubble.style.maxWidth = '90%'; + bubble.style.lineHeight = '1.4'; + if (msg.role === 'user') { + bubble.style.cssText += 'background:#0b5fff; color:#fff; margin-left:auto; white-space:pre-wrap;'; + bubble.textContent = msg.content; + } else { + bubble.style.cssText += 'background:#111319; color:#e6edf3; margin-right:auto;'; + bubble.innerHTML = (typeof window.renderMarkdown === 'function') + ? window.renderMarkdown(msg.content) + : msg.content.replace(/ { - this.ui.triggerAutoSave(); - }); - } else { - // If Monaco isn't ready yet, wait for it - window.addEventListener('monaco-loaded', () => { - setTimeout(() => { - if (this.ui.editorManager && this.ui.editorManager.editor) { - this.ui.editorManager.editor.onDidChangeModelContent(() => { - this.ui.triggerAutoSave(); - }); - } - }, 500); - }); - } - } - -/** - * Trigger auto save with debounce - */ - triggerAutoSave() { - if (!this.ui.autoSaveEnabled) return; - - // Clear existing timer - if (this.ui.autoSaveTimer) { - clearTimeout(this.ui.autoSaveTimer); - } - - // Set new timer - this.ui.autoSaveTimer = setTimeout(async () => { - const currentTab = this.ui.tabManager.getActiveTab(); - const isDirty = !!(currentTab && (currentTab.modified || currentTab.isDirty)); - if (currentTab && currentTab.filePath && isDirty) { - try { - await this.ui.fileOpsManager.saveFile({ silent: true, reason: 'autosave' }); - console.log('Auto-saved:', currentTab.fileName); - } catch (error) { - console.error('Auto-save failed:', error); - } - } - }, this.ui.autoSaveDelay); - } - -/** - * Setup custom title bar controls for frameless window. - * - * This method handles the custom window controls (minimize, maximize, close) - * and window state management for the frameless window. - * - * @memberof UIController - * @private - */ - setupTitleBarControls() { - // Setup IPC-based window controls (via secure preload bridge) - const minimizeBtn = document.getElementById('minimize-btn'); - const maximizeBtn = document.getElementById('maximize-btn'); - const closeBtn = document.getElementById('close-btn'); - - if (minimizeBtn) { - minimizeBtn.addEventListener('click', () => { - window.api.send('window-minimize'); - }); - } - - if (maximizeBtn) { - maximizeBtn.addEventListener('click', () => { - window.api.send('window-maximize-toggle'); - }); - } - - if (closeBtn) { - closeBtn.addEventListener('click', () => { - window.api.send('window-close'); - }); - } - - // Listen for window state changes - window.api.on('window-maximized', (isMaximized) => { - document.body.classList.toggle('window-maximized', isMaximized); - }); - } - -} - -if (typeof window !== 'undefined') window.EditorPanel = EditorPanel; -if (typeof module !== 'undefined') module.exports = EditorPanel; +class EditorPanel { + constructor(ui) { + this.ui = ui; + } + +/** + * Toggle auto save feature + */ + toggleAutoSave() { + this.ui.autoSaveEnabled = !this.ui.autoSaveEnabled; + this.ui.updateAutoSaveStatus(); + this.ui.saveAutoSaveState(); + + const statusMsg = this.ui.autoSaveEnabled ? 'Auto Save enabled' : 'Auto Save disabled'; + this.ui.notificationManager.showSuccess(statusMsg); + } + +/** + * Update auto save status in the status bar + */ + updateAutoSaveStatus() { + const statusElement = document.getElementById('autoSaveStatus'); + if (statusElement) { + if (this.ui.autoSaveEnabled) { + statusElement.style.display = 'inline-block'; + statusElement.textContent = 'Auto Save: ON'; + statusElement.style.background = 'rgba(31, 111, 235, 0.15)'; + statusElement.style.color = '#58a6ff'; + } else { + statusElement.style.display = 'none'; + } + } + } + +/** + * Save auto save state to localStorage + */ + saveAutoSaveState() { + try { + localStorage.setItem('autoSaveEnabled', JSON.stringify(this.ui.autoSaveEnabled)); + } catch (error) { + console.error('Failed to save auto save state:', error); + } + } + +/** + * Load auto save state from localStorage + */ + loadAutoSaveState() { + try { + const saved = localStorage.getItem('autoSaveEnabled'); + if (saved !== null) { + this.ui.autoSaveEnabled = JSON.parse(saved); + this.ui.updateAutoSaveStatus(); + } + } catch (error) { + console.error('Failed to load auto save state:', error); + } + } + +/** + * Setup auto save listener for editor content changes + */ + setupAutoSaveListener() { + // Listen to editor content changes via Monaco + if (this.ui.editorManager && this.ui.editorManager.editor) { + this.ui.editorManager.editor.onDidChangeModelContent(() => { + this.ui.triggerAutoSave(); + }); + } else { + // If Monaco isn't ready yet, wait for it + window.addEventListener('monaco-loaded', () => { + setTimeout(() => { + if (this.ui.editorManager && this.ui.editorManager.editor) { + this.ui.editorManager.editor.onDidChangeModelContent(() => { + this.ui.triggerAutoSave(); + }); + } + }, 500); + }); + } + } + +/** + * Trigger auto save with debounce + */ + triggerAutoSave() { + if (!this.ui.autoSaveEnabled) return; + + // Clear existing timer + if (this.ui.autoSaveTimer) { + clearTimeout(this.ui.autoSaveTimer); + } + + // Set new timer + this.ui.autoSaveTimer = setTimeout(async () => { + const currentTab = this.ui.tabManager.getActiveTab(); + const isDirty = !!(currentTab && (currentTab.modified || currentTab.isDirty)); + if (currentTab && currentTab.filePath && isDirty) { + try { + await this.ui.fileOpsManager.saveFile({ silent: true, reason: 'autosave' }); + console.log('Auto-saved:', currentTab.fileName); + } catch (error) { + console.error('Auto-save failed:', error); + } + } + }, this.ui.autoSaveDelay); + } + +/** + * Setup custom title bar controls for frameless window. + * + * This method handles the custom window controls (minimize, maximize, close) + * and window state management for the frameless window. + * + * @memberof UIController + * @private + */ + setupTitleBarControls() { + // Setup IPC-based window controls (via secure preload bridge) + const minimizeBtn = document.getElementById('minimize-btn'); + const maximizeBtn = document.getElementById('maximize-btn'); + const closeBtn = document.getElementById('close-btn'); + + if (minimizeBtn) { + minimizeBtn.addEventListener('click', () => { + window.api.send('window-minimize'); + }); + } + + if (maximizeBtn) { + maximizeBtn.addEventListener('click', () => { + window.api.send('window-maximize-toggle'); + }); + } + + if (closeBtn) { + closeBtn.addEventListener('click', () => { + window.api.send('window-close'); + }); + } + + // Listen for window state changes + window.api.on('window-maximized', (isMaximized) => { + document.body.classList.toggle('window-maximized', isMaximized); + }); + } + +} + +if (typeof window !== 'undefined') window.EditorPanel = EditorPanel; +if (typeof module !== 'undefined') module.exports = EditorPanel; diff --git a/src/renderer/components/FileTree.js b/src/renderer/components/FileTree.js index 8a25e29..b965c79 100644 --- a/src/renderer/components/FileTree.js +++ b/src/renderer/components/FileTree.js @@ -1,355 +1,355 @@ -class FileTree { - constructor(ui) { - this.ui = ui; - } - -/** - * Setup file tree watcher to listen for automatic updates. - * Debounces refreshes to avoid UI freezes on large workspaces. - */ - setupFileTreeWatcher() { - if (this.ui._fileTreeWatcherInitialized) return; - this.ui._fileTreeWatcherInitialized = true; - - let refreshTimer = null; - window.api.on('workspace-changed', (data) => { - if (!data || !data.success) { - if (data && data.error) { - console.error('Error in workspace change notification:', data.error); - } - return; - } - - const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); - if (!workspacePath) return; - if (data.folderPath && data.folderPath !== workspacePath) return; - - clearTimeout(refreshTimer); - refreshTimer = setTimeout(() => { - this.ui.refreshFileTree(true); - }, 1200); - }); - - // Show a discrete loading indicator for slow workspace operations - this.ui._workspaceLoadingTimer = null; - this.ui._workspaceLoadingNotification = null; - this.ui._workspaceLoadingRequestId = null; - - window.api.on('workspace-loading', (data) => { - if (!data || !data.status) return; - - // Only react for the active workspace. - const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); - if (workspacePath && data.folderPath && data.folderPath !== workspacePath && data.operation !== 'open') { - return; - } - - if (data.status === 'start') { - this.ui._workspaceLoadingRequestId = data.requestId || null; - clearTimeout(this.ui._workspaceLoadingTimer); - - this.ui._workspaceLoadingTimer = setTimeout(() => { - if (this.ui._workspaceLoadingNotification) return; - const folderName = (data.folderPath || '').split(/[/\\]/).pop() || 'workspace'; - const label = data.operation === 'refresh' - ? `Refreshing "${folderName}"...` - : `Loading "${folderName}"...`; - this.ui._workspaceLoadingNotification = this.ui.notificationManager.showLoading(label); - }, 450); - - return; - } - - if (data.status === 'end') { - if (this.ui._workspaceLoadingRequestId && data.requestId && data.requestId !== this.ui._workspaceLoadingRequestId) { - return; - } - - clearTimeout(this.ui._workspaceLoadingTimer); - this.ui._workspaceLoadingTimer = null; - - if (this.ui._workspaceLoadingNotification) { - this.ui._workspaceLoadingNotification.dismiss(); - this.ui._workspaceLoadingNotification = null; - } - - this.ui._workspaceLoadingRequestId = null; - } - }); - } - -/** - * Setup a custom context menu for the explorer file tree. - * - * Requirements: - * - Right click on empty area: New File.. / New Folder.. / Close Folder - * - Right click on folder: New File.. / New Folder.. / Rename / Delete - * - Right click on file: Rename / Delete - * - Right click on workspace name: Close Folder - */ - setupFileTreeContextMenu() { - const workspaceFolderEl = document.getElementById('workspace-folder'); - const fileTreeElement = document.getElementById('file-tree'); - const workspaceNameEl = document.getElementById('workspace-name'); - const targetEl = workspaceFolderEl || fileTreeElement; - if (!targetEl) return; - - // Context menu for workspace name - if (workspaceNameEl) { - workspaceNameEl.addEventListener('contextmenu', (e) => { - e.preventDefault(); - e.stopPropagation(); - - const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); - if (!workspacePath) return; - - this.ui.showFileTreeContextMenu(e.clientX, e.clientY, [{ - label: 'Close Folder', - action: () => this.ui.fileOpsManager.closeWorkspace() - }]); - }); - } - - targetEl.addEventListener('contextmenu', (e) => { - e.preventDefault(); - e.stopPropagation(); - - // No workspace => no file operations - const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); - if (!workspacePath) { - this.ui.notificationManager.showWarning('Open a workspace to use file operations'); - return; - } - - const itemEl = e.target.closest('.file-tree-item'); - const itemType = itemEl ? itemEl.getAttribute('data-type') : null; - const itemPath = itemEl ? (itemEl.getAttribute('data-path') || itemEl.getAttribute('data-file-path')) : null; - const itemName = itemEl ? (itemEl.getAttribute('data-name') || '') : ''; - - const menuItems = []; - - // Right click on empty area - if (!itemEl) { - menuItems.push({ - label: 'New file..', - action: () => this.ui.createFileInDirectory(workspacePath) - }); - menuItems.push({ - label: 'New folder..', - action: () => this.ui.createFolderInDirectory(workspacePath) - }); - menuItems.push({ - label: 'Close Folder', - action: () => this.ui.fileOpsManager.closeWorkspace() - }); - } else if (itemType === 'directory') { - // Folder menu - menuItems.push({ - label: 'New file..', - action: () => this.ui.createFileInDirectory(itemPath) - }); - menuItems.push({ - label: 'New folder..', - action: () => this.ui.createFolderInDirectory(itemPath) - }); - menuItems.push({ - label: 'Rename', - action: () => this.ui.renamePath(itemPath, itemName) - }); - menuItems.push({ - label: 'Delete', - action: () => this.ui.deletePath(itemPath, itemType) - }); - } else { - // File menu - menuItems.push({ - label: 'Rename', - action: () => this.ui.renamePath(itemPath, itemName) - }); - menuItems.push({ - label: 'Delete', - action: () => this.ui.deletePath(itemPath, itemType) - }); - } - - this.ui.showFileTreeContextMenu(e.clientX, e.clientY, menuItems); - }); - } - - showFileTreeContextMenu(x, y, items) { - this.ui.hideFileTreeContextMenu(); - - const menu = document.createElement('div'); - menu.className = 'context-menu'; - - items.forEach(({ label, action }) => { - const item = document.createElement('div'); - item.className = 'context-menu-item'; - item.textContent = label; - item.addEventListener('click', (e) => { - e.stopPropagation(); - this.ui.hideFileTreeContextMenu(); - action(); - }); - menu.appendChild(item); - }); - - document.body.appendChild(menu); - - // Position within viewport - const rect = menu.getBoundingClientRect(); - const maxX = window.innerWidth - rect.width - 8; - const maxY = window.innerHeight - rect.height - 8; - const posX = Math.max(8, Math.min(x, maxX)); - const posY = Math.max(8, Math.min(y, maxY)); - menu.style.left = posX + 'px'; - menu.style.top = posY + 'px'; - - this.ui.fileTreeContextMenu = menu; - } - - hideFileTreeContextMenu() { - if (this.ui.fileTreeContextMenu) { - this.ui.fileTreeContextMenu.remove(); - this.ui.fileTreeContextMenu = null; - } - } - -/** - * Show a simple in-app input dialog (avoids relying on window.prompt). - * @param {string} title - * @param {string} [placeholder] - * @param {string} [defaultValue] - * @returns {Promise} - */ - promptForText(title, placeholder = '', defaultValue = '') { - return new Promise((resolve) => { - const overlay = document.createElement('div'); - overlay.className = 'input-dialog-overlay'; - - const dialog = document.createElement('div'); - dialog.className = 'input-dialog'; - - const header = document.createElement('div'); - header.className = 'input-dialog-title'; - header.textContent = title; - - const input = document.createElement('input'); - input.className = 'input-dialog-input'; - input.type = 'text'; - input.placeholder = placeholder; - input.value = defaultValue || ''; - - const actions = document.createElement('div'); - actions.className = 'input-dialog-actions'; - - const cancelBtn = document.createElement('button'); - cancelBtn.className = 'input-dialog-button'; - cancelBtn.textContent = 'Cancel'; - - const okBtn = document.createElement('button'); - okBtn.className = 'input-dialog-button primary'; - okBtn.textContent = 'OK'; - - actions.appendChild(cancelBtn); - actions.appendChild(okBtn); - - dialog.appendChild(header); - dialog.appendChild(input); - dialog.appendChild(actions); - overlay.appendChild(dialog); - document.body.appendChild(overlay); - - const cleanup = (value) => { - overlay.remove(); - resolve(value); - }; - - cancelBtn.addEventListener('click', (e) => { - e.stopPropagation(); - cleanup(null); - }); - - okBtn.addEventListener('click', (e) => { - e.stopPropagation(); - cleanup(input.value); - }); - - overlay.addEventListener('click', (e) => { - if (e.target === overlay) cleanup(null); - }); - - input.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - cleanup(input.value); - } else if (e.key === 'Escape') { - e.preventDefault(); - cleanup(null); - } - }); - - // Focus synchronously (still inside the click handler user gesture) - // so typing works reliably even if the editor has global key handlers. - try { - input.disabled = false; - input.readOnly = false; - input.focus(); - input.select(); - } catch (_) { - // ignore - } - - // Fallback focus in next frame - requestAnimationFrame(() => { - try { - input.focus(); - } catch (_) { - // ignore - } - }); - }); - } - -/** - * Refreshes the file tree in the explorer view. - * - * This method manually triggers a refresh of the file tree to show any - * new files or folders that may have been added to the workspace. It - * communicates with the main process to get an updated file tree structure. - * - * @async - * @memberof UIController - * @throws {Error} When file tree refresh fails - * - * @example - * // Refresh is typically triggered by the refresh button - * await uiController.refreshFileTree(); - */ - async refreshFileTree(silent = false) { - // Only refresh if workspace is open - const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); - if (!workspacePath) { - this.ui.notificationManager.showWarning('No workspace open to refresh'); - return; - } - // Request updated file tree from main process - try { - const result = await window.api.invoke('get-file-tree', workspacePath); - if (result.success) { - const folderName = workspacePath.split(/[/\\]/).pop(); - this.ui.fileOpsManager.updateWorkspaceUI(folderName, result.fileTree); - if (!silent) { - this.ui.notificationManager.showSuccess('File tree refreshed'); - } - } else { - this.ui.notificationManager.showError('Failed to refresh file tree: ' + (result.error || 'Unknown error')); - } - } catch (error) { - this.ui.notificationManager.showError('Error refreshing file tree: ' + error.message); - } - } - -} - -window.FileTree = FileTree; +class FileTree { + constructor(ui) { + this.ui = ui; + } + +/** + * Setup file tree watcher to listen for automatic updates. + * Debounces refreshes to avoid UI freezes on large workspaces. + */ + setupFileTreeWatcher() { + if (this.ui._fileTreeWatcherInitialized) return; + this.ui._fileTreeWatcherInitialized = true; + + let refreshTimer = null; + window.api.on('workspace-changed', (data) => { + if (!data || !data.success) { + if (data && data.error) { + console.error('Error in workspace change notification:', data.error); + } + return; + } + + const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); + if (!workspacePath) return; + if (data.folderPath && data.folderPath !== workspacePath) return; + + clearTimeout(refreshTimer); + refreshTimer = setTimeout(() => { + this.ui.refreshFileTree(true); + }, 1200); + }); + + // Show a discrete loading indicator for slow workspace operations + this.ui._workspaceLoadingTimer = null; + this.ui._workspaceLoadingNotification = null; + this.ui._workspaceLoadingRequestId = null; + + window.api.on('workspace-loading', (data) => { + if (!data || !data.status) return; + + // Only react for the active workspace. + const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); + if (workspacePath && data.folderPath && data.folderPath !== workspacePath && data.operation !== 'open') { + return; + } + + if (data.status === 'start') { + this.ui._workspaceLoadingRequestId = data.requestId || null; + clearTimeout(this.ui._workspaceLoadingTimer); + + this.ui._workspaceLoadingTimer = setTimeout(() => { + if (this.ui._workspaceLoadingNotification) return; + const folderName = (data.folderPath || '').split(/[/\\]/).pop() || 'workspace'; + const label = data.operation === 'refresh' + ? `Refreshing "${folderName}"...` + : `Loading "${folderName}"...`; + this.ui._workspaceLoadingNotification = this.ui.notificationManager.showLoading(label); + }, 450); + + return; + } + + if (data.status === 'end') { + if (this.ui._workspaceLoadingRequestId && data.requestId && data.requestId !== this.ui._workspaceLoadingRequestId) { + return; + } + + clearTimeout(this.ui._workspaceLoadingTimer); + this.ui._workspaceLoadingTimer = null; + + if (this.ui._workspaceLoadingNotification) { + this.ui._workspaceLoadingNotification.dismiss(); + this.ui._workspaceLoadingNotification = null; + } + + this.ui._workspaceLoadingRequestId = null; + } + }); + } + +/** + * Setup a custom context menu for the explorer file tree. + * + * Requirements: + * - Right click on empty area: New File.. / New Folder.. / Close Folder + * - Right click on folder: New File.. / New Folder.. / Rename / Delete + * - Right click on file: Rename / Delete + * - Right click on workspace name: Close Folder + */ + setupFileTreeContextMenu() { + const workspaceFolderEl = document.getElementById('workspace-folder'); + const fileTreeElement = document.getElementById('file-tree'); + const workspaceNameEl = document.getElementById('workspace-name'); + const targetEl = workspaceFolderEl || fileTreeElement; + if (!targetEl) return; + + // Context menu for workspace name + if (workspaceNameEl) { + workspaceNameEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + + const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); + if (!workspacePath) return; + + this.ui.showFileTreeContextMenu(e.clientX, e.clientY, [{ + label: 'Close Folder', + action: () => this.ui.fileOpsManager.closeWorkspace() + }]); + }); + } + + targetEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + e.stopPropagation(); + + // No workspace => no file operations + const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); + if (!workspacePath) { + this.ui.notificationManager.showWarning('Open a workspace to use file operations'); + return; + } + + const itemEl = e.target.closest('.file-tree-item'); + const itemType = itemEl ? itemEl.getAttribute('data-type') : null; + const itemPath = itemEl ? (itemEl.getAttribute('data-path') || itemEl.getAttribute('data-file-path')) : null; + const itemName = itemEl ? (itemEl.getAttribute('data-name') || '') : ''; + + const menuItems = []; + + // Right click on empty area + if (!itemEl) { + menuItems.push({ + label: 'New file..', + action: () => this.ui.createFileInDirectory(workspacePath) + }); + menuItems.push({ + label: 'New folder..', + action: () => this.ui.createFolderInDirectory(workspacePath) + }); + menuItems.push({ + label: 'Close Folder', + action: () => this.ui.fileOpsManager.closeWorkspace() + }); + } else if (itemType === 'directory') { + // Folder menu + menuItems.push({ + label: 'New file..', + action: () => this.ui.createFileInDirectory(itemPath) + }); + menuItems.push({ + label: 'New folder..', + action: () => this.ui.createFolderInDirectory(itemPath) + }); + menuItems.push({ + label: 'Rename', + action: () => this.ui.renamePath(itemPath, itemName) + }); + menuItems.push({ + label: 'Delete', + action: () => this.ui.deletePath(itemPath, itemType) + }); + } else { + // File menu + menuItems.push({ + label: 'Rename', + action: () => this.ui.renamePath(itemPath, itemName) + }); + menuItems.push({ + label: 'Delete', + action: () => this.ui.deletePath(itemPath, itemType) + }); + } + + this.ui.showFileTreeContextMenu(e.clientX, e.clientY, menuItems); + }); + } + + showFileTreeContextMenu(x, y, items) { + this.ui.hideFileTreeContextMenu(); + + const menu = document.createElement('div'); + menu.className = 'context-menu'; + + items.forEach(({ label, action }) => { + const item = document.createElement('div'); + item.className = 'context-menu-item'; + item.textContent = label; + item.addEventListener('click', (e) => { + e.stopPropagation(); + this.ui.hideFileTreeContextMenu(); + action(); + }); + menu.appendChild(item); + }); + + document.body.appendChild(menu); + + // Position within viewport + const rect = menu.getBoundingClientRect(); + const maxX = window.innerWidth - rect.width - 8; + const maxY = window.innerHeight - rect.height - 8; + const posX = Math.max(8, Math.min(x, maxX)); + const posY = Math.max(8, Math.min(y, maxY)); + menu.style.left = posX + 'px'; + menu.style.top = posY + 'px'; + + this.ui.fileTreeContextMenu = menu; + } + + hideFileTreeContextMenu() { + if (this.ui.fileTreeContextMenu) { + this.ui.fileTreeContextMenu.remove(); + this.ui.fileTreeContextMenu = null; + } + } + +/** + * Show a simple in-app input dialog (avoids relying on window.prompt). + * @param {string} title + * @param {string} [placeholder] + * @param {string} [defaultValue] + * @returns {Promise} + */ + promptForText(title, placeholder = '', defaultValue = '') { + return new Promise((resolve) => { + const overlay = document.createElement('div'); + overlay.className = 'input-dialog-overlay'; + + const dialog = document.createElement('div'); + dialog.className = 'input-dialog'; + + const header = document.createElement('div'); + header.className = 'input-dialog-title'; + header.textContent = title; + + const input = document.createElement('input'); + input.className = 'input-dialog-input'; + input.type = 'text'; + input.placeholder = placeholder; + input.value = defaultValue || ''; + + const actions = document.createElement('div'); + actions.className = 'input-dialog-actions'; + + const cancelBtn = document.createElement('button'); + cancelBtn.className = 'input-dialog-button'; + cancelBtn.textContent = 'Cancel'; + + const okBtn = document.createElement('button'); + okBtn.className = 'input-dialog-button primary'; + okBtn.textContent = 'OK'; + + actions.appendChild(cancelBtn); + actions.appendChild(okBtn); + + dialog.appendChild(header); + dialog.appendChild(input); + dialog.appendChild(actions); + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const cleanup = (value) => { + overlay.remove(); + resolve(value); + }; + + cancelBtn.addEventListener('click', (e) => { + e.stopPropagation(); + cleanup(null); + }); + + okBtn.addEventListener('click', (e) => { + e.stopPropagation(); + cleanup(input.value); + }); + + overlay.addEventListener('click', (e) => { + if (e.target === overlay) cleanup(null); + }); + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + cleanup(input.value); + } else if (e.key === 'Escape') { + e.preventDefault(); + cleanup(null); + } + }); + + // Focus synchronously (still inside the click handler user gesture) + // so typing works reliably even if the editor has global key handlers. + try { + input.disabled = false; + input.readOnly = false; + input.focus(); + input.select(); + } catch (_) { + // ignore + } + + // Fallback focus in next frame + requestAnimationFrame(() => { + try { + input.focus(); + } catch (_) { + // ignore + } + }); + }); + } + +/** + * Refreshes the file tree in the explorer view. + * + * This method manually triggers a refresh of the file tree to show any + * new files or folders that may have been added to the workspace. It + * communicates with the main process to get an updated file tree structure. + * + * @async + * @memberof UIController + * @throws {Error} When file tree refresh fails + * + * @example + * // Refresh is typically triggered by the refresh button + * await uiController.refreshFileTree(); + */ + async refreshFileTree(silent = false) { + // Only refresh if workspace is open + const workspacePath = this.ui.fileOpsManager.getCurrentWorkspacePath(); + if (!workspacePath) { + this.ui.notificationManager.showWarning('No workspace open to refresh'); + return; + } + // Request updated file tree from main process + try { + const result = await window.api.invoke('get-file-tree', workspacePath); + if (result.success) { + const folderName = workspacePath.split(/[/\\]/).pop(); + this.ui.fileOpsManager.updateWorkspaceUI(folderName, result.fileTree); + if (!silent) { + this.ui.notificationManager.showSuccess('File tree refreshed'); + } + } else { + this.ui.notificationManager.showError('Failed to refresh file tree: ' + (result.error || 'Unknown error')); + } + } catch (error) { + this.ui.notificationManager.showError('Error refreshing file tree: ' + error.message); + } + } + +} + +window.FileTree = FileTree; diff --git a/src/renderer/managers/EditorManager.js b/src/renderer/managers/EditorManager.js index 6f9de28..02e2f4f 100644 --- a/src/renderer/managers/EditorManager.js +++ b/src/renderer/managers/EditorManager.js @@ -1,555 +1,555 @@ -/** - * Editor Manager - Handles editor functionality like gutter, status bar, formatting - */ - -// Import syntax highlighter - use window globals when available (contextIsolation mode) -const _syntaxHL = (typeof window !== 'undefined' && window.syntaxHighlighter) || (typeof require === 'function' ? require('../utils/syntaxHighlighter') : {}); -const highlightCppSyntax = _syntaxHL.highlightCppSyntax; -const applySyntaxHighlight = _syntaxHL.applySyntaxHighlight; -const shouldHighlight = _syntaxHL.shouldHighlight; -const detectFileType = (typeof window !== 'undefined' && window.detectFileType) || (typeof require === 'function' ? require('../utils/fileTypeUtils').detectFileType : null); - -class EditorManager { - constructor() { - this.editor = document.getElementById('editor'); - this.gutter = document.getElementById('gutter'); - this.lineCounter = document.getElementById('lineCounter'); - this.currentFileType = 'Plain Text'; - this.highlightEnabled = false; - this.updateTimer = null; - this.isUpdating = false; - this.init(); - } - - init() { - if (!this.editor || !this.gutter || !this.lineCounter) return; - - // Maintain visual selection when editor loses focus - this.setupPersistentSelection(); - - // Make TAB key insert tab character in editor - this.editor.addEventListener('keydown', (e) => { - if (e.key === 'Tab') { - e.preventDefault(); - document.execCommand('insertText', false, '\t'); - } else if (e.key === 'Enter') { - e.preventDefault(); - document.execCommand('insertLineBreak'); - } - }); - - // Enhanced input handler with debounced syntax highlighting - this.editor.addEventListener('input', (e) => { - if (!this.isUpdating) { - this.updateGutter(); - this.updateStatusBar(); - this.debouncedHighlightUpdate(); - } - }); - - this.editor.addEventListener('scroll', () => { - this.syncScroll(); - }); - this.editor.addEventListener('click', () => { - this.updateStatusBar(); - this.updateGutter(); - }); - this.editor.addEventListener('keyup', () => { - this.updateStatusBar(); - this.updateGutter(); - }); - - // Initialize - this.updateGutter(); - this.updateStatusBar(); - } - - /** - * Save current cursor position - */ - saveCursorPosition() { - const selection = window.getSelection(); - if (!selection.rangeCount) return null; - - const range = selection.getRangeAt(0); - const preCaretRange = range.cloneRange(); - preCaretRange.selectNodeContents(this.editor); - preCaretRange.setEnd(range.endContainer, range.endOffset); - - return preCaretRange.toString().length; - } - - /** - * Restore cursor position - */ - restoreCursorPosition(offset) { - if (offset === null || offset === undefined) return; - - const selection = window.getSelection(); - const range = document.createRange(); - - let currentOffset = 0; - const walker = document.createTreeWalker( - this.editor, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let node; - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - if (currentOffset + nodeLength >= offset) { - const targetOffset = offset - currentOffset; - range.setStart(node, Math.min(targetOffset, node.textContent.length)); - range.collapse(true); - selection.removeAllRanges(); - selection.addRange(range); - return; - } - currentOffset += nodeLength; - } - - // If we didn't find the position, place cursor at end - if (this.editor.lastChild) { - range.selectNodeContents(this.editor); - range.collapse(false); - selection.removeAllRanges(); - selection.addRange(range); - } - } - - /** - * Update syntax highlighting with debouncing - */ - debouncedHighlightUpdate() { - if (this.updateTimer) { - clearTimeout(this.updateTimer); - } - this.updateTimer = setTimeout(() => { - this.updateSyntaxHighlight(); - }, 150); - } - - /** - * Update syntax highlighting directly in editor - */ - updateSyntaxHighlight() { - if (!this.editor || this.isUpdating) return; - - this.isUpdating = true; - const savedOffset = this.saveCursorPosition(); - - // Get plain text content - const text = this.getContent(); - - // Apply syntax highlighting if enabled - if (this.highlightEnabled && shouldHighlight(this.currentFileType)) { - const highlighted = applySyntaxHighlight(text, this.currentFileType); - this.editor.innerHTML = highlighted; - } else { - this.editor.textContent = text; - } - - // Restore cursor position - this.restoreCursorPosition(savedOffset); - this.isUpdating = false; - } - - /** - * Sync gutter scroll with editor - */ - syncScroll() { - if (this.gutter && this.editor) { - this.gutter.scrollTop = this.editor.scrollTop; - } - } - - /** - * Enhanced gutter line numbers with proper formatting (like VS Code) - */ - updateGutter() { - if (!this.editor || !this.gutter) return; - - const text = this.getContent(); - // Count lines properly - if text ends with newline, don't count it as extra line - let lines = 1; - if (text) { - const splitLines = text.split('\n'); - lines = splitLines.length; - // If last element is empty string (trailing newline), don't count it - if (splitLines[splitLines.length - 1] === '') { - lines = Math.max(1, lines - 1); - } - } - - // Get current line number for highlighting - const currentLine = this.getCurrentLineNumber(); - - const maxDigits = Math.max(2, lines.toString().length); - - // Clear gutter and rebuild with styled line numbers - this.gutter.innerHTML = ''; - - // Create line numbers with current line highlighted - for (let i = 1; i <= lines; i++) { - const lineSpan = document.createElement('div'); - lineSpan.textContent = i.toString().padStart(maxDigits, ' '); - lineSpan.style.lineHeight = '20px'; - - if (i === currentLine) { - lineSpan.style.color = '#f0f6fc'; - lineSpan.style.fontWeight = 'bold'; - lineSpan.style.fontSize = '13px'; - } else { - lineSpan.style.color = '#6e7681'; - lineSpan.style.fontSize = '12px'; - } - - this.gutter.appendChild(lineSpan); - } - - // Update gutter width based on content - const charWidth = 8; // Approximate character width in monospace font - this.gutter.style.width = Math.max(60, (maxDigits + 2) * charWidth) + 'px'; - } - - /** - * Get current line number where cursor is - */ - getCurrentLineNumber() { - if (!this.editor) return 1; - - const selection = window.getSelection(); - if (!selection.rangeCount) return 1; - - const range = selection.getRangeAt(0); - const preCaretRange = range.cloneRange(); - preCaretRange.selectNodeContents(this.editor); - preCaretRange.setEnd(range.endContainer, range.endOffset); - - const textBeforeCursor = preCaretRange.toString(); - const lines = textBeforeCursor.split('\n'); - return lines.length; - } - - /** - * Enhanced status bar with cursor position tracking - */ - updateStatusBar() { - if (!this.editor || !this.lineCounter) return; - - const selection = window.getSelection(); - if (!selection.rangeCount) return; - - const range = selection.getRangeAt(0); - const preCaretRange = range.cloneRange(); - preCaretRange.selectNodeContents(this.editor); - preCaretRange.setEnd(range.endContainer, range.endOffset); - - const textBeforeCursor = preCaretRange.toString(); - const lines = textBeforeCursor.split('\n'); - const line = lines.length; - const col = lines[lines.length - 1].length + 1; - - if (range.toString().length > 0) { - const selectedText = range.toString(); - const selectedLines = selectedText.split('\n').length; - this.lineCounter.textContent = `Ln ${line}, Col ${col} (${selectedText.length} chars, ${selectedLines} lines selected)`; - } else { - this.lineCounter.textContent = `Ln ${line}, Col ${col}`; - } - } - - /** - * Jump to specific line in editor - * @param {number} lineNumber - Line number to jump to - */ - jumpToLine(lineNumber) { - console.log('Jumping to line:', lineNumber); - - if (!this.editor) { - console.error('Editor not found'); - return; - } - - const text = this.getContent(); - const lines = text.split('\n'); - console.log('Total lines in editor:', lines.length); - - if (lineNumber > lines.length) { - console.warn('Line number exceeds file length'); - return; - } - - // Calculate character position - let position = 0; - for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) { - position += lines[i].length + 1; // +1 for newline - } - - console.log('Calculated position:', position); - - // Create range and set cursor - const range = document.createRange(); - const sel = window.getSelection(); - - let currentPos = 0; - let targetNode = null; - let targetOffset = 0; - - // Find the text node and offset - const walker = document.createTreeWalker( - this.editor, - NodeFilter.SHOW_TEXT, - null, - false - ); - - let node; - while (node = walker.nextNode()) { - const nodeLength = node.textContent.length; - if (currentPos + nodeLength >= position) { - targetNode = node; - targetOffset = position - currentPos; - break; - } - currentPos += nodeLength; - } - - if (targetNode) { - range.setStart(targetNode, targetOffset); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - - // Scroll into view - const lineHeight = 20; - const targetScrollTop = Math.max(0, (lineNumber - 10) * lineHeight); - this.editor.scrollTop = targetScrollTop; - } - - this.editor.focus(); - this.updateStatusBar(); - - console.log('Successfully jumped to line:', lineNumber); - } - - /** - * Toggle word wrap - */ - toggleWordWrap() { - if (!this.editor) return; - - this.editor.style.whiteSpace = this.editor.style.whiteSpace === 'pre-wrap' ? 'pre' : 'pre-wrap'; - } - - /** - * Get editor content - * @returns {string} - Current editor content - */ - getContent() { - if (!this.editor) return ''; - // Replace
        tags with newlines and get text content - return this.editor.textContent || ''; - } - - /** - * Set editor content - * @param {string} content - Content to set - */ - setContent(content) { - if (this.editor) { - this.editor.textContent = content; - this.updateGutter(); - this.updateStatusBar(); - this.updateSyntaxHighlight(); - } - } - - /** - * Set file type (placeholder for future use) - * @param {string} filename - The filename to detect type from - */ - setFileType(filename) { - this.currentFileType = detectFileType(filename); - - // Enable syntax highlighting for C/C++ files - this.highlightEnabled = shouldHighlight(this.currentFileType); - - // Update highlighting immediately - this.updateSyntaxHighlight(); - } - - /** - * Setup persistent selection highlighting (like VS Code) - * Keeps selection visually highlighted even when editor loses focus - */ - setupPersistentSelection() { - // Create a canvas to measure text and draw highlights - const editorArea = document.getElementById('editor-area'); - if (!editorArea) return; - - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - ctx.font = '12px JetBrains Mono, monospace'; - - // Create highlight overlay that will contain absolute positioned divs - const highlightOverlay = document.createElement('div'); - highlightOverlay.id = 'selection-highlight-overlay'; - highlightOverlay.style.cssText = ` - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - pointer-events: none; - z-index: 0; - overflow: hidden; - `; - - // Insert overlay after gutter but before editor - const gutter = document.getElementById('gutter'); - if (gutter && gutter.nextSibling) { - editorArea.insertBefore(highlightOverlay, gutter.nextSibling); - } else { - editorArea.appendChild(highlightOverlay); - } - - // Make editor background transparent so overlay shows through - this.editor.style.position = 'relative'; - this.editor.style.zIndex = '1'; - this.editor.style.background = 'transparent'; - - let savedSelection = null; - - const updateHighlight = () => { - highlightOverlay.innerHTML = ''; - - if (!savedSelection || !this.editor.value) { - return; - } - - const { start, end } = savedSelection; - const text = this.editor.value; - const lines = text.split('\n'); - - // Get editor dimensions and position - const editorRect = this.editor.getBoundingClientRect(); - const editorAreaRect = editorArea.getBoundingClientRect(); - const gutterWidth = 61; // gutter width + border - const lineHeight = 20; - const paddingLeft = 20; - const paddingTop = 20; - - // Calculate which lines contain the selection - let charCount = 0; - let startLine = -1, startCol = -1; - let endLine = -1, endCol = -1; - - for (let i = 0; i < lines.length; i++) { - const lineLength = lines[i].length; - - if (startLine === -1 && charCount + lineLength >= start) { - startLine = i; - startCol = start - charCount; - } - - if (endLine === -1 && charCount + lineLength >= end) { - endLine = i; - endCol = end - charCount; - break; - } - - charCount += lineLength + 1; // +1 for newline - } - - if (startLine === -1 || endLine === -1) return; - - // Draw highlight rectangles for each line in selection - for (let line = startLine; line <= endLine; line++) { - const lineText = lines[line]; - let colStart = (line === startLine) ? startCol : 0; - let colEnd = (line === endLine) ? endCol : lineText.length; - - // Measure text width to get exact position - const beforeText = lineText.substring(0, colStart); - const selectedText = lineText.substring(colStart, colEnd); - - const startX = ctx.measureText(beforeText).width; - const width = ctx.measureText(selectedText).width; - - // Create highlight div for this line - const highlight = document.createElement('div'); - highlight.style.cssText = ` - position: absolute; - left: ${gutterWidth + paddingLeft + startX}px; - top: ${paddingTop + (line * lineHeight) - this.editor.scrollTop}px; - width: ${width + 2}px; - height: ${lineHeight}px; - background: #3a3d41; - pointer-events: none; - `; - - highlightOverlay.appendChild(highlight); - } - }; - - // Save selection when editor loses focus - this.editor.addEventListener('blur', () => { - const start = this.editor.selectionStart; - const end = this.editor.selectionEnd; - if (start !== end) { - savedSelection = { start, end }; - updateHighlight(); - } - }); - - // Clear highlight when editor gains focus - this.editor.addEventListener('focus', () => { - if (savedSelection) { - // Restore the selection - this.editor.setSelectionRange(savedSelection.start, savedSelection.end); - } - savedSelection = null; - highlightOverlay.innerHTML = ''; - }); - - // Update highlight position on scroll - this.editor.addEventListener('scroll', () => { - if (savedSelection) { - updateHighlight(); - } - }); - - // Clear on text change - this.editor.addEventListener('input', () => { - if (savedSelection) { - savedSelection = null; - highlightOverlay.innerHTML = ''; - } - }); - } - - escapeHtml(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } - - /** - * Focus the editor - */ - focus() { - if (this.editor) { - this.editor.focus(); - } - } -} - -if (typeof module !== 'undefined' && module.exports) { - module.exports = EditorManager; +/** + * Editor Manager - Handles editor functionality like gutter, status bar, formatting + */ + +// Import syntax highlighter - use window globals when available (contextIsolation mode) +const _syntaxHL = (typeof window !== 'undefined' && window.syntaxHighlighter) || (typeof require === 'function' ? require('../utils/syntaxHighlighter') : {}); +const highlightCppSyntax = _syntaxHL.highlightCppSyntax; +const applySyntaxHighlight = _syntaxHL.applySyntaxHighlight; +const shouldHighlight = _syntaxHL.shouldHighlight; +const detectFileType = (typeof window !== 'undefined' && window.detectFileType) || (typeof require === 'function' ? require('../utils/fileTypeUtils').detectFileType : null); + +class EditorManager { + constructor() { + this.editor = document.getElementById('editor'); + this.gutter = document.getElementById('gutter'); + this.lineCounter = document.getElementById('lineCounter'); + this.currentFileType = 'Plain Text'; + this.highlightEnabled = false; + this.updateTimer = null; + this.isUpdating = false; + this.init(); + } + + init() { + if (!this.editor || !this.gutter || !this.lineCounter) return; + + // Maintain visual selection when editor loses focus + this.setupPersistentSelection(); + + // Make TAB key insert tab character in editor + this.editor.addEventListener('keydown', (e) => { + if (e.key === 'Tab') { + e.preventDefault(); + document.execCommand('insertText', false, '\t'); + } else if (e.key === 'Enter') { + e.preventDefault(); + document.execCommand('insertLineBreak'); + } + }); + + // Enhanced input handler with debounced syntax highlighting + this.editor.addEventListener('input', (e) => { + if (!this.isUpdating) { + this.updateGutter(); + this.updateStatusBar(); + this.debouncedHighlightUpdate(); + } + }); + + this.editor.addEventListener('scroll', () => { + this.syncScroll(); + }); + this.editor.addEventListener('click', () => { + this.updateStatusBar(); + this.updateGutter(); + }); + this.editor.addEventListener('keyup', () => { + this.updateStatusBar(); + this.updateGutter(); + }); + + // Initialize + this.updateGutter(); + this.updateStatusBar(); + } + + /** + * Save current cursor position + */ + saveCursorPosition() { + const selection = window.getSelection(); + if (!selection.rangeCount) return null; + + const range = selection.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(this.editor); + preCaretRange.setEnd(range.endContainer, range.endOffset); + + return preCaretRange.toString().length; + } + + /** + * Restore cursor position + */ + restoreCursorPosition(offset) { + if (offset === null || offset === undefined) return; + + const selection = window.getSelection(); + const range = document.createRange(); + + let currentOffset = 0; + const walker = document.createTreeWalker( + this.editor, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let node; + while (node = walker.nextNode()) { + const nodeLength = node.textContent.length; + if (currentOffset + nodeLength >= offset) { + const targetOffset = offset - currentOffset; + range.setStart(node, Math.min(targetOffset, node.textContent.length)); + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); + return; + } + currentOffset += nodeLength; + } + + // If we didn't find the position, place cursor at end + if (this.editor.lastChild) { + range.selectNodeContents(this.editor); + range.collapse(false); + selection.removeAllRanges(); + selection.addRange(range); + } + } + + /** + * Update syntax highlighting with debouncing + */ + debouncedHighlightUpdate() { + if (this.updateTimer) { + clearTimeout(this.updateTimer); + } + this.updateTimer = setTimeout(() => { + this.updateSyntaxHighlight(); + }, 150); + } + + /** + * Update syntax highlighting directly in editor + */ + updateSyntaxHighlight() { + if (!this.editor || this.isUpdating) return; + + this.isUpdating = true; + const savedOffset = this.saveCursorPosition(); + + // Get plain text content + const text = this.getContent(); + + // Apply syntax highlighting if enabled + if (this.highlightEnabled && shouldHighlight(this.currentFileType)) { + const highlighted = applySyntaxHighlight(text, this.currentFileType); + this.editor.innerHTML = highlighted; + } else { + this.editor.textContent = text; + } + + // Restore cursor position + this.restoreCursorPosition(savedOffset); + this.isUpdating = false; + } + + /** + * Sync gutter scroll with editor + */ + syncScroll() { + if (this.gutter && this.editor) { + this.gutter.scrollTop = this.editor.scrollTop; + } + } + + /** + * Enhanced gutter line numbers with proper formatting (like VS Code) + */ + updateGutter() { + if (!this.editor || !this.gutter) return; + + const text = this.getContent(); + // Count lines properly - if text ends with newline, don't count it as extra line + let lines = 1; + if (text) { + const splitLines = text.split('\n'); + lines = splitLines.length; + // If last element is empty string (trailing newline), don't count it + if (splitLines[splitLines.length - 1] === '') { + lines = Math.max(1, lines - 1); + } + } + + // Get current line number for highlighting + const currentLine = this.getCurrentLineNumber(); + + const maxDigits = Math.max(2, lines.toString().length); + + // Clear gutter and rebuild with styled line numbers + this.gutter.innerHTML = ''; + + // Create line numbers with current line highlighted + for (let i = 1; i <= lines; i++) { + const lineSpan = document.createElement('div'); + lineSpan.textContent = i.toString().padStart(maxDigits, ' '); + lineSpan.style.lineHeight = '20px'; + + if (i === currentLine) { + lineSpan.style.color = '#f0f6fc'; + lineSpan.style.fontWeight = 'bold'; + lineSpan.style.fontSize = '13px'; + } else { + lineSpan.style.color = '#6e7681'; + lineSpan.style.fontSize = '12px'; + } + + this.gutter.appendChild(lineSpan); + } + + // Update gutter width based on content + const charWidth = 8; // Approximate character width in monospace font + this.gutter.style.width = Math.max(60, (maxDigits + 2) * charWidth) + 'px'; + } + + /** + * Get current line number where cursor is + */ + getCurrentLineNumber() { + if (!this.editor) return 1; + + const selection = window.getSelection(); + if (!selection.rangeCount) return 1; + + const range = selection.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(this.editor); + preCaretRange.setEnd(range.endContainer, range.endOffset); + + const textBeforeCursor = preCaretRange.toString(); + const lines = textBeforeCursor.split('\n'); + return lines.length; + } + + /** + * Enhanced status bar with cursor position tracking + */ + updateStatusBar() { + if (!this.editor || !this.lineCounter) return; + + const selection = window.getSelection(); + if (!selection.rangeCount) return; + + const range = selection.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(this.editor); + preCaretRange.setEnd(range.endContainer, range.endOffset); + + const textBeforeCursor = preCaretRange.toString(); + const lines = textBeforeCursor.split('\n'); + const line = lines.length; + const col = lines[lines.length - 1].length + 1; + + if (range.toString().length > 0) { + const selectedText = range.toString(); + const selectedLines = selectedText.split('\n').length; + this.lineCounter.textContent = `Ln ${line}, Col ${col} (${selectedText.length} chars, ${selectedLines} lines selected)`; + } else { + this.lineCounter.textContent = `Ln ${line}, Col ${col}`; + } + } + + /** + * Jump to specific line in editor + * @param {number} lineNumber - Line number to jump to + */ + jumpToLine(lineNumber) { + console.log('Jumping to line:', lineNumber); + + if (!this.editor) { + console.error('Editor not found'); + return; + } + + const text = this.getContent(); + const lines = text.split('\n'); + console.log('Total lines in editor:', lines.length); + + if (lineNumber > lines.length) { + console.warn('Line number exceeds file length'); + return; + } + + // Calculate character position + let position = 0; + for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) { + position += lines[i].length + 1; // +1 for newline + } + + console.log('Calculated position:', position); + + // Create range and set cursor + const range = document.createRange(); + const sel = window.getSelection(); + + let currentPos = 0; + let targetNode = null; + let targetOffset = 0; + + // Find the text node and offset + const walker = document.createTreeWalker( + this.editor, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let node; + while (node = walker.nextNode()) { + const nodeLength = node.textContent.length; + if (currentPos + nodeLength >= position) { + targetNode = node; + targetOffset = position - currentPos; + break; + } + currentPos += nodeLength; + } + + if (targetNode) { + range.setStart(targetNode, targetOffset); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + + // Scroll into view + const lineHeight = 20; + const targetScrollTop = Math.max(0, (lineNumber - 10) * lineHeight); + this.editor.scrollTop = targetScrollTop; + } + + this.editor.focus(); + this.updateStatusBar(); + + console.log('Successfully jumped to line:', lineNumber); + } + + /** + * Toggle word wrap + */ + toggleWordWrap() { + if (!this.editor) return; + + this.editor.style.whiteSpace = this.editor.style.whiteSpace === 'pre-wrap' ? 'pre' : 'pre-wrap'; + } + + /** + * Get editor content + * @returns {string} - Current editor content + */ + getContent() { + if (!this.editor) return ''; + // Replace
        tags with newlines and get text content + return this.editor.textContent || ''; + } + + /** + * Set editor content + * @param {string} content - Content to set + */ + setContent(content) { + if (this.editor) { + this.editor.textContent = content; + this.updateGutter(); + this.updateStatusBar(); + this.updateSyntaxHighlight(); + } + } + + /** + * Set file type (placeholder for future use) + * @param {string} filename - The filename to detect type from + */ + setFileType(filename) { + this.currentFileType = detectFileType(filename); + + // Enable syntax highlighting for C/C++ files + this.highlightEnabled = shouldHighlight(this.currentFileType); + + // Update highlighting immediately + this.updateSyntaxHighlight(); + } + + /** + * Setup persistent selection highlighting (like VS Code) + * Keeps selection visually highlighted even when editor loses focus + */ + setupPersistentSelection() { + // Create a canvas to measure text and draw highlights + const editorArea = document.getElementById('editor-area'); + if (!editorArea) return; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + ctx.font = '12px JetBrains Mono, monospace'; + + // Create highlight overlay that will contain absolute positioned divs + const highlightOverlay = document.createElement('div'); + highlightOverlay.id = 'selection-highlight-overlay'; + highlightOverlay.style.cssText = ` + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 0; + overflow: hidden; + `; + + // Insert overlay after gutter but before editor + const gutter = document.getElementById('gutter'); + if (gutter && gutter.nextSibling) { + editorArea.insertBefore(highlightOverlay, gutter.nextSibling); + } else { + editorArea.appendChild(highlightOverlay); + } + + // Make editor background transparent so overlay shows through + this.editor.style.position = 'relative'; + this.editor.style.zIndex = '1'; + this.editor.style.background = 'transparent'; + + let savedSelection = null; + + const updateHighlight = () => { + highlightOverlay.innerHTML = ''; + + if (!savedSelection || !this.editor.value) { + return; + } + + const { start, end } = savedSelection; + const text = this.editor.value; + const lines = text.split('\n'); + + // Get editor dimensions and position + const editorRect = this.editor.getBoundingClientRect(); + const editorAreaRect = editorArea.getBoundingClientRect(); + const gutterWidth = 61; // gutter width + border + const lineHeight = 20; + const paddingLeft = 20; + const paddingTop = 20; + + // Calculate which lines contain the selection + let charCount = 0; + let startLine = -1, startCol = -1; + let endLine = -1, endCol = -1; + + for (let i = 0; i < lines.length; i++) { + const lineLength = lines[i].length; + + if (startLine === -1 && charCount + lineLength >= start) { + startLine = i; + startCol = start - charCount; + } + + if (endLine === -1 && charCount + lineLength >= end) { + endLine = i; + endCol = end - charCount; + break; + } + + charCount += lineLength + 1; // +1 for newline + } + + if (startLine === -1 || endLine === -1) return; + + // Draw highlight rectangles for each line in selection + for (let line = startLine; line <= endLine; line++) { + const lineText = lines[line]; + let colStart = (line === startLine) ? startCol : 0; + let colEnd = (line === endLine) ? endCol : lineText.length; + + // Measure text width to get exact position + const beforeText = lineText.substring(0, colStart); + const selectedText = lineText.substring(colStart, colEnd); + + const startX = ctx.measureText(beforeText).width; + const width = ctx.measureText(selectedText).width; + + // Create highlight div for this line + const highlight = document.createElement('div'); + highlight.style.cssText = ` + position: absolute; + left: ${gutterWidth + paddingLeft + startX}px; + top: ${paddingTop + (line * lineHeight) - this.editor.scrollTop}px; + width: ${width + 2}px; + height: ${lineHeight}px; + background: #3a3d41; + pointer-events: none; + `; + + highlightOverlay.appendChild(highlight); + } + }; + + // Save selection when editor loses focus + this.editor.addEventListener('blur', () => { + const start = this.editor.selectionStart; + const end = this.editor.selectionEnd; + if (start !== end) { + savedSelection = { start, end }; + updateHighlight(); + } + }); + + // Clear highlight when editor gains focus + this.editor.addEventListener('focus', () => { + if (savedSelection) { + // Restore the selection + this.editor.setSelectionRange(savedSelection.start, savedSelection.end); + } + savedSelection = null; + highlightOverlay.innerHTML = ''; + }); + + // Update highlight position on scroll + this.editor.addEventListener('scroll', () => { + if (savedSelection) { + updateHighlight(); + } + }); + + // Clear on text change + this.editor.addEventListener('input', () => { + if (savedSelection) { + savedSelection = null; + highlightOverlay.innerHTML = ''; + } + }); + } + + escapeHtml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + /** + * Focus the editor + */ + focus() { + if (this.editor) { + this.editor.focus(); + } + } +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = EditorManager; } \ No newline at end of file diff --git a/src/renderer/managers/FileOperationsManager.js b/src/renderer/managers/FileOperationsManager.js index 1c20ad4..5e75eec 100644 --- a/src/renderer/managers/FileOperationsManager.js +++ b/src/renderer/managers/FileOperationsManager.js @@ -1,908 +1,908 @@ -;(function() { -/** - * File Operations Manager - Handles all file operations via IPC communication. - * - * This manager provides a high-level interface for file and workspace operations, - * including opening files/workspaces, saving files, and managing the file tree. - * It communicates with the main process through IPC to perform actual file system operations. - * - * @class FileOperationsManager - * @author CTrace GUI Team - * @version 1.0.0 - * - * @example - * const fileOpsManager = new FileOperationsManager(tabManager, notificationManager); - * await fileOpsManager.openWorkspace(); - */ -class FileOperationsManager { - /** - * Creates an instance of FileOperationsManager. - * - * @constructor - * @memberof FileOperationsManager - * @param {TabManager} tabManager - Tab manager instance for handling file tabs - * @param {NotificationManager} notificationManager - Notification manager for user feedback - */ - constructor(tabManager, notificationManager) { - /** - * Tab manager instance - * @type {TabManager} - * @private - */ - this.tabManager = tabManager; - - /** - * Notification manager instance - * @type {NotificationManager} - * @private - */ - this.notificationManager = notificationManager; - - /** - * Currently opened workspace path - * @type {string|null} - * @private - */ - this.currentWorkspacePath = null; - - this._fileTreeNodeData = new WeakMap(); - this._fileTreeClickHandler = this._fileTreeClickHandler.bind(this); - this._fileTreeClickHandlerAttached = false; - this._selectedFileTreeItem = null; - - /** Git decoration state: absolute file path -> status code ('U'|'M'|'A'|'D'|'R'|'C'). */ - this._gitFiles = {}; - /** Set of absolute folder paths that contain at least one changed file. */ - this._gitDirs = new Set(); - } - - /** - * Opens a workspace folder dialog and loads the selected folder. - * - * This method displays a folder selection dialog to the user, and if a folder - * is selected, it loads the folder structure and updates the UI to display - * the file tree. It also starts watching the workspace for file changes. - * - * @async - * @memberof FileOperationsManager - * @returns {Promise} Result object with folder info, or undefined if canceled - * - * @example - * const result = await fileOpsManager.openWorkspace(); - * if (result && result.success) { - * console.log('Workspace opened:', result.folderPath); - * } - */ - async openWorkspace() { - try { - const result = await window.api.invoke('open-folder-dialog'); - - if (result.success) { - this.currentWorkspacePath = result.folderPath; - const folderName = result.folderPath.split(/[/\\]/).pop(); - - // Update workspace UI - this.updateWorkspaceUI(folderName, result.fileTree); - - this.notificationManager.showSuccess(`Workspace "${folderName}" opened successfully`); - - return result; - } else if (!result.canceled) { - this.notificationManager.showError('Failed to open workspace: ' + (result.error || 'Unknown error')); - } - } catch (error) { - this.notificationManager.showError('Error opening workspace: ' + error.message); - } - } - - /** - * Close the current workspace - * Clears the workspace path and resets the file tree UI. - * - * @example - * fileOpsManager.closeWorkspace(); - */ - closeWorkspace() { - if (!this.currentWorkspacePath) { - this.notificationManager.showWarning('No workspace is currently open'); - return; - } - - const workspaceName = this.currentWorkspacePath.split(/[/\\]/).pop(); - this.currentWorkspacePath = null; - - // Clear workspace UI - const workspaceFolder = document.getElementById('workspace-folder'); - const noWorkspace = document.getElementById('no-workspace'); - const fileTreeElement = document.getElementById('file-tree'); - - if (workspaceFolder) { - workspaceFolder.style.display = 'none'; - } - - if (noWorkspace) { - noWorkspace.style.display = 'block'; - } - - if (fileTreeElement) { - fileTreeElement.innerHTML = ''; - } - - if (window.uiController && window.uiController.searchManager) { - window.uiController.searchManager.setWorkspacePath(null); - window.uiController.searchManager.clearSearchResults(); - } - - this.notificationManager.showSuccess(`Workspace "${workspaceName}" closed`); - } - - /** - * Open single file - */ - async openFile() { - try { - console.log('Opening file dialog...'); - const result = await window.api.invoke('open-file-dialog'); - console.log('Open file result:', result); - - if (result.success) { - console.log('File opened successfully, checking for warnings...'); - if (result.warning === 'large-file') { - const sizeStr = this.formatFileSize(result.totalSize); - const userChoice = await this.notificationManager.showLargeFileWarningDialog(result.fileName, sizeStr); - if (userChoice === 'cancel') return; - const largeResult = await window.api.invoke('read-large-file', result.filePath); - if (largeResult.success && largeResult.warning !== 'encoding') { - this.openFileInTab(result.filePath, largeResult.content, largeResult.fileName, { - isPartial: largeResult.isPartial, - totalSize: largeResult.totalSize, - loadedSize: largeResult.loadedSize - }); - this.notificationManager.showInfo(`Large file "${largeResult.fileName}" partially loaded (${this.formatFileSize(largeResult.loadedSize)} of ${sizeStr})`); - } else if (largeResult.warning === 'encoding') { - const encChoice = await this.notificationManager.showEncodingWarningDialog(); - if (encChoice === 'yes') { - const forceResult = await window.api.invoke('force-open-file', result.filePath); - if (forceResult.success) { - this.openFileInTab(result.filePath, forceResult.content, forceResult.fileName, { - isPartial: forceResult.isPartial, - totalSize: forceResult.totalSize, - loadedSize: forceResult.loadedSize, - encodingWarning: forceResult.encodingWarning - }); - this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); - } else { - this.notificationManager.showError('Failed to open file: ' + forceResult.error); - } - } - } else { - this.notificationManager.showError('Failed to open file: ' + largeResult.error); - } - return result; - } else if (result.warning === 'encoding') { - console.log('Encoding warning detected, showing dialog...'); - const userChoice = await this.notificationManager.showEncodingWarningDialog(); - console.log('User choice:', userChoice); - - if (userChoice === 'no') { - console.log('User chose not to open file'); - return; - } else if (userChoice === 'yes') { - console.log('User chose to open file anyway'); - const forceResult = await window.api.invoke('force-open-file', result.filePath); - console.log('Force open result:', forceResult); - - if (forceResult.success) { - this.openFileInTab(forceResult.filePath, forceResult.content, forceResult.fileName, { - isPartial: forceResult.isPartial, - totalSize: forceResult.totalSize, - loadedSize: forceResult.loadedSize, - encodingWarning: forceResult.encodingWarning - }); - this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); - } else { - this.notificationManager.showError('Failed to open file: ' + forceResult.error); - } - } - } else { - console.log('No warnings, opening file normally'); - this.openFileInTab(result.filePath, result.content, result.fileName, { - isPartial: result.isPartial, - totalSize: result.totalSize, - loadedSize: result.loadedSize - }); - - if (result.isPartial) { - this.notificationManager.showInfo(`Large file "${result.fileName}" partially loaded (${this.formatFileSize(result.loadedSize)} of ${this.formatFileSize(result.totalSize)})`); - } else { - this.notificationManager.showSuccess(`File "${result.fileName}" opened successfully`); - } - } - - return result; - } else if (!result.canceled) { - this.notificationManager.showError('Failed to open file: ' + (result.error || 'Unknown error')); - } - } catch (error) { - this.notificationManager.showError('Error opening file: ' + error.message); - } - } - - /** - * Open file in tab - * @param {string} filePath - File path - * @param {string} content - File content - * @param {string} fileName - File name - * @param {Object} fileInfo - File metadata - */ - openFileInTab(filePath, content, fileName, fileInfo = {}) { - // Check if file is already open - const existingTabId = this.tabManager.findTabByPath(filePath); - if (existingTabId) { - this.tabManager.switchToTab(existingTabId); - return existingTabId; - } - - // Create new tab - const tabId = this.tabManager.createTab(fileName, filePath, content, fileInfo); - this.tabManager.switchToTab(tabId); - return tabId; - } - - /** - * Save current file - */ - async saveFile(options = {}) { - try { - const currentTab = this.tabManager.getActiveTab(); - if (!currentTab) { - // Create a new file if none exists - this.tabManager.createNewFile(); - await new Promise(resolve => setTimeout(resolve, 100)); - return await this.saveFile(options); - } - - // Update current tab content from editor - currentTab.content = this.tabManager.editorManager.getContent(); - - if (currentTab.filePath) { - const result = await window.api.invoke('save-file', currentTab.filePath, currentTab.content); - if (result.success) { - this.tabManager.markTabClean(this.tabManager.activeTabId); - // File was saved successfully — clear any missing flag - if (this.tabManager.activeTabId) { - this.tabManager.markTabMissing(this.tabManager.activeTabId, false); - } - if (!options.silent) { - this.notificationManager.showSuccess('File saved successfully'); - } - this.refreshGitStatus(); - return result; - } else { - // If save failed because the file no longer exists, mark tab as missing - if (result.error && result.error.includes('ENOENT') && this.tabManager.activeTabId) { - this.tabManager.markTabMissing(this.tabManager.activeTabId, true); - this.notificationManager.showError('File not found on disk — it may have been moved or deleted'); - } else { - this.notificationManager.showError('Failed to save file: ' + result.error); - } - } - } else { - // Save as new file (untitled -> actual file) - const result = await this.saveAsFile(); - // Syntax highlighting is already updated in saveAsFile - return result; - } - } catch (error) { - this.notificationManager.showError('Error saving file: ' + error.message); - } - } - - /** - * Save a specific tab by id. - * Useful for auto-save on tab close when the tab is not active. - * - * @param {string} tabId - * @param {{silent?: boolean, reason?: string}} [options] - */ - async saveTabById(tabId, options = {}) { - try { - if (!this.tabManager || typeof this.tabManager.getTab !== 'function') { - return; - } - - const tab = this.tabManager.getTab(tabId); - if (!tab) return; - - // Avoid Save As dialogs during auto-save. - if (!tab.filePath) return; - - const content = tabId === this.tabManager.activeTabId - ? this.tabManager.editorManager.getContent() - : tab.content; - - const result = await window.api.invoke('save-file', tab.filePath, content); - if (result.success) { - tab.content = content; - this.tabManager.markTabClean(tabId); - if (!options.silent) { - this.notificationManager.showSuccess('File saved successfully'); - } - this.refreshGitStatus(); - return result; - } - - this.notificationManager.showError('Failed to save file: ' + (result.error || 'Unknown error')); - return result; - } catch (error) { - this.notificationManager.showError('Error saving file: ' + error.message); - } - } - - /** - * Save file as - */ - async saveAsFile() { - try { - const currentTab = this.tabManager.getActiveTab(); - if (!currentTab) { - this.notificationManager.showWarning('No file to save'); - return; - } - - currentTab.content = this.tabManager.editorManager.getContent(); - - const result = await window.api.invoke('save-file-as', currentTab.content); - if (result.success) { - this.tabManager.updateTabFile(this.tabManager.activeTabId, result.filePath, result.fileName); - this.tabManager.markTabClean(this.tabManager.activeTabId); - - // Update file type and trigger syntax highlighting - this.tabManager.editorManager.setFileType(result.fileName); - - this.notificationManager.showSuccess(`File saved as "${result.fileName}"`); - return result; - } else if (!result.canceled) { - this.notificationManager.showError('Failed to save file: ' + result.error); - } - } catch (error) { - this.notificationManager.showError('Error saving file: ' + error.message); - } - } - - /** - * Read file from file tree - * @param {string} filePath - File path to read - */ - async readFileFromTree(filePath) { - try { - console.log('Reading file from tree:', filePath); - const result = await window.api.invoke('read-file', filePath); - console.log('File tree read result:', result); - - if (result.success) { - if (result.warning === 'large-file') { - const sizeStr = this.formatFileSize(result.totalSize); - const userChoice = await this.notificationManager.showLargeFileWarningDialog(result.fileName, sizeStr); - if (userChoice === 'cancel') return; - const largeResult = await window.api.invoke('read-large-file', filePath); - if (largeResult.success && largeResult.warning !== 'encoding') { - const tabId = this.openFileInTab(filePath, largeResult.content, largeResult.fileName, { - isPartial: largeResult.isPartial, - totalSize: largeResult.totalSize, - loadedSize: largeResult.loadedSize - }); - this.notificationManager.showInfo(`Large file "${largeResult.fileName}" partially loaded (${this.formatFileSize(largeResult.loadedSize)} of ${sizeStr})`); - return tabId; - } else if (largeResult.warning === 'encoding') { - const encChoice = await this.notificationManager.showEncodingWarningDialog(); - if (encChoice === 'yes') { - const forceResult = await window.api.invoke('force-open-file', filePath); - if (forceResult.success) { - const tabId = this.openFileInTab(filePath, forceResult.content, forceResult.fileName, { - isPartial: forceResult.isPartial, - totalSize: forceResult.totalSize, - loadedSize: forceResult.loadedSize, - encodingWarning: forceResult.encodingWarning - }); - this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); - return tabId; - } else { - this.notificationManager.showError('Failed to open file: ' + forceResult.error); - } - } - } else { - this.notificationManager.showError('Failed to open file: ' + largeResult.error); - } - } else if (result.warning === 'encoding') { - console.log('File tree: Encoding warning detected, showing dialog...'); - const userChoice = await this.notificationManager.showEncodingWarningDialog(); - console.log('File tree: User choice:', userChoice); - - if (userChoice === 'no') { - console.log('File tree: User chose not to open file'); - return; - } else if (userChoice === 'yes') { - console.log('File tree: User chose to open file anyway'); - const forceResult = await window.api.invoke('force-open-file', filePath); - console.log('File tree: Force open result:', forceResult); - - if (forceResult.success) { - const tabId = this.openFileInTab(filePath, forceResult.content, forceResult.fileName, { - isPartial: forceResult.isPartial, - totalSize: forceResult.totalSize, - loadedSize: forceResult.loadedSize, - encodingWarning: forceResult.encodingWarning - }); - - this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); - return tabId; - } else { - this.notificationManager.showError('Failed to open file: ' + forceResult.error); - } - } - } else { - // Normal file opening - no encoding issues - console.log('File tree: No warnings, opening file normally'); - const tabId = this.openFileInTab(filePath, result.content, result.fileName, { - isPartial: result.isPartial, - totalSize: result.totalSize, - loadedSize: result.loadedSize, - encodingWarning: result.encodingWarning - }); - - return tabId; - } - } else { - this.notificationManager.showError('Failed to open file: ' + result.error); - } - } catch (error) { - this.notificationManager.showError('Error opening file: ' + error.message); - } - } - - /** - * Load the next 1 MB chunk of a partially loaded file - * @param {string} filePath - File path - * @param {number} offset - Byte offset to read from - */ - async loadNextChunk(filePath, offset) { - if (!filePath || !this.tabManager.activeTabId) return; - - const btn = document.getElementById('partial-load-more-btn'); - if (btn) btn.disabled = true; - - try { - const result = await window.api.invoke('read-file-chunk', filePath, offset); - - if (result.success) { - const currentTab = this.tabManager.getActiveTab(); - if (currentTab) { - const currentContent = this.tabManager.editorManager.getContent(); - const newContent = currentContent + result.content; - await this.tabManager.editorManager.setContent(newContent); - - currentTab.content = newContent; - currentTab.fileInfo = { - ...currentTab.fileInfo, - loadedSize: result.newOffset, - isPartial: result.isMore - }; - - // Update banner and status bar - this.tabManager._updatePartialBanner(currentTab); - this.tabManager.updateFileStatus(currentTab); - - const loadedMB = (result.newOffset / (1024 * 1024)).toFixed(1); - const totalMB = (result.totalSize / (1024 * 1024)).toFixed(1); - if (result.isMore) { - this.notificationManager.showInfo(`Loaded ${loadedMB} MB of ${totalMB} MB`); - } else { - this.notificationManager.showSuccess(`Full file loaded (${loadedMB} MB)`); - } - } - } else { - this.notificationManager.showError('Failed to load chunk: ' + result.error); - if (btn) btn.disabled = false; - } - } catch (error) { - this.notificationManager.showError('Error loading chunk: ' + error.message); - if (btn) btn.disabled = false; - } - } - - /** - * Load full file (for partially loaded large files) - * @param {string} filePath - File path - */ - async loadFullFile(filePath) { - if (!filePath || !this.tabManager.activeTabId) return; - - try { - this.notificationManager.showInfo('Loading full file...'); - const result = await window.api.invoke('force-load-full-file', filePath); - - if (result.success) { - const currentTab = this.tabManager.getActiveTab(); - if (currentTab) { - // Update tab content and file info - currentTab.content = result.content; - currentTab.fileInfo = { - ...currentTab.fileInfo, - isPartial: false, - loadedSize: result.totalSize, - totalSize: result.totalSize - }; - - // Update editor content - await this.tabManager.editorManager.setContent(result.content); - - // Update tab appearance to remove warning - const tabElement = document.querySelector(`[data-tab-id="${this.tabManager.activeTabId}"]`); - if (tabElement) { - const tabLabel = tabElement.querySelector('.tab-label'); - if (tabLabel) { - tabLabel.textContent = currentTab.fileName; // Remove warning indicator - } - } - - this.notificationManager.showSuccess(`Full file loaded (${Math.round(result.totalSize / 1024)}KB)`); - } - } else { - this.notificationManager.showError('Failed to load full file: ' + result.error); - } - } catch (error) { - console.error(`[loadFullFile] Failed to load "${filePath}": ${error.message}`); - this.notificationManager.showError('Error loading full file: ' + error.message); - } - } - - /** - * Update workspace UI - * @param {string} folderName - Folder name - * @param {Array} fileTree - File tree structure - */ - updateWorkspaceUI(folderName, fileTree) { - const workspaceName = document.getElementById('workspace-name'); - const workspaceFolder = document.getElementById('workspace-folder'); - const noWorkspace = document.getElementById('no-workspace'); - const fileTreeElement = document.getElementById('file-tree'); - - if (workspaceName) { - workspaceName.textContent = folderName.toUpperCase(); - } - - // Wire the collapsible workspace-root row once. Clicking it toggles the - // whole tree; the chevron rotates via the `.expanded` class. - const rootRow = document.getElementById('workspace-root'); - const treeEl = document.getElementById('file-tree'); - if (rootRow && treeEl && !this._workspaceRootToggleAttached) { - this._workspaceRootToggleAttached = true; - rootRow.addEventListener('click', () => { - const expanded = rootRow.classList.toggle('expanded'); - treeEl.style.display = expanded ? '' : 'none'; - }); - } - - if (workspaceFolder) { - // Must match sidebar.css's `#workspace-folder { display: flex; ... }` — - // 'block' would override the flex column layout that stretches - // .file-tree's empty space to fill the rest of the panel. - workspaceFolder.style.display = 'flex'; - } - - if (noWorkspace) { - noWorkspace.style.display = 'none'; - } - - if (fileTreeElement && fileTree) { - if (!this._fileTreeClickHandlerAttached) { - fileTreeElement.addEventListener('click', this._fileTreeClickHandler); - this._fileTreeClickHandlerAttached = true; - } - this.renderFileTree(fileTree, fileTreeElement); - this.refreshGitStatus(); - } - } - - /** - * Fetch git status for the workspace and (re)apply decorations to the tree. - * Safe to call repeatedly; resolves quietly when there is no repo/git. - */ - async refreshGitStatus() { - if (!window.api || typeof window.api.invoke !== 'function') return; - try { - const res = await window.api.invoke('get-git-status'); - const files = (res && res.files) || {}; - this._gitFiles = files; - - // Roll up: mark every ancestor folder of a changed file. - const dirs = new Set(); - for (const filePath of Object.keys(files)) { - let dir = filePath.replace(/[/\\][^/\\]*$/, ''); - while (dir && dir.length > 1) { - if (dirs.has(dir)) break; - dirs.add(dir); - const parent = dir.replace(/[/\\][^/\\]*$/, ''); - if (parent === dir) break; - dir = parent; - } - } - this._gitDirs = dirs; - - this.redecorateVisibleTree(); - } catch { - // Ignore — decorations are best-effort. - } - } - - /** Re-apply git decorations to every currently rendered tree row. */ - redecorateVisibleTree() { - const container = document.getElementById('file-tree'); - if (!container) return; - container.querySelectorAll('.file-tree-item').forEach((el) => { - const item = this._fileTreeNodeData.get(el); - if (item) this.applyGitDecoration(el, item); - }); - } - - /** - * Apply (or clear) the git decoration for a single tree row. Idempotent. - * @param {Element} el - The .file-tree-item element - * @param {Object} item - The tree node { path, type } - */ - applyGitDecoration(el, item) { - const badge = el.querySelector('.git-badge'); - // Reset prior decoration. - el.removeAttribute('data-git'); - if (badge) { badge.textContent = ''; badge.className = 'git-badge'; } - - if (item.type === 'directory') { - if (this._gitDirs.has(item.path) && badge) { - el.setAttribute('data-git', 'dir'); - badge.classList.add('git-dot'); - } - return; - } - - const code = this._gitFiles[item.path]; - if (code && badge) { - el.setAttribute('data-git', code); - badge.textContent = code; - } - } - - _fileTreeClickHandler(event) { - const container = document.getElementById('file-tree'); - if (!container) return; - - const itemElement = event.target && event.target.closest - ? event.target.closest('.file-tree-item') - : null; - if (!itemElement) return; - - const item = this._fileTreeNodeData.get(itemElement); - if (!item) return; - - if (item.type === 'directory') { - const isExpanded = itemElement.getAttribute('data-expanded') === 'true'; - - if (!isExpanded) { - const childContainer = document.createElement('div'); - childContainer.setAttribute('data-parent-path', item.path); - itemElement.parentNode.insertBefore(childContainer, itemElement.nextSibling); - - // Lazy load directory contents if not already loaded - if (!item.children) { - this.loadDirectoryContents(item, childContainer, itemElement); - } else { - this.renderFileTree(item.children, childContainer, (parseInt(itemElement.getAttribute('data-level') || '0', 10) + 1)); - } - - itemElement.classList.add('expanded'); - itemElement.setAttribute('data-expanded', 'true'); - } else { - const nextSibling = itemElement.nextSibling; - if (nextSibling && nextSibling.getAttribute && nextSibling.getAttribute('data-parent-path') === item.path) { - nextSibling.remove(); - } - itemElement.classList.remove('expanded'); - itemElement.setAttribute('data-expanded', 'false'); - } - - return; - } - - if (item.type === 'file') { - this.readFileFromTree(item.path).then(tabId => { - if (!tabId) return; - if (this._selectedFileTreeItem) { - this._selectedFileTreeItem.classList.remove('selected'); - } - itemElement.classList.add('selected'); - this._selectedFileTreeItem = itemElement; - }); - } - } - - /** - * Load directory contents on demand (lazy loading) - * @param {Object} item - Directory item - * @param {Element} container - Container to render into - * @param {Element} itemElement - The directory element - */ - async loadDirectoryContents(item, container, itemElement) { - const level = parseInt(itemElement.getAttribute('data-level') || '0', 10) + 1; - - // Show loading indicator - const loadingDiv = document.createElement('div'); - loadingDiv.style.marginLeft = (level * 16) + 'px'; - loadingDiv.style.color = '#7d8590'; - loadingDiv.style.fontSize = '11px'; - loadingDiv.style.padding = '2px 8px'; - loadingDiv.textContent = 'Loading...'; - container.appendChild(loadingDiv); - - try { - const result = await window.api.invoke('get-directory-contents', item.path); - - // Remove loading indicator - loadingDiv.remove(); - - if (result.success) { - // Store children in the item - item.children = result.contents; - - // Render the children (rows are git-decorated as they are built) - this.renderFileTree(result.contents, container, level); - - itemElement.classList.add('expanded'); - itemElement.setAttribute('data-expanded', 'true'); - } else { - // Show error - loadingDiv.textContent = 'Error loading directory'; - loadingDiv.style.color = '#f85149'; - } - } catch (error) { - console.error(`[loadDirectoryContents] Failed to load "${item.path}": ${error.message}`); - loadingDiv.textContent = `Error loading "${item.name}": ${error.message}`; - loadingDiv.style.color = '#f85149'; - } - } - - /** - * Render file tree - * @param {Array} tree - File tree structure - * @param {Element} container - Container element - * @param {number} level - Nesting level - */ - renderFileTree(tree, container = null, level = 0) { - if (!container) { - container = document.getElementById('file-tree'); - } - - if (!container) return; - - if (level === 0) { - container.textContent = ''; - this._selectedFileTreeItem = null; - } - - const CHUNK_SIZE = 250; - const raf = (typeof requestAnimationFrame === 'function') - ? requestAnimationFrame - : (cb) => setTimeout(cb, 0); - const renderChunk = (startIndex) => { - const fragment = document.createDocumentFragment(); - - for (let i = startIndex; i < Math.min(startIndex + CHUNK_SIZE, tree.length); i++) { - const item = tree[i]; - const itemElement = document.createElement('div'); - itemElement.className = 'file-tree-item'; - itemElement.setAttribute('data-path', item.path); - itemElement.setAttribute('data-type', item.type); - itemElement.setAttribute('data-name', item.name); - itemElement.setAttribute('data-level', String(level)); - - this._fileTreeNodeData.set(itemElement, item); - - // Indent guide lines, one per ancestor level (VSCode style). - const guides = ''.repeat(level); - - if (item.type === 'directory') { - itemElement.setAttribute('data-expanded', 'false'); - itemElement.innerHTML = ` - ${guides} - ${escapeHtml(item.name)} - - `; - } else { - const fileIcon = this.getFileIcon(item.name); - itemElement.innerHTML = ` - ${guides}${fileIcon} - ${escapeHtml(item.name)} - - `; - } - - this.applyGitDecoration(itemElement, item); - fragment.appendChild(itemElement); - } - - container.appendChild(fragment); - - if (startIndex + CHUNK_SIZE < tree.length) { - raf(() => renderChunk(startIndex + CHUNK_SIZE)); - } - }; - - renderChunk(0); - } - - /** - * Get file icon based on extension - * @param {string} filename - Filename - * @returns {string} - File icon markup or emoji - */ - getFileIcon(filename) { - if (typeof window !== 'undefined' && typeof window.getFileIcon === 'function') { - return window.getFileIcon(filename); - } - - const ext = filename.split('.').pop().toLowerCase(); - const fallbackIconMap = { - js: '🟨', - ts: '🔷', - html: '🟧', - css: '🎨', - json: '📋', - md: '📝', - py: '🐍', - cpp: '⚙️', - c: '⚙️', - cc: '⚙️', - cxx: '⚙️', - h: '📄', - hpp: '📄', - hh: '📄', - hxx: '📄', - java: '☕', - php: '🐘', - rb: '💎', - go: '🐹', - rs: '🦀', - sh: '📜' - }; - - return fallbackIconMap[ext] || '📄'; - } - - /** - * Format file size for display - * @param {number} bytes - File size in bytes - * @returns {string} - Formatted file size - */ - formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; - } - - /** - * Get current workspace path - * @returns {string|null} - Current workspace path - */ - getCurrentWorkspacePath() { - return this.currentWorkspacePath; - } -} - -if (typeof window !== 'undefined') { - window.FileOperationsManager = FileOperationsManager; -} -if (typeof module !== 'undefined' && module.exports) { - module.exports = FileOperationsManager; -} -})(); +;(function() { +/** + * File Operations Manager - Handles all file operations via IPC communication. + * + * This manager provides a high-level interface for file and workspace operations, + * including opening files/workspaces, saving files, and managing the file tree. + * It communicates with the main process through IPC to perform actual file system operations. + * + * @class FileOperationsManager + * @author CTrace GUI Team + * @version 1.0.0 + * + * @example + * const fileOpsManager = new FileOperationsManager(tabManager, notificationManager); + * await fileOpsManager.openWorkspace(); + */ +class FileOperationsManager { + /** + * Creates an instance of FileOperationsManager. + * + * @constructor + * @memberof FileOperationsManager + * @param {TabManager} tabManager - Tab manager instance for handling file tabs + * @param {NotificationManager} notificationManager - Notification manager for user feedback + */ + constructor(tabManager, notificationManager) { + /** + * Tab manager instance + * @type {TabManager} + * @private + */ + this.tabManager = tabManager; + + /** + * Notification manager instance + * @type {NotificationManager} + * @private + */ + this.notificationManager = notificationManager; + + /** + * Currently opened workspace path + * @type {string|null} + * @private + */ + this.currentWorkspacePath = null; + + this._fileTreeNodeData = new WeakMap(); + this._fileTreeClickHandler = this._fileTreeClickHandler.bind(this); + this._fileTreeClickHandlerAttached = false; + this._selectedFileTreeItem = null; + + /** Git decoration state: absolute file path -> status code ('U'|'M'|'A'|'D'|'R'|'C'). */ + this._gitFiles = {}; + /** Set of absolute folder paths that contain at least one changed file. */ + this._gitDirs = new Set(); + } + + /** + * Opens a workspace folder dialog and loads the selected folder. + * + * This method displays a folder selection dialog to the user, and if a folder + * is selected, it loads the folder structure and updates the UI to display + * the file tree. It also starts watching the workspace for file changes. + * + * @async + * @memberof FileOperationsManager + * @returns {Promise} Result object with folder info, or undefined if canceled + * + * @example + * const result = await fileOpsManager.openWorkspace(); + * if (result && result.success) { + * console.log('Workspace opened:', result.folderPath); + * } + */ + async openWorkspace() { + try { + const result = await window.api.invoke('open-folder-dialog'); + + if (result.success) { + this.currentWorkspacePath = result.folderPath; + const folderName = result.folderPath.split(/[/\\]/).pop(); + + // Update workspace UI + this.updateWorkspaceUI(folderName, result.fileTree); + + this.notificationManager.showSuccess(`Workspace "${folderName}" opened successfully`); + + return result; + } else if (!result.canceled) { + this.notificationManager.showError('Failed to open workspace: ' + (result.error || 'Unknown error')); + } + } catch (error) { + this.notificationManager.showError('Error opening workspace: ' + error.message); + } + } + + /** + * Close the current workspace + * Clears the workspace path and resets the file tree UI. + * + * @example + * fileOpsManager.closeWorkspace(); + */ + closeWorkspace() { + if (!this.currentWorkspacePath) { + this.notificationManager.showWarning('No workspace is currently open'); + return; + } + + const workspaceName = this.currentWorkspacePath.split(/[/\\]/).pop(); + this.currentWorkspacePath = null; + + // Clear workspace UI + const workspaceFolder = document.getElementById('workspace-folder'); + const noWorkspace = document.getElementById('no-workspace'); + const fileTreeElement = document.getElementById('file-tree'); + + if (workspaceFolder) { + workspaceFolder.style.display = 'none'; + } + + if (noWorkspace) { + noWorkspace.style.display = 'block'; + } + + if (fileTreeElement) { + fileTreeElement.innerHTML = ''; + } + + if (window.uiController && window.uiController.searchManager) { + window.uiController.searchManager.setWorkspacePath(null); + window.uiController.searchManager.clearSearchResults(); + } + + this.notificationManager.showSuccess(`Workspace "${workspaceName}" closed`); + } + + /** + * Open single file + */ + async openFile() { + try { + console.log('Opening file dialog...'); + const result = await window.api.invoke('open-file-dialog'); + console.log('Open file result:', result); + + if (result.success) { + console.log('File opened successfully, checking for warnings...'); + if (result.warning === 'large-file') { + const sizeStr = this.formatFileSize(result.totalSize); + const userChoice = await this.notificationManager.showLargeFileWarningDialog(result.fileName, sizeStr); + if (userChoice === 'cancel') return; + const largeResult = await window.api.invoke('read-large-file', result.filePath); + if (largeResult.success && largeResult.warning !== 'encoding') { + this.openFileInTab(result.filePath, largeResult.content, largeResult.fileName, { + isPartial: largeResult.isPartial, + totalSize: largeResult.totalSize, + loadedSize: largeResult.loadedSize + }); + this.notificationManager.showInfo(`Large file "${largeResult.fileName}" partially loaded (${this.formatFileSize(largeResult.loadedSize)} of ${sizeStr})`); + } else if (largeResult.warning === 'encoding') { + const encChoice = await this.notificationManager.showEncodingWarningDialog(); + if (encChoice === 'yes') { + const forceResult = await window.api.invoke('force-open-file', result.filePath); + if (forceResult.success) { + this.openFileInTab(result.filePath, forceResult.content, forceResult.fileName, { + isPartial: forceResult.isPartial, + totalSize: forceResult.totalSize, + loadedSize: forceResult.loadedSize, + encodingWarning: forceResult.encodingWarning + }); + this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); + } else { + this.notificationManager.showError('Failed to open file: ' + forceResult.error); + } + } + } else { + this.notificationManager.showError('Failed to open file: ' + largeResult.error); + } + return result; + } else if (result.warning === 'encoding') { + console.log('Encoding warning detected, showing dialog...'); + const userChoice = await this.notificationManager.showEncodingWarningDialog(); + console.log('User choice:', userChoice); + + if (userChoice === 'no') { + console.log('User chose not to open file'); + return; + } else if (userChoice === 'yes') { + console.log('User chose to open file anyway'); + const forceResult = await window.api.invoke('force-open-file', result.filePath); + console.log('Force open result:', forceResult); + + if (forceResult.success) { + this.openFileInTab(forceResult.filePath, forceResult.content, forceResult.fileName, { + isPartial: forceResult.isPartial, + totalSize: forceResult.totalSize, + loadedSize: forceResult.loadedSize, + encodingWarning: forceResult.encodingWarning + }); + this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); + } else { + this.notificationManager.showError('Failed to open file: ' + forceResult.error); + } + } + } else { + console.log('No warnings, opening file normally'); + this.openFileInTab(result.filePath, result.content, result.fileName, { + isPartial: result.isPartial, + totalSize: result.totalSize, + loadedSize: result.loadedSize + }); + + if (result.isPartial) { + this.notificationManager.showInfo(`Large file "${result.fileName}" partially loaded (${this.formatFileSize(result.loadedSize)} of ${this.formatFileSize(result.totalSize)})`); + } else { + this.notificationManager.showSuccess(`File "${result.fileName}" opened successfully`); + } + } + + return result; + } else if (!result.canceled) { + this.notificationManager.showError('Failed to open file: ' + (result.error || 'Unknown error')); + } + } catch (error) { + this.notificationManager.showError('Error opening file: ' + error.message); + } + } + + /** + * Open file in tab + * @param {string} filePath - File path + * @param {string} content - File content + * @param {string} fileName - File name + * @param {Object} fileInfo - File metadata + */ + openFileInTab(filePath, content, fileName, fileInfo = {}) { + // Check if file is already open + const existingTabId = this.tabManager.findTabByPath(filePath); + if (existingTabId) { + this.tabManager.switchToTab(existingTabId); + return existingTabId; + } + + // Create new tab + const tabId = this.tabManager.createTab(fileName, filePath, content, fileInfo); + this.tabManager.switchToTab(tabId); + return tabId; + } + + /** + * Save current file + */ + async saveFile(options = {}) { + try { + const currentTab = this.tabManager.getActiveTab(); + if (!currentTab) { + // Create a new file if none exists + this.tabManager.createNewFile(); + await new Promise(resolve => setTimeout(resolve, 100)); + return await this.saveFile(options); + } + + // Update current tab content from editor + currentTab.content = this.tabManager.editorManager.getContent(); + + if (currentTab.filePath) { + const result = await window.api.invoke('save-file', currentTab.filePath, currentTab.content); + if (result.success) { + this.tabManager.markTabClean(this.tabManager.activeTabId); + // File was saved successfully — clear any missing flag + if (this.tabManager.activeTabId) { + this.tabManager.markTabMissing(this.tabManager.activeTabId, false); + } + if (!options.silent) { + this.notificationManager.showSuccess('File saved successfully'); + } + this.refreshGitStatus(); + return result; + } else { + // If save failed because the file no longer exists, mark tab as missing + if (result.error && result.error.includes('ENOENT') && this.tabManager.activeTabId) { + this.tabManager.markTabMissing(this.tabManager.activeTabId, true); + this.notificationManager.showError('File not found on disk — it may have been moved or deleted'); + } else { + this.notificationManager.showError('Failed to save file: ' + result.error); + } + } + } else { + // Save as new file (untitled -> actual file) + const result = await this.saveAsFile(); + // Syntax highlighting is already updated in saveAsFile + return result; + } + } catch (error) { + this.notificationManager.showError('Error saving file: ' + error.message); + } + } + + /** + * Save a specific tab by id. + * Useful for auto-save on tab close when the tab is not active. + * + * @param {string} tabId + * @param {{silent?: boolean, reason?: string}} [options] + */ + async saveTabById(tabId, options = {}) { + try { + if (!this.tabManager || typeof this.tabManager.getTab !== 'function') { + return; + } + + const tab = this.tabManager.getTab(tabId); + if (!tab) return; + + // Avoid Save As dialogs during auto-save. + if (!tab.filePath) return; + + const content = tabId === this.tabManager.activeTabId + ? this.tabManager.editorManager.getContent() + : tab.content; + + const result = await window.api.invoke('save-file', tab.filePath, content); + if (result.success) { + tab.content = content; + this.tabManager.markTabClean(tabId); + if (!options.silent) { + this.notificationManager.showSuccess('File saved successfully'); + } + this.refreshGitStatus(); + return result; + } + + this.notificationManager.showError('Failed to save file: ' + (result.error || 'Unknown error')); + return result; + } catch (error) { + this.notificationManager.showError('Error saving file: ' + error.message); + } + } + + /** + * Save file as + */ + async saveAsFile() { + try { + const currentTab = this.tabManager.getActiveTab(); + if (!currentTab) { + this.notificationManager.showWarning('No file to save'); + return; + } + + currentTab.content = this.tabManager.editorManager.getContent(); + + const result = await window.api.invoke('save-file-as', currentTab.content); + if (result.success) { + this.tabManager.updateTabFile(this.tabManager.activeTabId, result.filePath, result.fileName); + this.tabManager.markTabClean(this.tabManager.activeTabId); + + // Update file type and trigger syntax highlighting + this.tabManager.editorManager.setFileType(result.fileName); + + this.notificationManager.showSuccess(`File saved as "${result.fileName}"`); + return result; + } else if (!result.canceled) { + this.notificationManager.showError('Failed to save file: ' + result.error); + } + } catch (error) { + this.notificationManager.showError('Error saving file: ' + error.message); + } + } + + /** + * Read file from file tree + * @param {string} filePath - File path to read + */ + async readFileFromTree(filePath) { + try { + console.log('Reading file from tree:', filePath); + const result = await window.api.invoke('read-file', filePath); + console.log('File tree read result:', result); + + if (result.success) { + if (result.warning === 'large-file') { + const sizeStr = this.formatFileSize(result.totalSize); + const userChoice = await this.notificationManager.showLargeFileWarningDialog(result.fileName, sizeStr); + if (userChoice === 'cancel') return; + const largeResult = await window.api.invoke('read-large-file', filePath); + if (largeResult.success && largeResult.warning !== 'encoding') { + const tabId = this.openFileInTab(filePath, largeResult.content, largeResult.fileName, { + isPartial: largeResult.isPartial, + totalSize: largeResult.totalSize, + loadedSize: largeResult.loadedSize + }); + this.notificationManager.showInfo(`Large file "${largeResult.fileName}" partially loaded (${this.formatFileSize(largeResult.loadedSize)} of ${sizeStr})`); + return tabId; + } else if (largeResult.warning === 'encoding') { + const encChoice = await this.notificationManager.showEncodingWarningDialog(); + if (encChoice === 'yes') { + const forceResult = await window.api.invoke('force-open-file', filePath); + if (forceResult.success) { + const tabId = this.openFileInTab(filePath, forceResult.content, forceResult.fileName, { + isPartial: forceResult.isPartial, + totalSize: forceResult.totalSize, + loadedSize: forceResult.loadedSize, + encodingWarning: forceResult.encodingWarning + }); + this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); + return tabId; + } else { + this.notificationManager.showError('Failed to open file: ' + forceResult.error); + } + } + } else { + this.notificationManager.showError('Failed to open file: ' + largeResult.error); + } + } else if (result.warning === 'encoding') { + console.log('File tree: Encoding warning detected, showing dialog...'); + const userChoice = await this.notificationManager.showEncodingWarningDialog(); + console.log('File tree: User choice:', userChoice); + + if (userChoice === 'no') { + console.log('File tree: User chose not to open file'); + return; + } else if (userChoice === 'yes') { + console.log('File tree: User chose to open file anyway'); + const forceResult = await window.api.invoke('force-open-file', filePath); + console.log('File tree: Force open result:', forceResult); + + if (forceResult.success) { + const tabId = this.openFileInTab(filePath, forceResult.content, forceResult.fileName, { + isPartial: forceResult.isPartial, + totalSize: forceResult.totalSize, + loadedSize: forceResult.loadedSize, + encodingWarning: forceResult.encodingWarning + }); + + this.notificationManager.showWarning(`File "${forceResult.fileName}" opened with encoding warnings`); + return tabId; + } else { + this.notificationManager.showError('Failed to open file: ' + forceResult.error); + } + } + } else { + // Normal file opening - no encoding issues + console.log('File tree: No warnings, opening file normally'); + const tabId = this.openFileInTab(filePath, result.content, result.fileName, { + isPartial: result.isPartial, + totalSize: result.totalSize, + loadedSize: result.loadedSize, + encodingWarning: result.encodingWarning + }); + + return tabId; + } + } else { + this.notificationManager.showError('Failed to open file: ' + result.error); + } + } catch (error) { + this.notificationManager.showError('Error opening file: ' + error.message); + } + } + + /** + * Load the next 1 MB chunk of a partially loaded file + * @param {string} filePath - File path + * @param {number} offset - Byte offset to read from + */ + async loadNextChunk(filePath, offset) { + if (!filePath || !this.tabManager.activeTabId) return; + + const btn = document.getElementById('partial-load-more-btn'); + if (btn) btn.disabled = true; + + try { + const result = await window.api.invoke('read-file-chunk', filePath, offset); + + if (result.success) { + const currentTab = this.tabManager.getActiveTab(); + if (currentTab) { + const currentContent = this.tabManager.editorManager.getContent(); + const newContent = currentContent + result.content; + await this.tabManager.editorManager.setContent(newContent); + + currentTab.content = newContent; + currentTab.fileInfo = { + ...currentTab.fileInfo, + loadedSize: result.newOffset, + isPartial: result.isMore + }; + + // Update banner and status bar + this.tabManager._updatePartialBanner(currentTab); + this.tabManager.updateFileStatus(currentTab); + + const loadedMB = (result.newOffset / (1024 * 1024)).toFixed(1); + const totalMB = (result.totalSize / (1024 * 1024)).toFixed(1); + if (result.isMore) { + this.notificationManager.showInfo(`Loaded ${loadedMB} MB of ${totalMB} MB`); + } else { + this.notificationManager.showSuccess(`Full file loaded (${loadedMB} MB)`); + } + } + } else { + this.notificationManager.showError('Failed to load chunk: ' + result.error); + if (btn) btn.disabled = false; + } + } catch (error) { + this.notificationManager.showError('Error loading chunk: ' + error.message); + if (btn) btn.disabled = false; + } + } + + /** + * Load full file (for partially loaded large files) + * @param {string} filePath - File path + */ + async loadFullFile(filePath) { + if (!filePath || !this.tabManager.activeTabId) return; + + try { + this.notificationManager.showInfo('Loading full file...'); + const result = await window.api.invoke('force-load-full-file', filePath); + + if (result.success) { + const currentTab = this.tabManager.getActiveTab(); + if (currentTab) { + // Update tab content and file info + currentTab.content = result.content; + currentTab.fileInfo = { + ...currentTab.fileInfo, + isPartial: false, + loadedSize: result.totalSize, + totalSize: result.totalSize + }; + + // Update editor content + await this.tabManager.editorManager.setContent(result.content); + + // Update tab appearance to remove warning + const tabElement = document.querySelector(`[data-tab-id="${this.tabManager.activeTabId}"]`); + if (tabElement) { + const tabLabel = tabElement.querySelector('.tab-label'); + if (tabLabel) { + tabLabel.textContent = currentTab.fileName; // Remove warning indicator + } + } + + this.notificationManager.showSuccess(`Full file loaded (${Math.round(result.totalSize / 1024)}KB)`); + } + } else { + this.notificationManager.showError('Failed to load full file: ' + result.error); + } + } catch (error) { + console.error(`[loadFullFile] Failed to load "${filePath}": ${error.message}`); + this.notificationManager.showError('Error loading full file: ' + error.message); + } + } + + /** + * Update workspace UI + * @param {string} folderName - Folder name + * @param {Array} fileTree - File tree structure + */ + updateWorkspaceUI(folderName, fileTree) { + const workspaceName = document.getElementById('workspace-name'); + const workspaceFolder = document.getElementById('workspace-folder'); + const noWorkspace = document.getElementById('no-workspace'); + const fileTreeElement = document.getElementById('file-tree'); + + if (workspaceName) { + workspaceName.textContent = folderName.toUpperCase(); + } + + // Wire the collapsible workspace-root row once. Clicking it toggles the + // whole tree; the chevron rotates via the `.expanded` class. + const rootRow = document.getElementById('workspace-root'); + const treeEl = document.getElementById('file-tree'); + if (rootRow && treeEl && !this._workspaceRootToggleAttached) { + this._workspaceRootToggleAttached = true; + rootRow.addEventListener('click', () => { + const expanded = rootRow.classList.toggle('expanded'); + treeEl.style.display = expanded ? '' : 'none'; + }); + } + + if (workspaceFolder) { + // Must match sidebar.css's `#workspace-folder { display: flex; ... }` — + // 'block' would override the flex column layout that stretches + // .file-tree's empty space to fill the rest of the panel. + workspaceFolder.style.display = 'flex'; + } + + if (noWorkspace) { + noWorkspace.style.display = 'none'; + } + + if (fileTreeElement && fileTree) { + if (!this._fileTreeClickHandlerAttached) { + fileTreeElement.addEventListener('click', this._fileTreeClickHandler); + this._fileTreeClickHandlerAttached = true; + } + this.renderFileTree(fileTree, fileTreeElement); + this.refreshGitStatus(); + } + } + + /** + * Fetch git status for the workspace and (re)apply decorations to the tree. + * Safe to call repeatedly; resolves quietly when there is no repo/git. + */ + async refreshGitStatus() { + if (!window.api || typeof window.api.invoke !== 'function') return; + try { + const res = await window.api.invoke('get-git-status'); + const files = (res && res.files) || {}; + this._gitFiles = files; + + // Roll up: mark every ancestor folder of a changed file. + const dirs = new Set(); + for (const filePath of Object.keys(files)) { + let dir = filePath.replace(/[/\\][^/\\]*$/, ''); + while (dir && dir.length > 1) { + if (dirs.has(dir)) break; + dirs.add(dir); + const parent = dir.replace(/[/\\][^/\\]*$/, ''); + if (parent === dir) break; + dir = parent; + } + } + this._gitDirs = dirs; + + this.redecorateVisibleTree(); + } catch { + // Ignore — decorations are best-effort. + } + } + + /** Re-apply git decorations to every currently rendered tree row. */ + redecorateVisibleTree() { + const container = document.getElementById('file-tree'); + if (!container) return; + container.querySelectorAll('.file-tree-item').forEach((el) => { + const item = this._fileTreeNodeData.get(el); + if (item) this.applyGitDecoration(el, item); + }); + } + + /** + * Apply (or clear) the git decoration for a single tree row. Idempotent. + * @param {Element} el - The .file-tree-item element + * @param {Object} item - The tree node { path, type } + */ + applyGitDecoration(el, item) { + const badge = el.querySelector('.git-badge'); + // Reset prior decoration. + el.removeAttribute('data-git'); + if (badge) { badge.textContent = ''; badge.className = 'git-badge'; } + + if (item.type === 'directory') { + if (this._gitDirs.has(item.path) && badge) { + el.setAttribute('data-git', 'dir'); + badge.classList.add('git-dot'); + } + return; + } + + const code = this._gitFiles[item.path]; + if (code && badge) { + el.setAttribute('data-git', code); + badge.textContent = code; + } + } + + _fileTreeClickHandler(event) { + const container = document.getElementById('file-tree'); + if (!container) return; + + const itemElement = event.target && event.target.closest + ? event.target.closest('.file-tree-item') + : null; + if (!itemElement) return; + + const item = this._fileTreeNodeData.get(itemElement); + if (!item) return; + + if (item.type === 'directory') { + const isExpanded = itemElement.getAttribute('data-expanded') === 'true'; + + if (!isExpanded) { + const childContainer = document.createElement('div'); + childContainer.setAttribute('data-parent-path', item.path); + itemElement.parentNode.insertBefore(childContainer, itemElement.nextSibling); + + // Lazy load directory contents if not already loaded + if (!item.children) { + this.loadDirectoryContents(item, childContainer, itemElement); + } else { + this.renderFileTree(item.children, childContainer, (parseInt(itemElement.getAttribute('data-level') || '0', 10) + 1)); + } + + itemElement.classList.add('expanded'); + itemElement.setAttribute('data-expanded', 'true'); + } else { + const nextSibling = itemElement.nextSibling; + if (nextSibling && nextSibling.getAttribute && nextSibling.getAttribute('data-parent-path') === item.path) { + nextSibling.remove(); + } + itemElement.classList.remove('expanded'); + itemElement.setAttribute('data-expanded', 'false'); + } + + return; + } + + if (item.type === 'file') { + this.readFileFromTree(item.path).then(tabId => { + if (!tabId) return; + if (this._selectedFileTreeItem) { + this._selectedFileTreeItem.classList.remove('selected'); + } + itemElement.classList.add('selected'); + this._selectedFileTreeItem = itemElement; + }); + } + } + + /** + * Load directory contents on demand (lazy loading) + * @param {Object} item - Directory item + * @param {Element} container - Container to render into + * @param {Element} itemElement - The directory element + */ + async loadDirectoryContents(item, container, itemElement) { + const level = parseInt(itemElement.getAttribute('data-level') || '0', 10) + 1; + + // Show loading indicator + const loadingDiv = document.createElement('div'); + loadingDiv.style.marginLeft = (level * 16) + 'px'; + loadingDiv.style.color = '#7d8590'; + loadingDiv.style.fontSize = '11px'; + loadingDiv.style.padding = '2px 8px'; + loadingDiv.textContent = 'Loading...'; + container.appendChild(loadingDiv); + + try { + const result = await window.api.invoke('get-directory-contents', item.path); + + // Remove loading indicator + loadingDiv.remove(); + + if (result.success) { + // Store children in the item + item.children = result.contents; + + // Render the children (rows are git-decorated as they are built) + this.renderFileTree(result.contents, container, level); + + itemElement.classList.add('expanded'); + itemElement.setAttribute('data-expanded', 'true'); + } else { + // Show error + loadingDiv.textContent = 'Error loading directory'; + loadingDiv.style.color = '#f85149'; + } + } catch (error) { + console.error(`[loadDirectoryContents] Failed to load "${item.path}": ${error.message}`); + loadingDiv.textContent = `Error loading "${item.name}": ${error.message}`; + loadingDiv.style.color = '#f85149'; + } + } + + /** + * Render file tree + * @param {Array} tree - File tree structure + * @param {Element} container - Container element + * @param {number} level - Nesting level + */ + renderFileTree(tree, container = null, level = 0) { + if (!container) { + container = document.getElementById('file-tree'); + } + + if (!container) return; + + if (level === 0) { + container.textContent = ''; + this._selectedFileTreeItem = null; + } + + const CHUNK_SIZE = 250; + const raf = (typeof requestAnimationFrame === 'function') + ? requestAnimationFrame + : (cb) => setTimeout(cb, 0); + const renderChunk = (startIndex) => { + const fragment = document.createDocumentFragment(); + + for (let i = startIndex; i < Math.min(startIndex + CHUNK_SIZE, tree.length); i++) { + const item = tree[i]; + const itemElement = document.createElement('div'); + itemElement.className = 'file-tree-item'; + itemElement.setAttribute('data-path', item.path); + itemElement.setAttribute('data-type', item.type); + itemElement.setAttribute('data-name', item.name); + itemElement.setAttribute('data-level', String(level)); + + this._fileTreeNodeData.set(itemElement, item); + + // Indent guide lines, one per ancestor level (VSCode style). + const guides = ''.repeat(level); + + if (item.type === 'directory') { + itemElement.setAttribute('data-expanded', 'false'); + itemElement.innerHTML = ` + ${guides} + ${escapeHtml(item.name)} + + `; + } else { + const fileIcon = this.getFileIcon(item.name); + itemElement.innerHTML = ` + ${guides}${fileIcon} + ${escapeHtml(item.name)} + + `; + } + + this.applyGitDecoration(itemElement, item); + fragment.appendChild(itemElement); + } + + container.appendChild(fragment); + + if (startIndex + CHUNK_SIZE < tree.length) { + raf(() => renderChunk(startIndex + CHUNK_SIZE)); + } + }; + + renderChunk(0); + } + + /** + * Get file icon based on extension + * @param {string} filename - Filename + * @returns {string} - File icon markup or emoji + */ + getFileIcon(filename) { + if (typeof window !== 'undefined' && typeof window.getFileIcon === 'function') { + return window.getFileIcon(filename); + } + + const ext = filename.split('.').pop().toLowerCase(); + const fallbackIconMap = { + js: '🟨', + ts: '🔷', + html: '🟧', + css: '🎨', + json: '📋', + md: '📝', + py: '🐍', + cpp: '⚙️', + c: '⚙️', + cc: '⚙️', + cxx: '⚙️', + h: '📄', + hpp: '📄', + hh: '📄', + hxx: '📄', + java: '☕', + php: '🐘', + rb: '💎', + go: '🐹', + rs: '🦀', + sh: '📜' + }; + + return fallbackIconMap[ext] || '📄'; + } + + /** + * Format file size for display + * @param {number} bytes - File size in bytes + * @returns {string} - Formatted file size + */ + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + } + + /** + * Get current workspace path + * @returns {string|null} - Current workspace path + */ + getCurrentWorkspacePath() { + return this.currentWorkspacePath; + } +} + +if (typeof window !== 'undefined') { + window.FileOperationsManager = FileOperationsManager; +} +if (typeof module !== 'undefined' && module.exports) { + module.exports = FileOperationsManager; +} +})(); diff --git a/src/renderer/managers/MonacoEditorManager.js b/src/renderer/managers/MonacoEditorManager.js index b9199f1..23cb8e8 100644 --- a/src/renderer/managers/MonacoEditorManager.js +++ b/src/renderer/managers/MonacoEditorManager.js @@ -1,604 +1,604 @@ -;(function() { -/** - * Monaco Editor Manager - Handles Monaco editor integration and functionality - */ - -const detectFileType = (typeof window !== 'undefined' && window.detectFileType) || (typeof require === 'function' ? require('../utils/fileTypeUtils').detectFileType : null); - -class MonacoEditorManager { - constructor() { - this.editor = null; - this.lineCounter = document.getElementById('lineCounter'); - this.currentFileType = 'Plain Text'; - this.currentFilePath = null; - this.editorContainer = document.getElementById('editor'); - this.initializationPromise = this.init(); - - // Toggle in DevTools console: window.__CTRACE_DEBUG_SUGGEST__ = true - // (kept false by default to avoid noisy logs) - if (typeof window !== 'undefined' && window.__CTRACE_DEBUG_SUGGEST__ === undefined) { - window.__CTRACE_DEBUG_SUGGEST__ = false; - } - } - - installSuggestWidgetDebug() { - try { - if (!window || !document) return; - - const shouldDebug = () => Boolean(window.__CTRACE_DEBUG_SUGGEST__); - let lastLogAt = 0; - - const ensureDebugStyle = (enabled) => { - const id = 'ctrace-suggest-debug-style'; - const existing = document.getElementById(id); - if (!enabled) { - if (existing) existing.remove(); - return; - } - if (existing) return; - const style = document.createElement('style'); - style.id = id; - style.textContent = ` - .monaco-editor .suggest-widget { z-index: 99999 !important; } - .monaco-editor .suggest-widget * { outline: 1px solid rgba(0,255,255,0.25) !important; } - .monaco-editor .suggest-widget .monaco-list-row { background: rgba(255,0,0,0.12) !important; } - .monaco-editor .suggest-widget .label-name { background: rgba(0,255,0,0.12) !important; } - .monaco-editor .suggest-widget .label-description { background: rgba(0,0,255,0.10) !important; } - `; - document.head.appendChild(style); - }; - - const dumpEl = (label, el) => { - if (!el) { - console.log(`[suggest-debug] ${label}: `); - return; - } - const cs = window.getComputedStyle(el); - const rect = el.getBoundingClientRect(); - console.log(`[suggest-debug] ${label}`, { - tag: el.tagName, - className: el.className, - textContent: (el.textContent || '').slice(0, 200), - rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height }, - color: cs.color, - backgroundColor: cs.backgroundColor, - opacity: cs.opacity, - visibility: cs.visibility, - display: cs.display, - fontSize: cs.fontSize, - lineHeight: cs.lineHeight, - webkitTextFillColor: cs.webkitTextFillColor, - filter: cs.filter - }); - }; - - const dumpSuggestWidget = () => { - const now = Date.now(); - if (now - lastLogAt < 500) return; - lastLogAt = now; - - ensureDebugStyle(shouldDebug()); - - const widget = document.querySelector('.monaco-editor .suggest-widget'); - if (!widget) return; - - console.group('[suggest-debug] suggest-widget'); - dumpEl('widget', widget); - - const list = widget.querySelector('.monaco-list'); - const rowsContainer = widget.querySelector('.monaco-list-rows'); - dumpEl('list', list); - dumpEl('rowsContainer', rowsContainer); - - // Show a short HTML snippet to detect weird overlays or missing children. - const html = widget.innerHTML || ''; - console.log('[suggest-debug] widget.innerHTML (head):', html.slice(0, 800)); - - // Try to introspect Monaco's suggest controller (best-effort; private API). - try { - const controller = this.editor && this.editor.getContribution - ? this.editor.getContribution('editor.contrib.suggestController') - : null; - if (controller) { - const c = controller; - const completionModel = c._model && (c._model._completionModel || c._model.completionModel || c._model._completionModel); - const items = completionModel && completionModel.items ? completionModel.items : null; - console.log('[suggest-debug] suggestController keys:', Object.keys(c)); - console.log('[suggest-debug] completion items length:', Array.isArray(items) ? items.length : null); - if (Array.isArray(items) && items.length) { - const sample = items.slice(0, 5).map(it => { - const s = it && (it.suggestion || it); - return { - label: s && s.label, - kind: s && s.kind, - detail: s && s.detail - }; - }); - console.log('[suggest-debug] completion sample:', sample); - } - } else { - console.log('[suggest-debug] suggestController: '); - } - } catch (e) { - console.log('[suggest-debug] suggestController introspection failed:', String(e && e.message ? e.message : e)); - } - - // Try to locate rows and label text. - const rows = widget.querySelectorAll('.monaco-list-row'); - console.log(`[suggest-debug] rows: ${rows.length}`); - rows.forEach((row, idx) => { - dumpEl(`row[${idx}]`, row); - const labelName = row.querySelector('.monaco-icon-label .label-name') || row.querySelector('.label-name'); - const labelDesc = row.querySelector('.monaco-icon-label .label-description') || row.querySelector('.label-description'); - dumpEl(`row[${idx}].label-name`, labelName); - dumpEl(`row[${idx}].label-description`, labelDesc); - - // Detect overlays: what element is actually "on top" of the label area? - const probe = (el, probeLabel) => { - if (!el) return; - const r = el.getBoundingClientRect(); - const cx = Math.max(0, r.left + Math.min(r.width / 2, r.width - 1)); - const cy = Math.max(0, r.top + Math.min(r.height / 2, r.height - 1)); - const topEl = document.elementFromPoint(cx, cy); - console.log(`[suggest-debug] ${probeLabel}.elementFromPoint`, { - center: { x: cx, y: cy }, - topTag: topEl ? topEl.tagName : null, - topClass: topEl ? topEl.className : null, - topText: topEl ? (topEl.textContent || '').slice(0, 80) : null - }); - - // Dump child nodes to see if text is in a span and potentially styled differently. - try { - const children = Array.from(el.childNodes || []).map(n => { - if (n.nodeType === Node.TEXT_NODE) { - return { type: 'text', text: (n.textContent || '').slice(0, 80) }; - } - if (n.nodeType === Node.ELEMENT_NODE) { - const cs = window.getComputedStyle(n); - return { - type: 'element', - tag: n.tagName, - className: n.className, - text: (n.textContent || '').slice(0, 80), - color: cs.color, - webkitTextFillColor: cs.webkitTextFillColor, - opacity: cs.opacity, - display: cs.display, - visibility: cs.visibility, - fontSize: cs.fontSize - }; - } - return { type: `node:${n.nodeType}` }; - }); - console.log(`[suggest-debug] ${probeLabel}.childNodes`, children); - } catch (e) { - console.log(`[suggest-debug] ${probeLabel}.childNodes failed`, String(e && e.message ? e.message : e)); - } - }; - - probe(labelName, `row[${idx}].label-name`); - }); - - console.groupEnd(); - }; - - // Observe DOM changes; Monaco toggles/updates this widget dynamically. - const observer = new MutationObserver((mutations) => { - if (!shouldDebug()) return; - for (const m of mutations) { - if (m.type === 'childList' || m.type === 'attributes') { - dumpSuggestWidget(); - break; - } - } - }); - - observer.observe(document.body, { - subtree: true, - childList: true, - attributes: true, - attributeFilter: ['class', 'style'] - }); - - // Expose a manual trigger. - window.__ctraceDumpSuggestWidget = dumpSuggestWidget; - window.__ctraceSuggestDebugStyle = (enabled) => ensureDebugStyle(Boolean(enabled)); - } catch (e) { - console.warn('Failed to install suggest-widget debug:', e); - } - } - - async init() { - console.log('MonacoEditorManager: Starting initialization'); - console.log('MonacoEditorManager: Editor container:', this.editorContainer); - - if (!this.editorContainer) { - console.error('Editor container not found'); - return; - } - - // Wait for Monaco to be loaded (from global window.monaco) - console.log('MonacoEditorManager: Waiting for Monaco...'); - await this.waitForMonaco(); - - if (!window.monaco) { - console.error('Monaco Editor not loaded'); - return; - } - - // Define a theme with explicit suggest-widget colors. In some Electron/CSS setups, - // the suggest dropdown can appear with invisible text unless these are set via theme. - try { - window.monaco.editor.defineTheme('ctrace-dark', { - base: 'vs-dark', - inherit: true, - rules: [], - colors: { - 'editor.background': '#0d1117', - 'editor.foreground': '#f0f6fc', - 'editorLineNumber.foreground': '#6e7681', - 'editorLineNumber.activeForeground': '#f0f6fc', - - // Professional suggest widget theme - 'editorSuggestWidget.background': '#1e1e1e', - 'editorSuggestWidget.foreground': '#cccccc', - 'editorSuggestWidget.border': '#3e3e42', - 'editorSuggestWidget.selectedBackground': '#37373d', - 'editorSuggestWidget.highlightForeground': '#0097fb', - - 'editorHoverWidget.background': '#1e1e1e', - 'editorHoverWidget.foreground': '#cccccc', - 'editorHoverWidget.border': '#3e3e42' - } - }); - } catch (e) { - console.warn('Failed to define Monaco theme:', e); - } - - console.log('MonacoEditorManager: Creating editor instance'); - // Create Monaco Editor instance - this.editor = window.monaco.editor.create(this.editorContainer, { - value: '', - language: 'plaintext', - theme: 'ctrace-dark', - automaticLayout: true, - fontSize: 12, - fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', monospace", - lineHeight: 20, - // VS Code-like suggest widget sizing (supported by Monaco; ignored if unavailable) - suggestFontSize: 13, - suggestLineHeight: 22, - minimap: { - enabled: true, - scale: 1, - showSlider: 'mouseover' - }, - scrollbar: { - verticalScrollbarSize: 10, - horizontalScrollbarSize: 10, - useShadows: false - }, - renderLineHighlight: 'all', - renderWhitespace: 'selection', - cursorBlinking: 'smooth', - cursorSmoothCaretAnimation: 'on', - smoothScrolling: true, - mouseWheelZoom: true, - padding: { - top: 0, - bottom: 0 - }, - bracketPairColorization: { - enabled: true - }, - guides: { - bracketPairs: false, - bracketPairsHorizontal: false, - highlightActiveBracketPair: true, - indentation: true, - highlightActiveIndentation: false - }, - lineNumbers: 'on', - folding: false, - wordWrap: 'off', - wrappingIndent: 'indent', - tabSize: 4, - insertSpaces: true, - detectIndentation: true, - trimAutoWhitespace: true, - formatOnPaste: true, - formatOnType: true, - autoClosingBrackets: 'always', - autoClosingQuotes: 'always', - autoIndent: 'full', - suggestOnTriggerCharacters: true, - acceptSuggestionOnEnter: 'on', - snippetSuggestions: 'none', - quickSuggestions: { - other: true, - comments: false, - strings: false - }, - wordBasedSuggestions: 'matchingDocuments', - wordBasedSuggestionsMode: 'matchingDocuments', - suggest: { - showWords: true, - showSnippets: false, - showClasses: true, - showFunctions: true, - showVariables: true, - showKeywords: true, - localityBonus: true, - shareSuggestSelections: false, - showIcons: true, - maxVisibleSuggestions: 12, - filteredTypes: { 'keyword': false, 'snippet': true }, - preview: false, - showDetails: false - }, - inlineSuggest: { - enabled: false - }, - fixedOverflowWidgets: true - }); - - // Debugging for "blank" autocomplete dropdown. - this.installSuggestWidgetDebug(); - - // Update status bar on cursor position change - this.editor.onDidChangeCursorPosition(() => { - this.updateStatusBar(); - }); - - // Update status bar on selection change - this.editor.onDidChangeCursorSelection(() => { - this.updateStatusBar(); - }); - - // Initialize status bar - this.updateStatusBar(); - - console.log('Monaco Editor initialized successfully'); - console.log('Monaco editor instance:', this.editor); - } - - /** - * Wait for Monaco to be loaded from the global scope - */ - async waitForMonaco() { - return new Promise((resolve) => { - if (window.monaco) { - console.log('Monaco already loaded'); - resolve(); - return; - } - - let timeoutId; - - const cleanup = () => { - clearInterval(checkInterval); - clearTimeout(timeoutId); - window.removeEventListener('monaco-loaded', onMonacoLoaded); - }; - - const onMonacoLoaded = () => { - console.log('Monaco loaded via event'); - cleanup(); - resolve(); - }; - window.addEventListener('monaco-loaded', onMonacoLoaded); - - const checkInterval = setInterval(() => { - if (window.monaco) { - console.log('Monaco loaded via polling'); - cleanup(); - resolve(); - } - }, 100); - - timeoutId = setTimeout(() => { - cleanup(); - console.error('Monaco Editor failed to load within timeout'); - resolve(); - }, 10000); - }); - } - - /** - * Update status bar with cursor position - */ - updateStatusBar() { - if (!this.editor || !this.lineCounter) return; - - const position = this.editor.getPosition(); - const selection = this.editor.getSelection(); - - if (!position) return; - - const line = position.lineNumber; - const col = position.column; - - if (selection && !selection.isEmpty()) { - const model = this.editor.getModel(); - if (model) { - const selectedText = model.getValueInRange(selection); - const selectedLines = selectedText.split('\n').length; - this.lineCounter.textContent = `Ln ${line}, Col ${col} (${selectedText.length} chars, ${selectedLines} lines selected)`; - } - } else { - this.lineCounter.textContent = `Ln ${line}, Col ${col}`; - } - } - - /** - * Jump to specific line in editor - * @param {number} lineNumber - Line number to jump to - */ - jumpToLine(lineNumber) { - if (!this.editor) { - console.error('Editor not initialized'); - return; - } - - const model = this.editor.getModel(); - if (!model) return; - - const totalLines = model.getLineCount(); - if (lineNumber > totalLines) { - console.warn(`Line number ${lineNumber} exceeds file length (${totalLines} lines)`); - lineNumber = totalLines; - } - - // Set cursor position - this.editor.setPosition({ lineNumber, column: 1 }); - - // Scroll to line and center it - this.editor.revealLineInCenter(lineNumber); - - // Focus editor - this.editor.focus(); - - console.log(`Jumped to line ${lineNumber}`); - } - - /** - * Toggle word wrap - */ - toggleWordWrap() { - if (!this.editor || !window.monaco) return; - - const currentWrap = this.editor.getOption(window.monaco.editor.EditorOption.wordWrap); - const newWrap = currentWrap === 'off' ? 'on' : 'off'; - this.editor.updateOptions({ wordWrap: newWrap }); - } - - /** - * Get editor content - * @returns {string} - Current editor content - */ - getContent() { - if (!this.editor) return ''; - const model = this.editor.getModel(); - return model ? model.getValue() : ''; - } - - /** - * Set editor content - * @param {string} content - Content to set - */ - async setContent(content) { - console.log('MonacoEditorManager.setContent called with content length:', content ? content.length : 0); - await this.initializationPromise; - console.log('MonacoEditorManager.setContent: Initialization complete, editor:', this.editor); - - if (!this.editor) { - console.error('MonacoEditorManager.setContent: Editor not initialized'); - return; - } - - const model = this.editor.getModel(); - console.log('MonacoEditorManager.setContent: Got model:', model); - - if (model) { - model.setValue(content || ''); - console.log('MonacoEditorManager.setContent: Content set successfully'); - this.updateStatusBar(); - this.editor.trigger('', 'editor.unfoldAll'); - - // Force layout update in case container was hidden when editor was created - setTimeout(() => { - if (this.editor) { - this.editor.layout(); - console.log('MonacoEditorManager.setContent: Layout updated'); - } - }, 100); - } else { - console.error('MonacoEditorManager.setContent: No model available'); - } - } - - /** - * Set file type and update language - * @param {string} filename - The filename to detect type from - */ - async setFileType(filename) { - await this.initializationPromise; - if (!this.editor) return; - - this.currentFileType = detectFileType(filename); - this.currentFilePath = filename; - - // Map file types to Monaco languages - const languageMap = { - 'C': 'c', - 'C++': 'cpp', - 'C/C++ Header': 'cpp', - 'JavaScript': 'javascript', - 'TypeScript': 'typescript', - 'Python': 'python', - 'Java': 'java', - 'JSON': 'json', - 'HTML': 'html', - 'CSS': 'css', - 'Markdown': 'markdown', - 'XML': 'xml', - 'YAML': 'yaml', - 'Shell Script': 'shell', - 'SQL': 'sql', - 'Makefile': 'makefile', - 'Plain Text': 'plaintext' - }; - - const monacoLanguage = languageMap[this.currentFileType] || 'plaintext'; - - const model = this.editor.getModel(); - if (model && window.monaco) { - window.monaco.editor.setModelLanguage(model, monacoLanguage); - } - - console.log(`Set language to ${monacoLanguage} for file type ${this.currentFileType}`); - } - - /** - * Show find/replace widget - */ - showFindDialog() { - if (!this.editor) return; - this.editor.trigger('', 'actions.find'); - } - - /** - * Focus the editor - */ - focus() { - if (this.editor) { - this.editor.focus(); - } - } - - /** - * Dispose the editor - */ - dispose() { - if (this.editor) { - this.editor.dispose(); - this.editor = null; - } - } - - /** - * Get Monaco editor instance (for advanced operations) - * @returns {monaco.editor.IStandaloneCodeEditor} Monaco editor instance - */ - getMonacoInstance() { - return this.editor; - } -} - -if (typeof window !== 'undefined') { - window.MonacoEditorManager = MonacoEditorManager; -} -if (typeof module !== 'undefined' && module.exports) { - module.exports = MonacoEditorManager; -} -})(); +;(function() { +/** + * Monaco Editor Manager - Handles Monaco editor integration and functionality + */ + +const detectFileType = (typeof window !== 'undefined' && window.detectFileType) || (typeof require === 'function' ? require('../utils/fileTypeUtils').detectFileType : null); + +class MonacoEditorManager { + constructor() { + this.editor = null; + this.lineCounter = document.getElementById('lineCounter'); + this.currentFileType = 'Plain Text'; + this.currentFilePath = null; + this.editorContainer = document.getElementById('editor'); + this.initializationPromise = this.init(); + + // Toggle in DevTools console: window.__CTRACE_DEBUG_SUGGEST__ = true + // (kept false by default to avoid noisy logs) + if (typeof window !== 'undefined' && window.__CTRACE_DEBUG_SUGGEST__ === undefined) { + window.__CTRACE_DEBUG_SUGGEST__ = false; + } + } + + installSuggestWidgetDebug() { + try { + if (!window || !document) return; + + const shouldDebug = () => Boolean(window.__CTRACE_DEBUG_SUGGEST__); + let lastLogAt = 0; + + const ensureDebugStyle = (enabled) => { + const id = 'ctrace-suggest-debug-style'; + const existing = document.getElementById(id); + if (!enabled) { + if (existing) existing.remove(); + return; + } + if (existing) return; + const style = document.createElement('style'); + style.id = id; + style.textContent = ` + .monaco-editor .suggest-widget { z-index: 99999 !important; } + .monaco-editor .suggest-widget * { outline: 1px solid rgba(0,255,255,0.25) !important; } + .monaco-editor .suggest-widget .monaco-list-row { background: rgba(255,0,0,0.12) !important; } + .monaco-editor .suggest-widget .label-name { background: rgba(0,255,0,0.12) !important; } + .monaco-editor .suggest-widget .label-description { background: rgba(0,0,255,0.10) !important; } + `; + document.head.appendChild(style); + }; + + const dumpEl = (label, el) => { + if (!el) { + console.log(`[suggest-debug] ${label}: `); + return; + } + const cs = window.getComputedStyle(el); + const rect = el.getBoundingClientRect(); + console.log(`[suggest-debug] ${label}`, { + tag: el.tagName, + className: el.className, + textContent: (el.textContent || '').slice(0, 200), + rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height }, + color: cs.color, + backgroundColor: cs.backgroundColor, + opacity: cs.opacity, + visibility: cs.visibility, + display: cs.display, + fontSize: cs.fontSize, + lineHeight: cs.lineHeight, + webkitTextFillColor: cs.webkitTextFillColor, + filter: cs.filter + }); + }; + + const dumpSuggestWidget = () => { + const now = Date.now(); + if (now - lastLogAt < 500) return; + lastLogAt = now; + + ensureDebugStyle(shouldDebug()); + + const widget = document.querySelector('.monaco-editor .suggest-widget'); + if (!widget) return; + + console.group('[suggest-debug] suggest-widget'); + dumpEl('widget', widget); + + const list = widget.querySelector('.monaco-list'); + const rowsContainer = widget.querySelector('.monaco-list-rows'); + dumpEl('list', list); + dumpEl('rowsContainer', rowsContainer); + + // Show a short HTML snippet to detect weird overlays or missing children. + const html = widget.innerHTML || ''; + console.log('[suggest-debug] widget.innerHTML (head):', html.slice(0, 800)); + + // Try to introspect Monaco's suggest controller (best-effort; private API). + try { + const controller = this.editor && this.editor.getContribution + ? this.editor.getContribution('editor.contrib.suggestController') + : null; + if (controller) { + const c = controller; + const completionModel = c._model && (c._model._completionModel || c._model.completionModel || c._model._completionModel); + const items = completionModel && completionModel.items ? completionModel.items : null; + console.log('[suggest-debug] suggestController keys:', Object.keys(c)); + console.log('[suggest-debug] completion items length:', Array.isArray(items) ? items.length : null); + if (Array.isArray(items) && items.length) { + const sample = items.slice(0, 5).map(it => { + const s = it && (it.suggestion || it); + return { + label: s && s.label, + kind: s && s.kind, + detail: s && s.detail + }; + }); + console.log('[suggest-debug] completion sample:', sample); + } + } else { + console.log('[suggest-debug] suggestController: '); + } + } catch (e) { + console.log('[suggest-debug] suggestController introspection failed:', String(e && e.message ? e.message : e)); + } + + // Try to locate rows and label text. + const rows = widget.querySelectorAll('.monaco-list-row'); + console.log(`[suggest-debug] rows: ${rows.length}`); + rows.forEach((row, idx) => { + dumpEl(`row[${idx}]`, row); + const labelName = row.querySelector('.monaco-icon-label .label-name') || row.querySelector('.label-name'); + const labelDesc = row.querySelector('.monaco-icon-label .label-description') || row.querySelector('.label-description'); + dumpEl(`row[${idx}].label-name`, labelName); + dumpEl(`row[${idx}].label-description`, labelDesc); + + // Detect overlays: what element is actually "on top" of the label area? + const probe = (el, probeLabel) => { + if (!el) return; + const r = el.getBoundingClientRect(); + const cx = Math.max(0, r.left + Math.min(r.width / 2, r.width - 1)); + const cy = Math.max(0, r.top + Math.min(r.height / 2, r.height - 1)); + const topEl = document.elementFromPoint(cx, cy); + console.log(`[suggest-debug] ${probeLabel}.elementFromPoint`, { + center: { x: cx, y: cy }, + topTag: topEl ? topEl.tagName : null, + topClass: topEl ? topEl.className : null, + topText: topEl ? (topEl.textContent || '').slice(0, 80) : null + }); + + // Dump child nodes to see if text is in a span and potentially styled differently. + try { + const children = Array.from(el.childNodes || []).map(n => { + if (n.nodeType === Node.TEXT_NODE) { + return { type: 'text', text: (n.textContent || '').slice(0, 80) }; + } + if (n.nodeType === Node.ELEMENT_NODE) { + const cs = window.getComputedStyle(n); + return { + type: 'element', + tag: n.tagName, + className: n.className, + text: (n.textContent || '').slice(0, 80), + color: cs.color, + webkitTextFillColor: cs.webkitTextFillColor, + opacity: cs.opacity, + display: cs.display, + visibility: cs.visibility, + fontSize: cs.fontSize + }; + } + return { type: `node:${n.nodeType}` }; + }); + console.log(`[suggest-debug] ${probeLabel}.childNodes`, children); + } catch (e) { + console.log(`[suggest-debug] ${probeLabel}.childNodes failed`, String(e && e.message ? e.message : e)); + } + }; + + probe(labelName, `row[${idx}].label-name`); + }); + + console.groupEnd(); + }; + + // Observe DOM changes; Monaco toggles/updates this widget dynamically. + const observer = new MutationObserver((mutations) => { + if (!shouldDebug()) return; + for (const m of mutations) { + if (m.type === 'childList' || m.type === 'attributes') { + dumpSuggestWidget(); + break; + } + } + }); + + observer.observe(document.body, { + subtree: true, + childList: true, + attributes: true, + attributeFilter: ['class', 'style'] + }); + + // Expose a manual trigger. + window.__ctraceDumpSuggestWidget = dumpSuggestWidget; + window.__ctraceSuggestDebugStyle = (enabled) => ensureDebugStyle(Boolean(enabled)); + } catch (e) { + console.warn('Failed to install suggest-widget debug:', e); + } + } + + async init() { + console.log('MonacoEditorManager: Starting initialization'); + console.log('MonacoEditorManager: Editor container:', this.editorContainer); + + if (!this.editorContainer) { + console.error('Editor container not found'); + return; + } + + // Wait for Monaco to be loaded (from global window.monaco) + console.log('MonacoEditorManager: Waiting for Monaco...'); + await this.waitForMonaco(); + + if (!window.monaco) { + console.error('Monaco Editor not loaded'); + return; + } + + // Define a theme with explicit suggest-widget colors. In some Electron/CSS setups, + // the suggest dropdown can appear with invisible text unless these are set via theme. + try { + window.monaco.editor.defineTheme('ctrace-dark', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': '#0d1117', + 'editor.foreground': '#f0f6fc', + 'editorLineNumber.foreground': '#6e7681', + 'editorLineNumber.activeForeground': '#f0f6fc', + + // Professional suggest widget theme + 'editorSuggestWidget.background': '#1e1e1e', + 'editorSuggestWidget.foreground': '#cccccc', + 'editorSuggestWidget.border': '#3e3e42', + 'editorSuggestWidget.selectedBackground': '#37373d', + 'editorSuggestWidget.highlightForeground': '#0097fb', + + 'editorHoverWidget.background': '#1e1e1e', + 'editorHoverWidget.foreground': '#cccccc', + 'editorHoverWidget.border': '#3e3e42' + } + }); + } catch (e) { + console.warn('Failed to define Monaco theme:', e); + } + + console.log('MonacoEditorManager: Creating editor instance'); + // Create Monaco Editor instance + this.editor = window.monaco.editor.create(this.editorContainer, { + value: '', + language: 'plaintext', + theme: 'ctrace-dark', + automaticLayout: true, + fontSize: 12, + fontFamily: "'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', monospace", + lineHeight: 20, + // VS Code-like suggest widget sizing (supported by Monaco; ignored if unavailable) + suggestFontSize: 13, + suggestLineHeight: 22, + minimap: { + enabled: true, + scale: 1, + showSlider: 'mouseover' + }, + scrollbar: { + verticalScrollbarSize: 10, + horizontalScrollbarSize: 10, + useShadows: false + }, + renderLineHighlight: 'all', + renderWhitespace: 'selection', + cursorBlinking: 'smooth', + cursorSmoothCaretAnimation: 'on', + smoothScrolling: true, + mouseWheelZoom: true, + padding: { + top: 0, + bottom: 0 + }, + bracketPairColorization: { + enabled: true + }, + guides: { + bracketPairs: false, + bracketPairsHorizontal: false, + highlightActiveBracketPair: true, + indentation: true, + highlightActiveIndentation: false + }, + lineNumbers: 'on', + folding: false, + wordWrap: 'off', + wrappingIndent: 'indent', + tabSize: 4, + insertSpaces: true, + detectIndentation: true, + trimAutoWhitespace: true, + formatOnPaste: true, + formatOnType: true, + autoClosingBrackets: 'always', + autoClosingQuotes: 'always', + autoIndent: 'full', + suggestOnTriggerCharacters: true, + acceptSuggestionOnEnter: 'on', + snippetSuggestions: 'none', + quickSuggestions: { + other: true, + comments: false, + strings: false + }, + wordBasedSuggestions: 'matchingDocuments', + wordBasedSuggestionsMode: 'matchingDocuments', + suggest: { + showWords: true, + showSnippets: false, + showClasses: true, + showFunctions: true, + showVariables: true, + showKeywords: true, + localityBonus: true, + shareSuggestSelections: false, + showIcons: true, + maxVisibleSuggestions: 12, + filteredTypes: { 'keyword': false, 'snippet': true }, + preview: false, + showDetails: false + }, + inlineSuggest: { + enabled: false + }, + fixedOverflowWidgets: true + }); + + // Debugging for "blank" autocomplete dropdown. + this.installSuggestWidgetDebug(); + + // Update status bar on cursor position change + this.editor.onDidChangeCursorPosition(() => { + this.updateStatusBar(); + }); + + // Update status bar on selection change + this.editor.onDidChangeCursorSelection(() => { + this.updateStatusBar(); + }); + + // Initialize status bar + this.updateStatusBar(); + + console.log('Monaco Editor initialized successfully'); + console.log('Monaco editor instance:', this.editor); + } + + /** + * Wait for Monaco to be loaded from the global scope + */ + async waitForMonaco() { + return new Promise((resolve) => { + if (window.monaco) { + console.log('Monaco already loaded'); + resolve(); + return; + } + + let timeoutId; + + const cleanup = () => { + clearInterval(checkInterval); + clearTimeout(timeoutId); + window.removeEventListener('monaco-loaded', onMonacoLoaded); + }; + + const onMonacoLoaded = () => { + console.log('Monaco loaded via event'); + cleanup(); + resolve(); + }; + window.addEventListener('monaco-loaded', onMonacoLoaded); + + const checkInterval = setInterval(() => { + if (window.monaco) { + console.log('Monaco loaded via polling'); + cleanup(); + resolve(); + } + }, 100); + + timeoutId = setTimeout(() => { + cleanup(); + console.error('Monaco Editor failed to load within timeout'); + resolve(); + }, 10000); + }); + } + + /** + * Update status bar with cursor position + */ + updateStatusBar() { + if (!this.editor || !this.lineCounter) return; + + const position = this.editor.getPosition(); + const selection = this.editor.getSelection(); + + if (!position) return; + + const line = position.lineNumber; + const col = position.column; + + if (selection && !selection.isEmpty()) { + const model = this.editor.getModel(); + if (model) { + const selectedText = model.getValueInRange(selection); + const selectedLines = selectedText.split('\n').length; + this.lineCounter.textContent = `Ln ${line}, Col ${col} (${selectedText.length} chars, ${selectedLines} lines selected)`; + } + } else { + this.lineCounter.textContent = `Ln ${line}, Col ${col}`; + } + } + + /** + * Jump to specific line in editor + * @param {number} lineNumber - Line number to jump to + */ + jumpToLine(lineNumber) { + if (!this.editor) { + console.error('Editor not initialized'); + return; + } + + const model = this.editor.getModel(); + if (!model) return; + + const totalLines = model.getLineCount(); + if (lineNumber > totalLines) { + console.warn(`Line number ${lineNumber} exceeds file length (${totalLines} lines)`); + lineNumber = totalLines; + } + + // Set cursor position + this.editor.setPosition({ lineNumber, column: 1 }); + + // Scroll to line and center it + this.editor.revealLineInCenter(lineNumber); + + // Focus editor + this.editor.focus(); + + console.log(`Jumped to line ${lineNumber}`); + } + + /** + * Toggle word wrap + */ + toggleWordWrap() { + if (!this.editor || !window.monaco) return; + + const currentWrap = this.editor.getOption(window.monaco.editor.EditorOption.wordWrap); + const newWrap = currentWrap === 'off' ? 'on' : 'off'; + this.editor.updateOptions({ wordWrap: newWrap }); + } + + /** + * Get editor content + * @returns {string} - Current editor content + */ + getContent() { + if (!this.editor) return ''; + const model = this.editor.getModel(); + return model ? model.getValue() : ''; + } + + /** + * Set editor content + * @param {string} content - Content to set + */ + async setContent(content) { + console.log('MonacoEditorManager.setContent called with content length:', content ? content.length : 0); + await this.initializationPromise; + console.log('MonacoEditorManager.setContent: Initialization complete, editor:', this.editor); + + if (!this.editor) { + console.error('MonacoEditorManager.setContent: Editor not initialized'); + return; + } + + const model = this.editor.getModel(); + console.log('MonacoEditorManager.setContent: Got model:', model); + + if (model) { + model.setValue(content || ''); + console.log('MonacoEditorManager.setContent: Content set successfully'); + this.updateStatusBar(); + this.editor.trigger('', 'editor.unfoldAll'); + + // Force layout update in case container was hidden when editor was created + setTimeout(() => { + if (this.editor) { + this.editor.layout(); + console.log('MonacoEditorManager.setContent: Layout updated'); + } + }, 100); + } else { + console.error('MonacoEditorManager.setContent: No model available'); + } + } + + /** + * Set file type and update language + * @param {string} filename - The filename to detect type from + */ + async setFileType(filename) { + await this.initializationPromise; + if (!this.editor) return; + + this.currentFileType = detectFileType(filename); + this.currentFilePath = filename; + + // Map file types to Monaco languages + const languageMap = { + 'C': 'c', + 'C++': 'cpp', + 'C/C++ Header': 'cpp', + 'JavaScript': 'javascript', + 'TypeScript': 'typescript', + 'Python': 'python', + 'Java': 'java', + 'JSON': 'json', + 'HTML': 'html', + 'CSS': 'css', + 'Markdown': 'markdown', + 'XML': 'xml', + 'YAML': 'yaml', + 'Shell Script': 'shell', + 'SQL': 'sql', + 'Makefile': 'makefile', + 'Plain Text': 'plaintext' + }; + + const monacoLanguage = languageMap[this.currentFileType] || 'plaintext'; + + const model = this.editor.getModel(); + if (model && window.monaco) { + window.monaco.editor.setModelLanguage(model, monacoLanguage); + } + + console.log(`Set language to ${monacoLanguage} for file type ${this.currentFileType}`); + } + + /** + * Show find/replace widget + */ + showFindDialog() { + if (!this.editor) return; + this.editor.trigger('', 'actions.find'); + } + + /** + * Focus the editor + */ + focus() { + if (this.editor) { + this.editor.focus(); + } + } + + /** + * Dispose the editor + */ + dispose() { + if (this.editor) { + this.editor.dispose(); + this.editor = null; + } + } + + /** + * Get Monaco editor instance (for advanced operations) + * @returns {monaco.editor.IStandaloneCodeEditor} Monaco editor instance + */ + getMonacoInstance() { + return this.editor; + } +} + +if (typeof window !== 'undefined') { + window.MonacoEditorManager = MonacoEditorManager; +} +if (typeof module !== 'undefined' && module.exports) { + module.exports = MonacoEditorManager; +} +})(); diff --git a/src/renderer/managers/NotificationManager.js b/src/renderer/managers/NotificationManager.js index 5cf2950..a00690f 100644 --- a/src/renderer/managers/NotificationManager.js +++ b/src/renderer/managers/NotificationManager.js @@ -1,316 +1,316 @@ -;(function() { -/** - * Notification Manager - Handles user notifications and messages - */ -class NotificationManager { - constructor() { - this.notifications = []; - } - - /** - * Show notification - * @param {string} message - Message to display - * @param {string} type - Notification type (success, error, info, warning) - * @param {number} duration - Duration in milliseconds (default 4000) - */ - showNotification(message, type = 'info', duration = 4000, options = {}) { - // Automatically dismiss all previous notifications when a new one appears - this.clearAll(); - - const colors = { - success: '#238636', - error: '#f85149', - info: '#1f6feb', - warning: '#d29922' - }; - - const icons = { - success: '✓', - error: '✗', - warning: '⚠', - info: 'ℹ' - }; - - const showSpinner = Boolean(options && options.showSpinner); - - const notification = document.createElement('div'); - notification.innerHTML = ` -
        - ${showSpinner ? '' : ''} - - ${escapeHtml(message)} - -
        - - `; - - document.body.appendChild(notification); - this.notifications.push(notification); - - let timeoutId = null; - - const dismiss = () => { - if (timeoutId) { - clearTimeout(timeoutId); - timeoutId = null; - } - if (notification.parentNode) { - notification.remove(); - } - this.notifications = this.notifications.filter(n => n !== notification); - }; - - const messageEl = notification.querySelector('.ctrace-notification-message'); - const update = (nextMessage) => { - if (messageEl) { - messageEl.textContent = String(nextMessage); - } - }; - - const closeBtn = notification.querySelector('.ctrace-notification-close'); - if (closeBtn) { - closeBtn.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - dismiss(); - }); - } - - if (typeof duration === 'number' && duration > 0) { - timeoutId = setTimeout(dismiss, duration); - } - - return { dismiss, update, element: notification }; - } - - /** - * Show a non-dismissed loading notification. - * @param {string} message - Message to display - * @returns {{dismiss: Function, update: Function, element: HTMLElement}} handle - */ - showLoading(message) { - return this.showNotification(message, 'info', 0, { showSpinner: true }); - } - - /** - * Show success notification - * @param {string} message - Message to display - */ - showSuccess(message) { - this.showNotification(message, 'success'); - } - - /** - * Show error notification - * @param {string} message - Message to display - */ - showError(message) { - this.showNotification(message, 'error'); - } - - /** - * Show warning notification - * @param {string} message - Message to display - */ - showWarning(message) { - this.showNotification(message, 'warning'); - } - - /** - * Show info notification - * @param {string} message - Message to display - */ - showInfo(message) { - this.showNotification(message, 'info'); - } - - /** - * Clear all notifications - */ - clearAll() { - this.notifications.forEach(notification => { - if (notification.parentNode) { - notification.remove(); - } - }); - this.notifications = []; - } - - /** - * Show encoding warning dialog - * @returns {Promise} - User choice ('yes' or 'no') - */ - showEncodingWarningDialog() { - return new Promise((resolve) => { - const overlay = document.createElement('div'); - overlay.style.cssText = ` - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - `; - - const dialog = document.createElement('div'); - dialog.style.cssText = ` - background: #1c2128; - border: 1px solid #30363d; - border-radius: 6px; - padding: 24px; - max-width: 500px; - color: #f0f6fc; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; - `; - - dialog.innerHTML = ` -

        ⚠️ Non-UTF8 File Detected

        -

        The file contains non UTF8 characters it may cause improper display. Open still?

        -
        - - -
        - `; - - overlay.appendChild(dialog); - document.body.appendChild(overlay); - - const noBtn = dialog.querySelector('#no-btn'); - const yesBtn = dialog.querySelector('#yes-btn'); - - noBtn.addEventListener('click', () => { - document.body.removeChild(overlay); - resolve('no'); - }); - - yesBtn.addEventListener('click', () => { - document.body.removeChild(overlay); - resolve('yes'); - }); - - // Close on escape key - const handleKeyDown = (e) => { - if (e.key === 'Escape') { - document.body.removeChild(overlay); - document.removeEventListener('keydown', handleKeyDown); - resolve('no'); - } - }; - document.addEventListener('keydown', handleKeyDown); - }); - } - - /** - * Show large file warning dialog - * @param {string} fileName - File name - * @param {string} sizeStr - Human-readable file size - * @returns {Promise<'open'|'cancel'>} - */ - showLargeFileWarningDialog(fileName, sizeStr) { - return new Promise((resolve) => { - const overlay = document.createElement('div'); - overlay.style.cssText = ` - position: fixed; - top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.6); - display: flex; align-items: center; justify-content: center; - z-index: 10000; - `; - - const dialog = document.createElement('div'); - dialog.style.cssText = ` - background: #1c2128; - border: 1px solid #30363d; - border-radius: 6px; - padding: 24px; - max-width: 500px; - color: #f0f6fc; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; - `; - - dialog.innerHTML = ` -

        ⚠️ Large File

        -

        ${fileName} is ${sizeStr}. Only the first 1 MB will be loaded to prevent performance issues.

        -

        Opening very large files may slow down the editor.

        -
        - - -
        - `; - - overlay.appendChild(dialog); - document.body.appendChild(overlay); - - const cleanup = () => { - if (overlay.parentNode) document.body.removeChild(overlay); - document.removeEventListener('keydown', handleKeyDown); - }; - - const handleKeyDown = (e) => { - if (e.key === 'Escape') { cleanup(); resolve('cancel'); } - }; - document.addEventListener('keydown', handleKeyDown); - - dialog.querySelector('#large-cancel-btn').addEventListener('click', () => { cleanup(); resolve('cancel'); }); - dialog.querySelector('#large-open-btn').addEventListener('click', () => { cleanup(); resolve('open'); }); - }); - } - - /** - * Show confirmation dialog - * @param {string} message - Message to display - * @param {string} title - Dialog title - * @returns {Promise} - User confirmation - */ - showConfirmDialog(message, title = 'Confirm') { - return new Promise((resolve) => { - const result = confirm(`${title}: ${message}`); - resolve(result); - }); - } -} - -if (typeof window !== 'undefined') { - window.NotificationManager = NotificationManager; -} -if (typeof module !== 'undefined' && module.exports) { - module.exports = NotificationManager; -} +;(function() { +/** + * Notification Manager - Handles user notifications and messages + */ +class NotificationManager { + constructor() { + this.notifications = []; + } + + /** + * Show notification + * @param {string} message - Message to display + * @param {string} type - Notification type (success, error, info, warning) + * @param {number} duration - Duration in milliseconds (default 4000) + */ + showNotification(message, type = 'info', duration = 4000, options = {}) { + // Automatically dismiss all previous notifications when a new one appears + this.clearAll(); + + const colors = { + success: '#238636', + error: '#f85149', + info: '#1f6feb', + warning: '#d29922' + }; + + const icons = { + success: '✓', + error: '✗', + warning: '⚠', + info: 'ℹ' + }; + + const showSpinner = Boolean(options && options.showSpinner); + + const notification = document.createElement('div'); + notification.innerHTML = ` +
        + ${showSpinner ? '' : ''} + + ${escapeHtml(message)} + +
        + + `; + + document.body.appendChild(notification); + this.notifications.push(notification); + + let timeoutId = null; + + const dismiss = () => { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + if (notification.parentNode) { + notification.remove(); + } + this.notifications = this.notifications.filter(n => n !== notification); + }; + + const messageEl = notification.querySelector('.ctrace-notification-message'); + const update = (nextMessage) => { + if (messageEl) { + messageEl.textContent = String(nextMessage); + } + }; + + const closeBtn = notification.querySelector('.ctrace-notification-close'); + if (closeBtn) { + closeBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + dismiss(); + }); + } + + if (typeof duration === 'number' && duration > 0) { + timeoutId = setTimeout(dismiss, duration); + } + + return { dismiss, update, element: notification }; + } + + /** + * Show a non-dismissed loading notification. + * @param {string} message - Message to display + * @returns {{dismiss: Function, update: Function, element: HTMLElement}} handle + */ + showLoading(message) { + return this.showNotification(message, 'info', 0, { showSpinner: true }); + } + + /** + * Show success notification + * @param {string} message - Message to display + */ + showSuccess(message) { + this.showNotification(message, 'success'); + } + + /** + * Show error notification + * @param {string} message - Message to display + */ + showError(message) { + this.showNotification(message, 'error'); + } + + /** + * Show warning notification + * @param {string} message - Message to display + */ + showWarning(message) { + this.showNotification(message, 'warning'); + } + + /** + * Show info notification + * @param {string} message - Message to display + */ + showInfo(message) { + this.showNotification(message, 'info'); + } + + /** + * Clear all notifications + */ + clearAll() { + this.notifications.forEach(notification => { + if (notification.parentNode) { + notification.remove(); + } + }); + this.notifications = []; + } + + /** + * Show encoding warning dialog + * @returns {Promise} - User choice ('yes' or 'no') + */ + showEncodingWarningDialog() { + return new Promise((resolve) => { + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + `; + + const dialog = document.createElement('div'); + dialog.style.cssText = ` + background: #1c2128; + border: 1px solid #30363d; + border-radius: 6px; + padding: 24px; + max-width: 500px; + color: #f0f6fc; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; + `; + + dialog.innerHTML = ` +

        ⚠️ Non-UTF8 File Detected

        +

        The file contains non UTF8 characters it may cause improper display. Open still?

        +
        + + +
        + `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const noBtn = dialog.querySelector('#no-btn'); + const yesBtn = dialog.querySelector('#yes-btn'); + + noBtn.addEventListener('click', () => { + document.body.removeChild(overlay); + resolve('no'); + }); + + yesBtn.addEventListener('click', () => { + document.body.removeChild(overlay); + resolve('yes'); + }); + + // Close on escape key + const handleKeyDown = (e) => { + if (e.key === 'Escape') { + document.body.removeChild(overlay); + document.removeEventListener('keydown', handleKeyDown); + resolve('no'); + } + }; + document.addEventListener('keydown', handleKeyDown); + }); + } + + /** + * Show large file warning dialog + * @param {string} fileName - File name + * @param {string} sizeStr - Human-readable file size + * @returns {Promise<'open'|'cancel'>} + */ + showLargeFileWarningDialog(fileName, sizeStr) { + return new Promise((resolve) => { + const overlay = document.createElement('div'); + overlay.style.cssText = ` + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; align-items: center; justify-content: center; + z-index: 10000; + `; + + const dialog = document.createElement('div'); + dialog.style.cssText = ` + background: #1c2128; + border: 1px solid #30363d; + border-radius: 6px; + padding: 24px; + max-width: 500px; + color: #f0f6fc; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif; + `; + + dialog.innerHTML = ` +

        ⚠️ Large File

        +

        ${fileName} is ${sizeStr}. Only the first 1 MB will be loaded to prevent performance issues.

        +

        Opening very large files may slow down the editor.

        +
        + + +
        + `; + + overlay.appendChild(dialog); + document.body.appendChild(overlay); + + const cleanup = () => { + if (overlay.parentNode) document.body.removeChild(overlay); + document.removeEventListener('keydown', handleKeyDown); + }; + + const handleKeyDown = (e) => { + if (e.key === 'Escape') { cleanup(); resolve('cancel'); } + }; + document.addEventListener('keydown', handleKeyDown); + + dialog.querySelector('#large-cancel-btn').addEventListener('click', () => { cleanup(); resolve('cancel'); }); + dialog.querySelector('#large-open-btn').addEventListener('click', () => { cleanup(); resolve('open'); }); + }); + } + + /** + * Show confirmation dialog + * @param {string} message - Message to display + * @param {string} title - Dialog title + * @returns {Promise} - User confirmation + */ + showConfirmDialog(message, title = 'Confirm') { + return new Promise((resolve) => { + const result = confirm(`${title}: ${message}`); + resolve(result); + }); + } +} + +if (typeof window !== 'undefined') { + window.NotificationManager = NotificationManager; +} +if (typeof module !== 'undefined' && module.exports) { + module.exports = NotificationManager; +} })(); \ No newline at end of file diff --git a/src/renderer/managers/SearchManager.js b/src/renderer/managers/SearchManager.js index 41723ff..06774cc 100644 --- a/src/renderer/managers/SearchManager.js +++ b/src/renderer/managers/SearchManager.js @@ -1,469 +1,469 @@ -;(function() { -/** - * Search Manager - Handles search functionality - */ -class SearchManager { - constructor(editorManager, notificationManager) { - this.editorManager = editorManager; - this.notificationManager = notificationManager; - this.currentSearchMatches = []; - this.currentMatchIndex = -1; - this.searchTimeout = null; - this.init(); - } - - init() { - this.setupSearchWidget(); - this.setupGoToLineDialog(); - this.setupSidebarSearch(); - this.setupResultsDelegation(); - } - - /** - * Single delegated click handler for the results list. Result rows carry - * their target in data attributes instead of inline onclick handlers, so no - * file path ever ends up inside a JavaScript string in the DOM. - */ - setupResultsDelegation() { - const searchResults = document.getElementById('search-results'); - if (!searchResults) return; - searchResults.addEventListener('click', (e) => { - const target = e.target.closest('[data-toggle-target], [data-result-path]'); - if (!target || !searchResults.contains(target)) return; - - if (target.dataset.toggleTarget) { - this.toggleSearchGroup(target, target.dataset.toggleTarget); - return; - } - - e.stopPropagation(); - this.openSearchResult(target.dataset.resultPath, Number(target.dataset.resultLine)); - }); - } - - /** - * Setup search widget functionality - */ - setupSearchWidget() { - const widgetSearchInput = document.getElementById('widget-search-input'); - if (widgetSearchInput) { - widgetSearchInput.addEventListener('input', (e) => { - this.performLiveSearch(e.target.value); - }); - } - } - - /** - * Setup go to line dialog - */ - setupGoToLineDialog() { - // Event handlers will be attached by the UI controller - } - - /** - * Setup sidebar search - */ - setupSidebarSearch() { - const searchInput = document.getElementById('sidebar-search-input'); - if (searchInput) { - searchInput.addEventListener('input', (e) => { - const searchTerm = e.target.value.trim(); - - if (this.searchTimeout) { - clearTimeout(this.searchTimeout); - } - - if (searchTerm.length >= 2 && this.currentWorkspacePath) { - this.searchTimeout = setTimeout(() => { - this.performWorkspaceSearch(searchTerm); - }, 300); - } else { - this.clearSearchResults(); - } - }); - } - } - - /** - * Show find dialog - */ - showFindDialog() { - const widget = document.getElementById('search-widget'); - const input = document.getElementById('widget-search-input'); - widget.classList.add('visible'); - input.focus(); - input.select(); - } - - /** - * Close search widget - */ - closeSearchWidget() { - const widget = document.getElementById('search-widget'); - widget.classList.remove('visible'); - this.clearSearchHighlights(); - this.currentSearchMatches = []; - this.currentMatchIndex = -1; - this.updateSearchResults(); - } - - /** - * Perform live search in current document - * @param {string} searchText - Text to search for - */ - performLiveSearch(searchText) { - this.clearSearchHighlights(); - this.currentSearchMatches = []; - this.currentMatchIndex = -1; - - if (!searchText.trim()) { - this.updateSearchResults(); - return; - } - - const text = this.editorManager.getContent(); - - if (!text) { - this.updateSearchResults(); - return; - } - - try { - const regex = new RegExp(searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); - let match; - - while ((match = regex.exec(text)) !== null) { - this.currentSearchMatches.push({ - start: match.index, - end: match.index + match[0].length, - text: match[0] - }); - - if (match.index === regex.lastIndex) { - regex.lastIndex++; - } - } - - if (this.currentSearchMatches.length > 0) { - this.currentMatchIndex = 0; - this.highlightAllMatches(); - } - } catch (e) { - console.warn('Invalid search regex:', e); - } - - this.updateSearchResults(); - } - - /** - * Search next match - */ - searchNext() { - if (this.currentSearchMatches.length === 0) return; - - this.currentMatchIndex = (this.currentMatchIndex + 1) % this.currentSearchMatches.length; - this.highlightAllMatches(); - this.focusOnCurrentMatch(); - this.updateSearchResults(); - } - - /** - * Search previous match - */ - searchPrev() { - if (this.currentSearchMatches.length === 0) return; - - this.currentMatchIndex = this.currentMatchIndex <= 0 - ? this.currentSearchMatches.length - 1 - : this.currentMatchIndex - 1; - this.highlightAllMatches(); - this.focusOnCurrentMatch(); - this.updateSearchResults(); - } - - /** - * Update search results display - */ - updateSearchResults() { - const resultsText = document.getElementById('search-results-text'); - const prevBtn = document.getElementById('search-prev'); - const nextBtn = document.getElementById('search-next'); - - if (!resultsText || !prevBtn || !nextBtn) return; - - if (this.currentSearchMatches.length === 0) { - resultsText.textContent = 'No results'; - prevBtn.disabled = true; - nextBtn.disabled = true; - } else { - resultsText.textContent = `${this.currentMatchIndex + 1} of ${this.currentSearchMatches.length}`; - prevBtn.disabled = false; - nextBtn.disabled = false; - } - } - - /** - * Focus on current match - */ - focusOnCurrentMatch() { - if (this.currentMatchIndex >= 0 && this.currentMatchIndex < this.currentSearchMatches.length) { - const match = this.currentSearchMatches[this.currentMatchIndex]; - const monacoEditor = this.editorManager.getMonacoInstance ? this.editorManager.getMonacoInstance() : null; - - if (monacoEditor) { - const model = monacoEditor.getModel(); - if (model) { - const startPos = model.getPositionAt(match.start); - const endPos = model.getPositionAt(match.end); - const range = { - startLineNumber: startPos.lineNumber, - startColumn: startPos.column, - endLineNumber: endPos.lineNumber, - endColumn: endPos.column - }; - monacoEditor.focus(); - monacoEditor.setSelection(range); - monacoEditor.revealRangeInCenter(range); - return; - } - } - - const editor = this.editorManager.editor; - editor.focus(); - editor.setSelectionRange(match.start, match.end); - const lines = editor.value.substring(0, match.start).split('\n'); - const lineNumber = lines.length; - const approximateLineHeight = 20; - const targetScrollTop = (lineNumber - 5) * approximateLineHeight; - editor.scrollTop = Math.max(0, targetScrollTop); - setTimeout(() => { - if (editor.setSelectionRange) { - editor.setSelectionRange(match.start, match.end); - } - }, 10); - } - } - - /** - * Highlight all matches - */ - highlightAllMatches() { - // For now, we'll keep it simple and just highlight the current match when navigating - } - - /** - * Clear search highlights - */ - clearSearchHighlights() { - // Clear any visual highlights if implemented - } - - /** - * Show go to line dialog - */ - showGoToLineDialog() { - const dialog = document.getElementById('goto-dialog'); - const input = document.getElementById('goto-input'); - - const text = this.editorManager.getContent(); - if (text) { - const lineCount = text.split('\n').length; - input.max = lineCount; - } - - dialog.classList.add('visible'); - input.focus(); - input.select(); - } - - /** - * Close go to line dialog - */ - closeGoToLineDialog() { - const dialog = document.getElementById('goto-dialog'); - dialog.classList.remove('visible'); - } - - /** - * Perform go to line - */ - performGoToLine() { - const input = document.getElementById('goto-input'); - const lineNumber = parseInt(input.value, 10); - - if (!lineNumber || lineNumber < 1) { - this.notificationManager.showError('Please enter a valid line number'); - return; - } - - const text = this.editorManager.getContent(); - if (text) { - const lines = text.split('\n'); - - if (lineNumber > lines.length) { - this.notificationManager.showError(`Line ${lineNumber} does not exist. Maximum line is ${lines.length}`); - return; - } - - this.editorManager.jumpToLine(lineNumber); - this.notificationManager.showSuccess(`Jumped to line ${lineNumber}`); - } else { - this.notificationManager.showError('No file is currently open'); - } - - this.closeGoToLineDialog(); - } - - /** - * Perform workspace search - * @param {string} searchTerm - Term to search for - */ - async performWorkspaceSearch(searchTerm) { - if (!this.currentWorkspacePath) { - this.displaySearchResults([], searchTerm); - return; - } - - try { - const searchResults = document.getElementById('search-results'); - if (searchResults) { - searchResults.innerHTML = '
        Searching...
        '; - } - - const result = await window.api.invoke('search-in-files', searchTerm, this.currentWorkspacePath); - - if (result.success) { - this.displaySearchResults(result.results, searchTerm); - } else { - const searchResults = document.getElementById('search-results'); - if (searchResults) { - searchResults.innerHTML = `
        Search failed: ${this.escapeHtml(result.error || 'Unknown error')}
        `; - } - } - } catch (error) { - const searchResults = document.getElementById('search-results'); - if (searchResults) { - searchResults.innerHTML = `
        Search error: ${this.escapeHtml(error.message || 'Unknown error')}
        `; - } - } - } - - /** - * Display search results in sidebar - * @param {Array} results - Search results - * @param {string} searchTerm - Search term - */ - displaySearchResults(results, searchTerm) { - const searchResults = document.getElementById('search-results'); - if (!searchResults) return; - - if (results.length === 0) { - searchResults.innerHTML = '
        No results found
        '; - return; - } - - const groupedResults = {}; - results.forEach((result) => { - if (!groupedResults[result.file]) { - groupedResults[result.file] = []; - } - groupedResults[result.file].push(result); - }); - - let html = '
        '; - - Object.keys(groupedResults).forEach((file, fileIndex) => { - const fileResults = groupedResults[file]; - const fileName = file.split(/[/\\]/).pop(); - const relativePath = file.replace(this.currentWorkspacePath, '').replace(/^[/\\]/, ''); - const pathAttr = this.escapeHtml(file); - const matchesId = `sr-matches-${fileIndex}`; - // Collapse groups with more than one match; single matches stay visible - const collapsed = fileResults.length > 1; - - html += '
        '; - html += '
        '; - html += `'; - html += ``; - html += `${fileResults.length}`; - html += '
        '; - html += `
        ${this.escapeHtml(relativePath)}
        `; - html += `'; - html += '
        '; - }); - - html += '
        '; - searchResults.innerHTML = html; - } - - /** - * Toggle collapsed/expanded state of a file's match list. - * @param {HTMLElement} btn - The toggle button element - * @param {string} matchesId - The id of the matches container to toggle - */ - toggleSearchGroup(btn, matchesId) { - const matches = document.getElementById(matchesId); - if (!matches) return; - const isCollapsed = matches.style.display === 'none'; - matches.style.display = isCollapsed ? '' : 'none'; - btn.setAttribute('aria-expanded', isCollapsed ? 'true' : 'false'); - const icon = btn.querySelector('.search-result-toggle-icon'); - if (icon) icon.textContent = isCollapsed ? '▼' : '▶'; - } - - /** - * Clear search results - */ - clearSearchResults() { - const searchResults = document.getElementById('search-results'); - if (searchResults) { - searchResults.innerHTML = ''; - } - } - - escapeHtml(text) { - return window.escapeHtml(text); - } - - /** - * Open search result (to be implemented by parent controller) - * @param {string} filePath - File path - * @param {number} lineNumber - Line number - */ - async openSearchResult(filePath, lineNumber) { - // This will be implemented by the main controller - console.log('Open search result requested:', filePath, 'at line', lineNumber); - } - - /** - * Set current workspace path - * @param {string} workspacePath - Workspace path - */ - setWorkspacePath(workspacePath) { - this.currentWorkspacePath = workspacePath; - } -} - -if (typeof window !== 'undefined') { - window.SearchManager = SearchManager; -} -if (typeof module !== 'undefined' && module.exports) { - module.exports = SearchManager; -} -})(); +;(function() { +/** + * Search Manager - Handles search functionality + */ +class SearchManager { + constructor(editorManager, notificationManager) { + this.editorManager = editorManager; + this.notificationManager = notificationManager; + this.currentSearchMatches = []; + this.currentMatchIndex = -1; + this.searchTimeout = null; + this.init(); + } + + init() { + this.setupSearchWidget(); + this.setupGoToLineDialog(); + this.setupSidebarSearch(); + this.setupResultsDelegation(); + } + + /** + * Single delegated click handler for the results list. Result rows carry + * their target in data attributes instead of inline onclick handlers, so no + * file path ever ends up inside a JavaScript string in the DOM. + */ + setupResultsDelegation() { + const searchResults = document.getElementById('search-results'); + if (!searchResults) return; + searchResults.addEventListener('click', (e) => { + const target = e.target.closest('[data-toggle-target], [data-result-path]'); + if (!target || !searchResults.contains(target)) return; + + if (target.dataset.toggleTarget) { + this.toggleSearchGroup(target, target.dataset.toggleTarget); + return; + } + + e.stopPropagation(); + this.openSearchResult(target.dataset.resultPath, Number(target.dataset.resultLine)); + }); + } + + /** + * Setup search widget functionality + */ + setupSearchWidget() { + const widgetSearchInput = document.getElementById('widget-search-input'); + if (widgetSearchInput) { + widgetSearchInput.addEventListener('input', (e) => { + this.performLiveSearch(e.target.value); + }); + } + } + + /** + * Setup go to line dialog + */ + setupGoToLineDialog() { + // Event handlers will be attached by the UI controller + } + + /** + * Setup sidebar search + */ + setupSidebarSearch() { + const searchInput = document.getElementById('sidebar-search-input'); + if (searchInput) { + searchInput.addEventListener('input', (e) => { + const searchTerm = e.target.value.trim(); + + if (this.searchTimeout) { + clearTimeout(this.searchTimeout); + } + + if (searchTerm.length >= 2 && this.currentWorkspacePath) { + this.searchTimeout = setTimeout(() => { + this.performWorkspaceSearch(searchTerm); + }, 300); + } else { + this.clearSearchResults(); + } + }); + } + } + + /** + * Show find dialog + */ + showFindDialog() { + const widget = document.getElementById('search-widget'); + const input = document.getElementById('widget-search-input'); + widget.classList.add('visible'); + input.focus(); + input.select(); + } + + /** + * Close search widget + */ + closeSearchWidget() { + const widget = document.getElementById('search-widget'); + widget.classList.remove('visible'); + this.clearSearchHighlights(); + this.currentSearchMatches = []; + this.currentMatchIndex = -1; + this.updateSearchResults(); + } + + /** + * Perform live search in current document + * @param {string} searchText - Text to search for + */ + performLiveSearch(searchText) { + this.clearSearchHighlights(); + this.currentSearchMatches = []; + this.currentMatchIndex = -1; + + if (!searchText.trim()) { + this.updateSearchResults(); + return; + } + + const text = this.editorManager.getContent(); + + if (!text) { + this.updateSearchResults(); + return; + } + + try { + const regex = new RegExp(searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'); + let match; + + while ((match = regex.exec(text)) !== null) { + this.currentSearchMatches.push({ + start: match.index, + end: match.index + match[0].length, + text: match[0] + }); + + if (match.index === regex.lastIndex) { + regex.lastIndex++; + } + } + + if (this.currentSearchMatches.length > 0) { + this.currentMatchIndex = 0; + this.highlightAllMatches(); + } + } catch (e) { + console.warn('Invalid search regex:', e); + } + + this.updateSearchResults(); + } + + /** + * Search next match + */ + searchNext() { + if (this.currentSearchMatches.length === 0) return; + + this.currentMatchIndex = (this.currentMatchIndex + 1) % this.currentSearchMatches.length; + this.highlightAllMatches(); + this.focusOnCurrentMatch(); + this.updateSearchResults(); + } + + /** + * Search previous match + */ + searchPrev() { + if (this.currentSearchMatches.length === 0) return; + + this.currentMatchIndex = this.currentMatchIndex <= 0 + ? this.currentSearchMatches.length - 1 + : this.currentMatchIndex - 1; + this.highlightAllMatches(); + this.focusOnCurrentMatch(); + this.updateSearchResults(); + } + + /** + * Update search results display + */ + updateSearchResults() { + const resultsText = document.getElementById('search-results-text'); + const prevBtn = document.getElementById('search-prev'); + const nextBtn = document.getElementById('search-next'); + + if (!resultsText || !prevBtn || !nextBtn) return; + + if (this.currentSearchMatches.length === 0) { + resultsText.textContent = 'No results'; + prevBtn.disabled = true; + nextBtn.disabled = true; + } else { + resultsText.textContent = `${this.currentMatchIndex + 1} of ${this.currentSearchMatches.length}`; + prevBtn.disabled = false; + nextBtn.disabled = false; + } + } + + /** + * Focus on current match + */ + focusOnCurrentMatch() { + if (this.currentMatchIndex >= 0 && this.currentMatchIndex < this.currentSearchMatches.length) { + const match = this.currentSearchMatches[this.currentMatchIndex]; + const monacoEditor = this.editorManager.getMonacoInstance ? this.editorManager.getMonacoInstance() : null; + + if (monacoEditor) { + const model = monacoEditor.getModel(); + if (model) { + const startPos = model.getPositionAt(match.start); + const endPos = model.getPositionAt(match.end); + const range = { + startLineNumber: startPos.lineNumber, + startColumn: startPos.column, + endLineNumber: endPos.lineNumber, + endColumn: endPos.column + }; + monacoEditor.focus(); + monacoEditor.setSelection(range); + monacoEditor.revealRangeInCenter(range); + return; + } + } + + const editor = this.editorManager.editor; + editor.focus(); + editor.setSelectionRange(match.start, match.end); + const lines = editor.value.substring(0, match.start).split('\n'); + const lineNumber = lines.length; + const approximateLineHeight = 20; + const targetScrollTop = (lineNumber - 5) * approximateLineHeight; + editor.scrollTop = Math.max(0, targetScrollTop); + setTimeout(() => { + if (editor.setSelectionRange) { + editor.setSelectionRange(match.start, match.end); + } + }, 10); + } + } + + /** + * Highlight all matches + */ + highlightAllMatches() { + // For now, we'll keep it simple and just highlight the current match when navigating + } + + /** + * Clear search highlights + */ + clearSearchHighlights() { + // Clear any visual highlights if implemented + } + + /** + * Show go to line dialog + */ + showGoToLineDialog() { + const dialog = document.getElementById('goto-dialog'); + const input = document.getElementById('goto-input'); + + const text = this.editorManager.getContent(); + if (text) { + const lineCount = text.split('\n').length; + input.max = lineCount; + } + + dialog.classList.add('visible'); + input.focus(); + input.select(); + } + + /** + * Close go to line dialog + */ + closeGoToLineDialog() { + const dialog = document.getElementById('goto-dialog'); + dialog.classList.remove('visible'); + } + + /** + * Perform go to line + */ + performGoToLine() { + const input = document.getElementById('goto-input'); + const lineNumber = parseInt(input.value, 10); + + if (!lineNumber || lineNumber < 1) { + this.notificationManager.showError('Please enter a valid line number'); + return; + } + + const text = this.editorManager.getContent(); + if (text) { + const lines = text.split('\n'); + + if (lineNumber > lines.length) { + this.notificationManager.showError(`Line ${lineNumber} does not exist. Maximum line is ${lines.length}`); + return; + } + + this.editorManager.jumpToLine(lineNumber); + this.notificationManager.showSuccess(`Jumped to line ${lineNumber}`); + } else { + this.notificationManager.showError('No file is currently open'); + } + + this.closeGoToLineDialog(); + } + + /** + * Perform workspace search + * @param {string} searchTerm - Term to search for + */ + async performWorkspaceSearch(searchTerm) { + if (!this.currentWorkspacePath) { + this.displaySearchResults([], searchTerm); + return; + } + + try { + const searchResults = document.getElementById('search-results'); + if (searchResults) { + searchResults.innerHTML = '
        Searching...
        '; + } + + const result = await window.api.invoke('search-in-files', searchTerm, this.currentWorkspacePath); + + if (result.success) { + this.displaySearchResults(result.results, searchTerm); + } else { + const searchResults = document.getElementById('search-results'); + if (searchResults) { + searchResults.innerHTML = `
        Search failed: ${this.escapeHtml(result.error || 'Unknown error')}
        `; + } + } + } catch (error) { + const searchResults = document.getElementById('search-results'); + if (searchResults) { + searchResults.innerHTML = `
        Search error: ${this.escapeHtml(error.message || 'Unknown error')}
        `; + } + } + } + + /** + * Display search results in sidebar + * @param {Array} results - Search results + * @param {string} searchTerm - Search term + */ + displaySearchResults(results, searchTerm) { + const searchResults = document.getElementById('search-results'); + if (!searchResults) return; + + if (results.length === 0) { + searchResults.innerHTML = '
        No results found
        '; + return; + } + + const groupedResults = {}; + results.forEach((result) => { + if (!groupedResults[result.file]) { + groupedResults[result.file] = []; + } + groupedResults[result.file].push(result); + }); + + let html = '
        '; + + Object.keys(groupedResults).forEach((file, fileIndex) => { + const fileResults = groupedResults[file]; + const fileName = file.split(/[/\\]/).pop(); + const relativePath = file.replace(this.currentWorkspacePath, '').replace(/^[/\\]/, ''); + const pathAttr = this.escapeHtml(file); + const matchesId = `sr-matches-${fileIndex}`; + // Collapse groups with more than one match; single matches stay visible + const collapsed = fileResults.length > 1; + + html += '
        '; + html += '
        '; + html += `'; + html += ``; + html += `${fileResults.length}`; + html += '
        '; + html += `
        ${this.escapeHtml(relativePath)}
        `; + html += `'; + html += '
        '; + }); + + html += '
        '; + searchResults.innerHTML = html; + } + + /** + * Toggle collapsed/expanded state of a file's match list. + * @param {HTMLElement} btn - The toggle button element + * @param {string} matchesId - The id of the matches container to toggle + */ + toggleSearchGroup(btn, matchesId) { + const matches = document.getElementById(matchesId); + if (!matches) return; + const isCollapsed = matches.style.display === 'none'; + matches.style.display = isCollapsed ? '' : 'none'; + btn.setAttribute('aria-expanded', isCollapsed ? 'true' : 'false'); + const icon = btn.querySelector('.search-result-toggle-icon'); + if (icon) icon.textContent = isCollapsed ? '▼' : '▶'; + } + + /** + * Clear search results + */ + clearSearchResults() { + const searchResults = document.getElementById('search-results'); + if (searchResults) { + searchResults.innerHTML = ''; + } + } + + escapeHtml(text) { + return window.escapeHtml(text); + } + + /** + * Open search result (to be implemented by parent controller) + * @param {string} filePath - File path + * @param {number} lineNumber - Line number + */ + async openSearchResult(filePath, lineNumber) { + // This will be implemented by the main controller + console.log('Open search result requested:', filePath, 'at line', lineNumber); + } + + /** + * Set current workspace path + * @param {string} workspacePath - Workspace path + */ + setWorkspacePath(workspacePath) { + this.currentWorkspacePath = workspacePath; + } +} + +if (typeof window !== 'undefined') { + window.SearchManager = SearchManager; +} +if (typeof module !== 'undefined' && module.exports) { + module.exports = SearchManager; +} +})(); diff --git a/src/renderer/managers/StateManager.js b/src/renderer/managers/StateManager.js index b725d76..d53da03 100644 --- a/src/renderer/managers/StateManager.js +++ b/src/renderer/managers/StateManager.js @@ -1,487 +1,487 @@ -;(function() { -/** - * State Manager - Handles application state persistence for work loss prevention - * - * This manager saves and restores the complete application state including: - * - All open tabs with their content - * - Active tab selection - * - Last analysis results - * - Editor state (cursor position, scroll, etc.) - * - * State is automatically saved on various events and restored on application startup. - * - * @class StateManager - * @author CTrace GUI Team - * @version 1.0.0 - */ -class StateManager { - /** - * Creates an instance of StateManager. - * - * @constructor - * @memberof StateManager - * @param {TabManager} tabManager - Tab manager instance - * @param {MonacoEditorManager} editorManager - Editor manager instance - * @param {DiagnosticsManager} diagnosticsManager - Diagnostics manager instance - */ - constructor(tabManager, editorManager, diagnosticsManager) { - this.tabManager = tabManager; - this.editorManager = editorManager; - this.diagnosticsManager = diagnosticsManager; - - /** - * Auto-save interval in milliseconds (default: 30 seconds) - * @type {number} - * @private - */ - this.autoSaveInterval = 30000; - - /** - * Auto-save timer reference - * @type {number|null} - * @private - */ - this.autoSaveTimer = null; - - /** - * Flag to track if state is being saved - * @type {boolean} - * @private - */ - this.isSaving = false; - - /** - * Last saved state timestamp - * @type {Date|null} - * @private - */ - this.lastSaveTime = null; - - /** - * Debounce timer for save operations - * @type {number|null} - * @private - */ - this.saveDebounceTimer = null; - - /** - * Debounce delay in milliseconds - * @type {number} - * @private - */ - this.saveDebounceDelay = 2000; - } - - /** - * Start auto-save mechanism - * Automatically saves state at regular intervals - */ - startAutoSave() { - if (this.autoSaveTimer) { - return; // Already running - } - - console.log('[StateManager] Starting auto-save with interval:', this.autoSaveInterval, 'ms'); - - this.autoSaveTimer = setInterval(async () => { - await this.saveState(); - }, this.autoSaveInterval); - } - - /** - * Stop auto-save mechanism - */ - stopAutoSave() { - if (this.autoSaveTimer) { - clearInterval(this.autoSaveTimer); - this.autoSaveTimer = null; - console.log('[StateManager] Auto-save stopped'); - } - } - - /** - * Save state with debouncing to avoid excessive saves - * @param {boolean} immediate - If true, save immediately without debouncing - */ - async saveStateDebounced(immediate = false) { - if (immediate) { - if (this.saveDebounceTimer) { - clearTimeout(this.saveDebounceTimer); - this.saveDebounceTimer = null; - } - await this.saveState(); - return; - } - - if (this.saveDebounceTimer) { - clearTimeout(this.saveDebounceTimer); - } - - this.saveDebounceTimer = setTimeout(async () => { - await this.saveState(); - this.saveDebounceTimer = null; - }, this.saveDebounceDelay); - } - - /** - * Collect current application state - * @returns {Object} Current application state - * @private - */ - async collectState() { - const state = { - version: '1.0.0', - timestamp: new Date().toISOString(), - tabs: [], - activeTabId: null, - workspacePath: null, - diagnostics: null, - editorState: null - }; - - // Collect all open tabs with their content - if (this.tabManager.openTabs && this.tabManager.openTabs.size > 0) { - // Save current editor content to active tab first - if (this.tabManager.activeTabId) { - const activeTab = this.tabManager.openTabs.get(this.tabManager.activeTabId); - if (activeTab) { - activeTab.content = this.editorManager.getContent(); - } - } - - // Collect all tabs - for (const [tabId, tabData] of this.tabManager.openTabs) { - state.tabs.push({ - tabId: tabId, - fileName: tabData.fileName, - filePath: tabData.filePath, - content: tabData.content, - modified: tabData.modified, - fileInfo: tabData.fileInfo || {} - }); - } - - state.activeTabId = this.tabManager.activeTabId; - } - - // Collect editor state (cursor position, scroll, etc.) - if (this.editorManager.editor) { - const position = this.editorManager.editor.getPosition(); - const scrollTop = this.editorManager.editor.getScrollTop(); - const scrollLeft = this.editorManager.editor.getScrollLeft(); - - state.editorState = { - position: position ? { - lineNumber: position.lineNumber, - column: position.column - } : null, - scrollTop: scrollTop, - scrollLeft: scrollLeft - }; - } - // Collect current workspace path - if (window.uiController && window.uiController.fileOpsManager) { - state.workspacePath = window.uiController.fileOpsManager.getCurrentWorkspacePath(); - } - // Collect last analysis results - if (this.diagnosticsManager.currentDiagnostics) { - state.diagnostics = { - metadata: this.diagnosticsManager.currentMetadata, - functions: this.diagnosticsManager.currentFunctions, - diagnostics: this.diagnosticsManager.currentDiagnostics, - severityFilter: this.diagnosticsManager.currentSeverityFilter - }; - } - - return state; - } - - /** - * Save current application state to disk - * @returns {Promise} Success status - */ - async saveState() { - if (this.isSaving) { - console.log('[StateManager] Save already in progress, skipping'); - return false; - } - - this.isSaving = true; - - try { - const state = await this.collectState(); - - // Always save state, even if there are no tabs (to clear previous state) - console.log('[StateManager] Saving state:', { - tabCount: state.tabs.length, - activeTabId: state.activeTabId, - hasDiagnostics: !!state.diagnostics - }); - - // Save to disk via IPC - const result = await window.api.invoke('save-app-state', state); - - if (result.success) { - this.lastSaveTime = new Date(); - // console.log('[StateManager] State saved successfully at', this.lastSaveTime.toLocaleTimeString()); - return true; - } else { - console.error('[StateManager] Failed to save state:', result.error); - return false; - } - } catch (error) { - console.error('[StateManager] Error saving state:', error); - return false; - } finally { - this.isSaving = false; - } - } - - /** - * Restore application state from disk - * @returns {Promise} Success status - */ - async restoreState() { - try { - console.log('[StateManager] Attempting to restore state...'); - - const result = await window.api.invoke('load-app-state'); - - if (!result.success || !result.state) { - console.log('[StateManager] No saved state found or load failed'); - return false; - } - - const state = result.state; - console.log('[StateManager] Loaded state:', { - version: state.version, - timestamp: state.timestamp, - tabCount: state.tabs?.length || 0, - hasDiagnostics: !!state.diagnostics - }); - - // Restore tabs - if (state.tabs && state.tabs.length > 0) { - console.log('[StateManager] Restoring', state.tabs.length, 'tabs...'); - - // For unmodified tabs that have a file path, refresh content from disk - // so that stale cached content never causes a blank editor after restart. - await Promise.all(state.tabs.map(async (tabData) => { - if (!tabData.modified && tabData.filePath) { - try { - const res = await window.api.invoke('read-file', tabData.filePath); - if (res && res.success && typeof res.content === 'string') { - tabData.content = res.content; - console.log('[StateManager] Refreshed content from disk for:', tabData.fileName); - } - } catch (_) { - // File may no longer exist — keep cached content as fallback - } - } - })); - - // Counter to continue from where we left off - let maxTabId = 0; - for (const tab of state.tabs) { - const match = tab.tabId.match(/tab_(\d+)/); - if (match) { - const num = parseInt(match[1], 10); - if (num > maxTabId) maxTabId = num; - } - } - - // Set the counter so new tabs don't conflict with restored tabs - this.tabManager.tabIdCounter = maxTabId; - - // Restore each tab - for (const tabData of state.tabs) { - // Use the exact tab ID from saved state - const restoredTabId = tabData.tabId; - - // Create the tab using internal method to preserve tab ID - if (this.tabManager.openTabs.size === 0) { - this.tabManager.showEditor(); - } - - // Manually create tab with preserved ID - this.tabManager.openTabs.set(restoredTabId, { - filePath: tabData.filePath, - content: tabData.content, - modified: tabData.modified, - fileName: tabData.fileName, - fileInfo: tabData.fileInfo || {} - }); - - // Create tab element - const tabElement = document.createElement('div'); - tabElement.className = 'tab'; - tabElement.setAttribute('data-tab-id', restoredTabId); - tabElement.setAttribute('data-file-path', tabData.filePath || ''); - - const warningIndicator = (tabData.fileInfo?.encodingWarning || tabData.fileInfo?.isPartial) ? - `⚠️` : ''; - - tabElement.innerHTML = ` -
        ${escapeHtml(tabData.fileName)}${warningIndicator}
        -
        ×
        - `; - - tabElement.querySelector('.tab-close').addEventListener('click', (e) => this.tabManager.closeTab(e, restoredTabId)); - tabElement.addEventListener('click', (e) => { - if (!e.target.classList.contains('tab-close')) { - this.tabManager.switchToTab(restoredTabId); - } - }); - - this.tabManager.tabsContainer.appendChild(tabElement); - - // Mark as modified if it was modified - if (tabData.modified) { - tabElement.classList.add('modified'); - } - } - - // Restore active tab - if (state.activeTabId && this.tabManager.openTabs.has(state.activeTabId)) { - await this.tabManager.switchToTab(state.activeTabId); - - // Restore editor state - if (state.editorState) { - setTimeout(() => { - if (state.editorState.position && this.editorManager.editor) { - this.editorManager.editor.setPosition(state.editorState.position); - this.editorManager.editor.revealPositionInCenter(state.editorState.position); - } - - if (this.editorManager.editor) { - if (typeof state.editorState.scrollTop === 'number') { - this.editorManager.editor.setScrollTop(state.editorState.scrollTop); - } - if (typeof state.editorState.scrollLeft === 'number') { - this.editorManager.editor.setScrollLeft(state.editorState.scrollLeft); - } - } - }, 100); // Small delay to ensure editor is ready - } - } else if (state.tabs.length > 0) { - // If no active tab or active tab not found, switch to first tab - await this.tabManager.switchToTab(state.tabs[0].tabId); - } - } - - // Restore workspace if one was open - if (state.workspacePath) { - console.log('[StateManager] Restoring workspace:', state.workspacePath); - try { - // Re-arm the main-process watcher first: file access is validated - // against the watched root, so it must exist before the tree loads. - const watch = await window.api.invoke('watch-workspace', state.workspacePath); - if (!watch || !watch.success) { - throw new Error((watch && watch.error) || 'Workspace could not be watched'); - } - const result = await window.api.invoke('get-file-tree', state.workspacePath); - if (result.success && window.uiController && window.uiController.fileOpsManager) { - const folderName = state.workspacePath.split(/[\/\\]/).pop(); - window.uiController.fileOpsManager.currentWorkspacePath = state.workspacePath; - window.uiController.fileOpsManager.updateWorkspaceUI(folderName, result.fileTree); - if (window.uiController.searchManager) { - window.uiController.searchManager.setWorkspacePath(state.workspacePath); - } - console.log('[StateManager] Workspace restored successfully'); - } - } catch (error) { - console.error('[StateManager] Failed to restore workspace:', error); - } - } - - // Restore diagnostics - if (state.diagnostics) { - console.log('[StateManager] Restoring diagnostics...'); - this.diagnosticsManager.currentMetadata = state.diagnostics.metadata; - this.diagnosticsManager.currentFunctions = state.diagnostics.functions; - this.diagnosticsManager.currentDiagnostics = state.diagnostics.diagnostics; - this.diagnosticsManager.currentSeverityFilter = state.diagnostics.severityFilter || 'ALL'; - - // Display diagnostics - await this.diagnosticsManager.displayDiagnostics(); - } - - // Async: check each restored tab's file and mark missing ones visually. - // Don't await — let the UI show first, then apply warnings. - if (state.tabs && state.tabs.length > 0) { - this._checkRestoredFilesExist(state.tabs); - } - - console.log('[StateManager] State restored successfully'); - return true; - } catch (error) { - console.error('[StateManager] Error restoring state:', error); - return false; - } - } - - /** - * Async post-restore check: for each tab that has a file path, verify the - * file still exists on disk. Marks missing ones visually via TabManager. - * @param {Array} tabs - Restored tab data array - */ - async _checkRestoredFilesExist(tabs) { - // Run all checks in parallel instead of sequentially to avoid blocking startup. - const checks = tabs - .filter(tabData => tabData.filePath && tabData.tabId) - .map(async (tabData) => { - try { - const res = await window.api.invoke('check-file-exists', tabData.filePath); - if (!res.exists) { - this.tabManager.markTabMissing(tabData.tabId, true); - } - } catch { - // IPC unavailable, skip silently - } - }); - await Promise.all(checks); - } - - /** - * Clear saved state from disk - * @returns {Promise} Success status - */ - async clearState() { - try { - console.log('[StateManager] Clearing saved state...'); - const result = await window.api.invoke('clear-app-state'); - - if (result.success) { - console.log('[StateManager] State cleared successfully'); - return true; - } else { - console.error('[StateManager] Failed to clear state:', result.error); - return false; - } - } catch (error) { - console.error('[StateManager] Error clearing state:', error); - return false; - } - } - - /** - * Get information about the last saved state - * @returns {Promise} State info or null if no state exists - */ - async getStateInfo() { - try { - const result = await window.api.invoke('get-state-info'); - return result.success ? result.info : null; - } catch (error) { - console.error('[StateManager] Error getting state info:', error); - return null; - } - } -} - -window.StateManager = StateManager; -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = StateManager; -} -})(); +;(function() { +/** + * State Manager - Handles application state persistence for work loss prevention + * + * This manager saves and restores the complete application state including: + * - All open tabs with their content + * - Active tab selection + * - Last analysis results + * - Editor state (cursor position, scroll, etc.) + * + * State is automatically saved on various events and restored on application startup. + * + * @class StateManager + * @author CTrace GUI Team + * @version 1.0.0 + */ +class StateManager { + /** + * Creates an instance of StateManager. + * + * @constructor + * @memberof StateManager + * @param {TabManager} tabManager - Tab manager instance + * @param {MonacoEditorManager} editorManager - Editor manager instance + * @param {DiagnosticsManager} diagnosticsManager - Diagnostics manager instance + */ + constructor(tabManager, editorManager, diagnosticsManager) { + this.tabManager = tabManager; + this.editorManager = editorManager; + this.diagnosticsManager = diagnosticsManager; + + /** + * Auto-save interval in milliseconds (default: 30 seconds) + * @type {number} + * @private + */ + this.autoSaveInterval = 30000; + + /** + * Auto-save timer reference + * @type {number|null} + * @private + */ + this.autoSaveTimer = null; + + /** + * Flag to track if state is being saved + * @type {boolean} + * @private + */ + this.isSaving = false; + + /** + * Last saved state timestamp + * @type {Date|null} + * @private + */ + this.lastSaveTime = null; + + /** + * Debounce timer for save operations + * @type {number|null} + * @private + */ + this.saveDebounceTimer = null; + + /** + * Debounce delay in milliseconds + * @type {number} + * @private + */ + this.saveDebounceDelay = 2000; + } + + /** + * Start auto-save mechanism + * Automatically saves state at regular intervals + */ + startAutoSave() { + if (this.autoSaveTimer) { + return; // Already running + } + + console.log('[StateManager] Starting auto-save with interval:', this.autoSaveInterval, 'ms'); + + this.autoSaveTimer = setInterval(async () => { + await this.saveState(); + }, this.autoSaveInterval); + } + + /** + * Stop auto-save mechanism + */ + stopAutoSave() { + if (this.autoSaveTimer) { + clearInterval(this.autoSaveTimer); + this.autoSaveTimer = null; + console.log('[StateManager] Auto-save stopped'); + } + } + + /** + * Save state with debouncing to avoid excessive saves + * @param {boolean} immediate - If true, save immediately without debouncing + */ + async saveStateDebounced(immediate = false) { + if (immediate) { + if (this.saveDebounceTimer) { + clearTimeout(this.saveDebounceTimer); + this.saveDebounceTimer = null; + } + await this.saveState(); + return; + } + + if (this.saveDebounceTimer) { + clearTimeout(this.saveDebounceTimer); + } + + this.saveDebounceTimer = setTimeout(async () => { + await this.saveState(); + this.saveDebounceTimer = null; + }, this.saveDebounceDelay); + } + + /** + * Collect current application state + * @returns {Object} Current application state + * @private + */ + async collectState() { + const state = { + version: '1.0.0', + timestamp: new Date().toISOString(), + tabs: [], + activeTabId: null, + workspacePath: null, + diagnostics: null, + editorState: null + }; + + // Collect all open tabs with their content + if (this.tabManager.openTabs && this.tabManager.openTabs.size > 0) { + // Save current editor content to active tab first + if (this.tabManager.activeTabId) { + const activeTab = this.tabManager.openTabs.get(this.tabManager.activeTabId); + if (activeTab) { + activeTab.content = this.editorManager.getContent(); + } + } + + // Collect all tabs + for (const [tabId, tabData] of this.tabManager.openTabs) { + state.tabs.push({ + tabId: tabId, + fileName: tabData.fileName, + filePath: tabData.filePath, + content: tabData.content, + modified: tabData.modified, + fileInfo: tabData.fileInfo || {} + }); + } + + state.activeTabId = this.tabManager.activeTabId; + } + + // Collect editor state (cursor position, scroll, etc.) + if (this.editorManager.editor) { + const position = this.editorManager.editor.getPosition(); + const scrollTop = this.editorManager.editor.getScrollTop(); + const scrollLeft = this.editorManager.editor.getScrollLeft(); + + state.editorState = { + position: position ? { + lineNumber: position.lineNumber, + column: position.column + } : null, + scrollTop: scrollTop, + scrollLeft: scrollLeft + }; + } + // Collect current workspace path + if (window.uiController && window.uiController.fileOpsManager) { + state.workspacePath = window.uiController.fileOpsManager.getCurrentWorkspacePath(); + } + // Collect last analysis results + if (this.diagnosticsManager.currentDiagnostics) { + state.diagnostics = { + metadata: this.diagnosticsManager.currentMetadata, + functions: this.diagnosticsManager.currentFunctions, + diagnostics: this.diagnosticsManager.currentDiagnostics, + severityFilter: this.diagnosticsManager.currentSeverityFilter + }; + } + + return state; + } + + /** + * Save current application state to disk + * @returns {Promise} Success status + */ + async saveState() { + if (this.isSaving) { + console.log('[StateManager] Save already in progress, skipping'); + return false; + } + + this.isSaving = true; + + try { + const state = await this.collectState(); + + // Always save state, even if there are no tabs (to clear previous state) + console.log('[StateManager] Saving state:', { + tabCount: state.tabs.length, + activeTabId: state.activeTabId, + hasDiagnostics: !!state.diagnostics + }); + + // Save to disk via IPC + const result = await window.api.invoke('save-app-state', state); + + if (result.success) { + this.lastSaveTime = new Date(); + // console.log('[StateManager] State saved successfully at', this.lastSaveTime.toLocaleTimeString()); + return true; + } else { + console.error('[StateManager] Failed to save state:', result.error); + return false; + } + } catch (error) { + console.error('[StateManager] Error saving state:', error); + return false; + } finally { + this.isSaving = false; + } + } + + /** + * Restore application state from disk + * @returns {Promise} Success status + */ + async restoreState() { + try { + console.log('[StateManager] Attempting to restore state...'); + + const result = await window.api.invoke('load-app-state'); + + if (!result.success || !result.state) { + console.log('[StateManager] No saved state found or load failed'); + return false; + } + + const state = result.state; + console.log('[StateManager] Loaded state:', { + version: state.version, + timestamp: state.timestamp, + tabCount: state.tabs?.length || 0, + hasDiagnostics: !!state.diagnostics + }); + + // Restore tabs + if (state.tabs && state.tabs.length > 0) { + console.log('[StateManager] Restoring', state.tabs.length, 'tabs...'); + + // For unmodified tabs that have a file path, refresh content from disk + // so that stale cached content never causes a blank editor after restart. + await Promise.all(state.tabs.map(async (tabData) => { + if (!tabData.modified && tabData.filePath) { + try { + const res = await window.api.invoke('read-file', tabData.filePath); + if (res && res.success && typeof res.content === 'string') { + tabData.content = res.content; + console.log('[StateManager] Refreshed content from disk for:', tabData.fileName); + } + } catch (_) { + // File may no longer exist — keep cached content as fallback + } + } + })); + + // Counter to continue from where we left off + let maxTabId = 0; + for (const tab of state.tabs) { + const match = tab.tabId.match(/tab_(\d+)/); + if (match) { + const num = parseInt(match[1], 10); + if (num > maxTabId) maxTabId = num; + } + } + + // Set the counter so new tabs don't conflict with restored tabs + this.tabManager.tabIdCounter = maxTabId; + + // Restore each tab + for (const tabData of state.tabs) { + // Use the exact tab ID from saved state + const restoredTabId = tabData.tabId; + + // Create the tab using internal method to preserve tab ID + if (this.tabManager.openTabs.size === 0) { + this.tabManager.showEditor(); + } + + // Manually create tab with preserved ID + this.tabManager.openTabs.set(restoredTabId, { + filePath: tabData.filePath, + content: tabData.content, + modified: tabData.modified, + fileName: tabData.fileName, + fileInfo: tabData.fileInfo || {} + }); + + // Create tab element + const tabElement = document.createElement('div'); + tabElement.className = 'tab'; + tabElement.setAttribute('data-tab-id', restoredTabId); + tabElement.setAttribute('data-file-path', tabData.filePath || ''); + + const warningIndicator = (tabData.fileInfo?.encodingWarning || tabData.fileInfo?.isPartial) ? + `⚠️` : ''; + + tabElement.innerHTML = ` +
        ${escapeHtml(tabData.fileName)}${warningIndicator}
        +
        ×
        + `; + + tabElement.querySelector('.tab-close').addEventListener('click', (e) => this.tabManager.closeTab(e, restoredTabId)); + tabElement.addEventListener('click', (e) => { + if (!e.target.classList.contains('tab-close')) { + this.tabManager.switchToTab(restoredTabId); + } + }); + + this.tabManager.tabsContainer.appendChild(tabElement); + + // Mark as modified if it was modified + if (tabData.modified) { + tabElement.classList.add('modified'); + } + } + + // Restore active tab + if (state.activeTabId && this.tabManager.openTabs.has(state.activeTabId)) { + await this.tabManager.switchToTab(state.activeTabId); + + // Restore editor state + if (state.editorState) { + setTimeout(() => { + if (state.editorState.position && this.editorManager.editor) { + this.editorManager.editor.setPosition(state.editorState.position); + this.editorManager.editor.revealPositionInCenter(state.editorState.position); + } + + if (this.editorManager.editor) { + if (typeof state.editorState.scrollTop === 'number') { + this.editorManager.editor.setScrollTop(state.editorState.scrollTop); + } + if (typeof state.editorState.scrollLeft === 'number') { + this.editorManager.editor.setScrollLeft(state.editorState.scrollLeft); + } + } + }, 100); // Small delay to ensure editor is ready + } + } else if (state.tabs.length > 0) { + // If no active tab or active tab not found, switch to first tab + await this.tabManager.switchToTab(state.tabs[0].tabId); + } + } + + // Restore workspace if one was open + if (state.workspacePath) { + console.log('[StateManager] Restoring workspace:', state.workspacePath); + try { + // Re-arm the main-process watcher first: file access is validated + // against the watched root, so it must exist before the tree loads. + const watch = await window.api.invoke('watch-workspace', state.workspacePath); + if (!watch || !watch.success) { + throw new Error((watch && watch.error) || 'Workspace could not be watched'); + } + const result = await window.api.invoke('get-file-tree', state.workspacePath); + if (result.success && window.uiController && window.uiController.fileOpsManager) { + const folderName = state.workspacePath.split(/[\/\\]/).pop(); + window.uiController.fileOpsManager.currentWorkspacePath = state.workspacePath; + window.uiController.fileOpsManager.updateWorkspaceUI(folderName, result.fileTree); + if (window.uiController.searchManager) { + window.uiController.searchManager.setWorkspacePath(state.workspacePath); + } + console.log('[StateManager] Workspace restored successfully'); + } + } catch (error) { + console.error('[StateManager] Failed to restore workspace:', error); + } + } + + // Restore diagnostics + if (state.diagnostics) { + console.log('[StateManager] Restoring diagnostics...'); + this.diagnosticsManager.currentMetadata = state.diagnostics.metadata; + this.diagnosticsManager.currentFunctions = state.diagnostics.functions; + this.diagnosticsManager.currentDiagnostics = state.diagnostics.diagnostics; + this.diagnosticsManager.currentSeverityFilter = state.diagnostics.severityFilter || 'ALL'; + + // Display diagnostics + await this.diagnosticsManager.displayDiagnostics(); + } + + // Async: check each restored tab's file and mark missing ones visually. + // Don't await — let the UI show first, then apply warnings. + if (state.tabs && state.tabs.length > 0) { + this._checkRestoredFilesExist(state.tabs); + } + + console.log('[StateManager] State restored successfully'); + return true; + } catch (error) { + console.error('[StateManager] Error restoring state:', error); + return false; + } + } + + /** + * Async post-restore check: for each tab that has a file path, verify the + * file still exists on disk. Marks missing ones visually via TabManager. + * @param {Array} tabs - Restored tab data array + */ + async _checkRestoredFilesExist(tabs) { + // Run all checks in parallel instead of sequentially to avoid blocking startup. + const checks = tabs + .filter(tabData => tabData.filePath && tabData.tabId) + .map(async (tabData) => { + try { + const res = await window.api.invoke('check-file-exists', tabData.filePath); + if (!res.exists) { + this.tabManager.markTabMissing(tabData.tabId, true); + } + } catch { + // IPC unavailable, skip silently + } + }); + await Promise.all(checks); + } + + /** + * Clear saved state from disk + * @returns {Promise} Success status + */ + async clearState() { + try { + console.log('[StateManager] Clearing saved state...'); + const result = await window.api.invoke('clear-app-state'); + + if (result.success) { + console.log('[StateManager] State cleared successfully'); + return true; + } else { + console.error('[StateManager] Failed to clear state:', result.error); + return false; + } + } catch (error) { + console.error('[StateManager] Error clearing state:', error); + return false; + } + } + + /** + * Get information about the last saved state + * @returns {Promise} State info or null if no state exists + */ + async getStateInfo() { + try { + const result = await window.api.invoke('get-state-info'); + return result.success ? result.info : null; + } catch (error) { + console.error('[StateManager] Error getting state info:', error); + return null; + } + } +} + +window.StateManager = StateManager; +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = StateManager; +} +})(); diff --git a/src/renderer/managers/TabManager.js b/src/renderer/managers/TabManager.js index aba5672..cce0bbc 100644 --- a/src/renderer/managers/TabManager.js +++ b/src/renderer/managers/TabManager.js @@ -1,684 +1,684 @@ -;(function() { -/** - * Tab Manager - Handles all tab operations including creation, switching, and closing. - * - * This manager provides a complete tab interface similar to modern code editors, - * allowing users to work with multiple files simultaneously. It manages tab state, - * coordinates with the editor manager, and handles file modifications tracking. - * - * @class TabManager - * @author CTrace GUI Team - * @version 1.0.0 - * - * @example - * const tabManager = new TabManager(editorManager, notificationManager); - * const tabId = tabManager.createTab('example.js', '/path/to/example.js', 'console.log("Hello")'); - */ -class TabManager { - /** - * Creates an instance of TabManager. - * - * @constructor - * @memberof TabManager - * @param {EditorManager} editorManager - Editor manager instance for content management - * @param {NotificationManager} notificationManager - Notification manager for user feedback - */ - constructor(editorManager, notificationManager) { - /** - * Editor manager instance - * @type {EditorManager} - * @private - */ - this.editorManager = editorManager; - - /** - * Notification manager instance - * @type {NotificationManager} - * @private - */ - this.notificationManager = notificationManager; - - /** - * Counter for generating unique tab IDs - * @type {number} - * @private - */ - this.tabIdCounter = 0; - - /** - * Currently active tab ID - * @type {string|null} - */ - this.activeTabId = null; - - /** - * Set of tab IDs whose backing file is missing from disk - * @type {Set} - */ - this.missingFileTabs = new Set(); - - /** - * Map of open tabs with their data - * @type {Map} - * @private - */ - this.openTabs = new Map(); - - /** - * DOM element containing the tabs - * @type {HTMLElement} - * @private - */ - this.tabsContainer = document.getElementById('tabs-container'); - - // Create inner wrapper for proper scrolling - if (this.tabsContainer && !this.tabsContainer.querySelector('.tabs-inner-wrapper')) { - const wrapper = document.createElement('div'); - wrapper.className = 'tabs-inner-wrapper'; - this.tabsContainer.appendChild(wrapper); - this.tabsInnerWrapper = wrapper; - } else { - this.tabsInnerWrapper = this.tabsContainer.querySelector('.tabs-inner-wrapper'); - } - - /** - * Welcome screen DOM element - * @type {HTMLElement} - * @private - */ - this.welcomeScreen = document.getElementById('welcome-screen'); - - /** - * Editor area DOM element - * @type {HTMLElement} - * @private - */ - this.editorArea = document.getElementById('editor-area'); - - /** - * Optional async callback fired before switching tabs. - * @type {(fromTabId: (string|null), toTabId: string) => Promise|null} - */ - this.onBeforeTabSwitch = null; - - /** - * Optional async callback fired before closing a tab. - * @type {(tabId: string) => Promise|null} - */ - this.onBeforeTabClose = null; - } - - /** - * Create a new tab - * @param {string} fileName - Tab file name - * @param {string} filePath - File path (optional) - * @param {string} content - File content - * @param {Object} fileInfo - File metadata - * @returns {string} - Tab ID - */ - createTab(fileName, filePath = null, content = '', fileInfo = {}) { - // Show editor area if this is the first tab - if (this.openTabs.size === 0) { - this.showEditor(); - } - - const tabId = 'tab_' + (++this.tabIdCounter); - - // Create tab data - this.openTabs.set(tabId, { - filePath: filePath, - content: content, - modified: false, - fileName: fileName, - fileInfo: fileInfo // Store file metadata - }); - - // Create tab element - const tabElement = document.createElement('div'); - tabElement.className = 'tab'; - tabElement.setAttribute('data-tab-id', tabId); - tabElement.setAttribute('data-file-path', filePath || ''); - - // Add warning indicator if file has encoding issues or is partial - const warningIndicator = (fileInfo.encodingWarning || fileInfo.isPartial) ? - `⚠️` : ''; - - tabElement.innerHTML = ` -
        ${escapeHtml(fileName)}${warningIndicator}
        -
        ×
        - `; - - tabElement.querySelector('.tab-close').addEventListener('click', (e) => this.closeTab(e, tabId)); - tabElement.addEventListener('click', (e) => { - if (!e.target.classList.contains('tab-close')) { - this.switchToTab(tabId); - } - }); - - const targetContainer = this.tabsInnerWrapper || this.tabsContainer; - targetContainer.appendChild(tabElement); - return tabId; - } - - /** - * Switch to a specific tab - * @param {string} tabId - Tab ID to switch to - */ - async switchToTab(tabId) { - // Save current tab content FIRST (before auto-save callback) if we have an active tab - if (this.activeTabId && this.openTabs.has(this.activeTabId)) { - const currentTab = this.openTabs.get(this.activeTabId); - currentTab.content = this.editorManager.getContent(); - } - - // Give consumers a chance to flush pending edits (e.g., auto-save) before switching. - if (typeof this.onBeforeTabSwitch === 'function') { - try { - await this.onBeforeTabSwitch(this.activeTabId, tabId); - } catch (err) { - console.warn('TabManager onBeforeTabSwitch failed:', err); - } - } - - // Update active tab - this.activeTabId = tabId; - const newTab = this.openTabs.get(tabId); - - if (newTab) { - // Update editor - await this.editorManager.setContent(newTab.content); - - // Set file type for syntax highlighting - if (newTab.fileName) { - await this.editorManager.setFileType(newTab.fileName); - } - - // Update tab appearance - document.querySelectorAll('.tab').forEach(tab => { - tab.classList.remove('active'); - }); - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - if (tabElement) { - tabElement.classList.add('active'); - } - - // Update file tree selection - if (newTab.filePath) { - document.querySelectorAll('.file-tree-item').forEach(el => el.classList.remove('selected')); - const fileTreeItem = document.querySelector(`[data-file-path="${newTab.filePath}"]`); - if (fileTreeItem) { - fileTreeItem.classList.add('selected'); - } - } - - // Show or hide the missing-file banner based on current flag. - // Also re-check existence asynchronously — file may have been restored. - this._syncMissingBanner(tabId, newTab.filePath); - if (newTab.filePath) { - this._recheckFileExists(tabId, newTab.filePath); - } - - // Emit tab switch event for other components - this.onTabSwitch(newTab); - } - } - - /** - * Close a tab - * @param {Event} event - Click event - * @param {string} tabId - Tab ID to close - */ - closeTab(event, tabId) { - event.stopPropagation(); - - // Fire and forget (UI click handler can't await). - void this.closeTabById(tabId); - } - - /** - * Close a tab by id (non-click workflows) - * @param {string} tabId - Tab ID to close - * @param {boolean} [force=false] - If true, close even if modified - * @returns {boolean} - True if closed, false if user canceled - */ - async closeTabById(tabId, force = false) { - const tab = this.openTabs.get(tabId); - if (!tab) return true; - - // Sync content from editor if closing the active tab - if (tabId === this.activeTabId) { - tab.content = this.editorManager.getContent(); - console.log('[TabManager] Synced content from editor for tab:', tab.fileName, 'modified:', tab.modified); - } - - // Give consumers a chance to flush pending edits (e.g., auto-save) before close confirmation. - if (typeof this.onBeforeTabClose === 'function') { - console.log('[TabManager] Calling onBeforeTabClose for:', tab.fileName); - try { - await this.onBeforeTabClose(tabId); - console.log('[TabManager] onBeforeTabClose completed for:', tab.fileName, 'modified now:', tab.modified); - } catch (err) { - console.warn('TabManager onBeforeTabClose failed:', err); - } - } - - // Re-fetch tab state in case auto-save cleaned it - const updatedTab = this.openTabs.get(tabId); - if (!updatedTab) return true; - - // Check if modified - if (updatedTab.modified && !force) { - console.log('[TabManager] Tab still modified after auto-save, prompting user:', updatedTab.fileName); - const result = confirm(`${updatedTab.fileName} has unsaved changes. Do you want to close it anyway?`); - if (!result) return false; - } - - // Remove tab element - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - if (tabElement) { - tabElement.remove(); - } - - // Remove from data - this.openTabs.delete(tabId); - this.missingFileTabs.delete(tabId); - - // If closing active tab, switch to another tab or show welcome screen - if (this.activeTabId === tabId) { - const remainingTabs = Array.from(this.openTabs.keys()); - if (remainingTabs.length > 0) { - await this.switchToTab(remainingTabs[remainingTabs.length - 1]); - } else { - this.activeTabId = null; - this.showWelcomeScreen(); - } - } - - return true; - } - - /** - * Get a tab by id. - * @param {string} tabId - * @returns {Object|null} - */ - getTab(tabId) { - return this.openTabs.get(tabId) || null; - } - - /** - * Mark tab as modified - * @param {string} tabId - Tab ID - */ - markTabModified(tabId) { - const tab = this.openTabs.get(tabId); - if (tab && !tab.modified) { - tab.modified = true; - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - if (tabElement) { - tabElement.classList.add('modified'); - } - } - } - - /** - * Mark tab as clean (not modified) - * @param {string} tabId - Tab ID - */ - markTabClean(tabId) { - const tab = this.openTabs.get(tabId); - if (tab && tab.modified) { - tab.modified = false; - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - if (tabElement) { - tabElement.classList.remove('modified'); - } - } - } - - /** - * Check if file is already open in a tab - * @param {string} filePath - File path to check - * @returns {string|null} - Tab ID if found, null otherwise - */ - findTabByPath(filePath) { - for (const [tabId, tab] of this.openTabs) { - if (tab.filePath === filePath) { - return tabId; - } - } - return null; - } - - /** - * Get current active tab - * @returns {Object|null} - Active tab data or null - */ - getActiveTab() { - return this.activeTabId ? this.openTabs.get(this.activeTabId) : null; - } - - /** - * Update tab file info (for file operations) - * @param {string} tabId - Tab ID - * @param {string} filePath - New file path - * @param {string} fileName - New file name - */ - updateTabFile(tabId, filePath, fileName) { - const tab = this.openTabs.get(tabId); - if (tab) { - tab.filePath = filePath; - tab.fileName = fileName; - - // Update tab label - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - if (tabElement) { - const labelElement = tabElement.querySelector('.tab-label'); - if (labelElement) { - labelElement.textContent = fileName; - } - tabElement.setAttribute('data-file-path', filePath); - } - } - } - - /** - * Show welcome screen - */ - showWelcomeScreen() { - this.welcomeScreen.style.display = 'flex'; - this.editorArea.style.display = 'none'; - this.tabsContainer.style.display = 'none'; - const banner = document.getElementById('partial-file-banner'); - if (banner) banner.classList.remove('visible'); - } - - /** - * Show editor - */ - showEditor() { - this.welcomeScreen.style.display = 'none'; - this.editorArea.style.display = 'flex'; - this.tabsContainer.style.display = 'flex'; - - // Trigger Monaco layout update after showing editor area. - // Multiple retries cover the case where Monaco hasn't initialized yet. - const triggerLayout = () => { - if (this.editorManager && this.editorManager.editor) { - this.editorManager.editor.layout(); - } - }; - setTimeout(triggerLayout, 50); - setTimeout(triggerLayout, 200); - setTimeout(triggerLayout, 500); - } - - /** - * Handle tab content changes (for modification tracking) - * @param {string} tabId - Tab ID - * @param {string} newContent - New content - */ - handleContentChange(tabId, newContent) { - const tab = this.openTabs.get(tabId); - if (tab && tab.content !== newContent) { - tab.content = newContent; - this.markTabModified(tabId); - } - } - - /** - * Callback for when tab switches (for other components to listen to) - * @param {Object} tabData - Tab data - */ - onTabSwitch(tabData) { - // Update file type in status bar if file type utils are available - if (window.updateFileTypeStatus) { - window.updateFileTypeStatus(tabData.fileName); - } - - // Update file status - this.updateFileStatus(tabData); - } - - /** - * Update file status indicator - * @param {Object} tabData - Tab data - */ - updateFileStatus(tabData) { - const fileStatusElement = document.getElementById('fileStatus'); - if (fileStatusElement && tabData.fileInfo) { - const { fileInfo } = tabData; - let statusText = 'UTF-8'; - let statusStyle = ''; - - if (fileInfo.encodingWarning) { - statusText = '⚠️ Non-UTF8'; - statusStyle = 'color: #f85149; cursor: pointer;'; - fileStatusElement.title = 'File may contain non-UTF8 characters'; - } else if (fileInfo.isPartial) { - const loadedKB = Math.round(fileInfo.loadedSize / 1024); - const totalKB = Math.round(fileInfo.totalSize / 1024); - statusText = `📄 Partial (${loadedKB}KB/${totalKB}KB)`; - statusStyle = 'color: #f0883e; cursor: pointer;'; - fileStatusElement.title = 'Click to load full file'; - fileStatusElement.onclick = () => this.onLoadFullFile(tabData.filePath); - } else { - statusText = 'UTF-8'; - statusStyle = ''; - fileStatusElement.onclick = null; - fileStatusElement.title = ''; - } - - fileStatusElement.textContent = statusText; - fileStatusElement.style.cssText = statusStyle; - } - - // Show or hide the partial-file banner - this._updatePartialBanner(tabData); - } - - _updatePartialBanner(tabData) { - const banner = document.getElementById('partial-file-banner'); - const msg = document.getElementById('partial-file-msg'); - const btn = document.getElementById('partial-load-more-btn'); - if (!banner) return; - - const fileInfo = tabData && tabData.fileInfo; - if (fileInfo && fileInfo.isPartial) { - const loadedMB = (fileInfo.loadedSize / (1024 * 1024)).toFixed(1); - const totalMB = (fileInfo.totalSize / (1024 * 1024)).toFixed(1); - if (msg) msg.textContent = `Showing first ${loadedMB} MB of ${totalMB} MB`; - if (btn) { - btn.disabled = false; - btn.onclick = () => this.onLoadNextChunk(tabData.filePath, fileInfo.loadedSize); - } - banner.classList.add('visible'); - } else { - banner.classList.remove('visible'); - if (btn) btn.onclick = null; - } - } - - /** - * Callback for loading full file (to be implemented by parent) - * @param {string} filePath - File path to load - */ - onLoadFullFile(filePath) { - // This will be set by the main UI controller - console.log('Load full file requested for:', filePath); - } - - /** - * Callback for loading the next chunk (to be implemented by parent) - * @param {string} filePath - File path - * @param {number} offset - Byte offset to read from - */ - onLoadNextChunk(filePath, offset) { - // This will be set by the main UI controller - console.log('Load next chunk requested:', filePath, 'offset:', offset); - } - - /** - * Mark or unmark a tab as having a missing backing file. - * Updates both the internal set and the tab element styling. - * @param {string} tabId - * @param {boolean} isMissing - */ - markTabMissing(tabId, isMissing) { - if (isMissing) { - this.missingFileTabs.add(tabId); - } else { - this.missingFileTabs.delete(tabId); - } - - const tabEl = document.querySelector(`[data-tab-id="${tabId}"]`); - if (!tabEl) return; - - if (isMissing) { - tabEl.classList.add('file-missing'); - // Add missing icon if not already present - const label = tabEl.querySelector('.tab-label'); - if (label && !label.querySelector('.tab-missing-icon')) { - const icon = document.createElement('span'); - icon.className = 'tab-missing-icon'; - icon.textContent = '⊘'; - icon.title = 'File not found on disk'; - label.insertBefore(icon, label.firstChild); - } - } else { - tabEl.classList.remove('file-missing'); - const icon = tabEl.querySelector('.tab-missing-icon'); - if (icon) icon.remove(); - } - - // If this is the active tab, sync the banner too - if (tabId === this.activeTabId) { - const tab = this.openTabs.get(tabId); - this._syncMissingBanner(tabId, tab ? tab.filePath : null); - } - } - - /** @returns {boolean} True if the tab's file is known to be missing */ - isTabFileMissing(tabId) { - return this.missingFileTabs.has(tabId); - } - - /** - * Show or hide the missing-file banner based on the tab's current missing state. - */ - _syncMissingBanner(tabId, filePath) { - const banner = document.getElementById('file-missing-banner'); - const msg = document.getElementById('file-missing-msg'); - if (!banner) return; - - if (this.missingFileTabs.has(tabId) && filePath) { - if (msg) msg.textContent = `File not found on disk: ${filePath}`; - banner.classList.add('visible'); - - // Wire close-tab button once (idempotent via replacing onclick) - const closeBtn = document.getElementById('file-missing-close-btn'); - if (closeBtn) { - closeBtn.onclick = () => { - const fakeEvent = { stopPropagation: () => {} }; - this.closeTab(fakeEvent, tabId); - }; - } - } else { - banner.classList.remove('visible'); - } - } - - /** - * Async re-check whether a file exists and update the missing flag accordingly. - * Silently no-ops if the file path is empty or IPC is unavailable. - */ - async _recheckFileExists(tabId, filePath) { - if (!filePath || !window.api) return; - try { - const res = await window.api.invoke('check-file-exists', filePath); - const nowMissing = !res.exists; - const wasMissing = this.missingFileTabs.has(tabId); - if (nowMissing !== wasMissing) { - this.markTabMissing(tabId, nowMissing); - } - } catch { - // IPC unavailable or permission denied — leave flag unchanged - } - } - - /** - * Create a new untitled file tab - * @returns {string} - New tab ID - */ - createNewFile() { - const fileName = 'untitled-' + (this.tabIdCounter + 1); - const tabId = this.createTab(fileName); - this.switchToTab(tabId); - this.editorManager.focus(); - return tabId; - } - - /** - * Switch to next tab - */ - switchToNextTab() { - const tabIds = Array.from(this.openTabs.keys()); - if (tabIds.length > 0 && this.activeTabId) { - const currentIndex = tabIds.indexOf(this.activeTabId); - const nextIndex = (currentIndex + 1) % tabIds.length; - this.switchToTab(tabIds[nextIndex]); - this.scrollTabIntoView(tabIds[nextIndex]); - } - } - - /** - * Switch to previous tab - */ - switchToPreviousTab() { - const tabIds = Array.from(this.openTabs.keys()); - if (tabIds.length > 0 && this.activeTabId) { - const currentIndex = tabIds.indexOf(this.activeTabId); - const previousIndex = (currentIndex - 1 + tabIds.length) % tabIds.length; - this.switchToTab(tabIds[previousIndex]); - this.scrollTabIntoView(tabIds[previousIndex]); - } - } - - /** - * Scroll a tab into view - * @param {string} tabId - Tab ID to scroll into view - */ - scrollTabIntoView(tabId) { - const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); - if (tabElement && this.tabsContainer) { - // Get positions relative to the container - const containerRect = this.tabsContainer.getBoundingClientRect(); - const tabRect = tabElement.getBoundingClientRect(); - - // Calculate scroll position to center the tab - const scrollLeft = tabElement.offsetLeft - (this.tabsContainer.offsetWidth / 2) + (tabElement.offsetWidth / 2); - - // Smooth scroll within the tabs container only - this.tabsContainer.scrollTo({ - left: scrollLeft, - behavior: 'smooth' - }); - } - } - - /** - * Get all open tabs count - * @returns {number} - Number of open tabs - */ - getTabCount() { - return this.openTabs.size; - } -} - -if (typeof window !== 'undefined') { - window.TabManager = TabManager; -} -if (typeof module !== 'undefined' && module.exports) { - module.exports = TabManager; -} +;(function() { +/** + * Tab Manager - Handles all tab operations including creation, switching, and closing. + * + * This manager provides a complete tab interface similar to modern code editors, + * allowing users to work with multiple files simultaneously. It manages tab state, + * coordinates with the editor manager, and handles file modifications tracking. + * + * @class TabManager + * @author CTrace GUI Team + * @version 1.0.0 + * + * @example + * const tabManager = new TabManager(editorManager, notificationManager); + * const tabId = tabManager.createTab('example.js', '/path/to/example.js', 'console.log("Hello")'); + */ +class TabManager { + /** + * Creates an instance of TabManager. + * + * @constructor + * @memberof TabManager + * @param {EditorManager} editorManager - Editor manager instance for content management + * @param {NotificationManager} notificationManager - Notification manager for user feedback + */ + constructor(editorManager, notificationManager) { + /** + * Editor manager instance + * @type {EditorManager} + * @private + */ + this.editorManager = editorManager; + + /** + * Notification manager instance + * @type {NotificationManager} + * @private + */ + this.notificationManager = notificationManager; + + /** + * Counter for generating unique tab IDs + * @type {number} + * @private + */ + this.tabIdCounter = 0; + + /** + * Currently active tab ID + * @type {string|null} + */ + this.activeTabId = null; + + /** + * Set of tab IDs whose backing file is missing from disk + * @type {Set} + */ + this.missingFileTabs = new Set(); + + /** + * Map of open tabs with their data + * @type {Map} + * @private + */ + this.openTabs = new Map(); + + /** + * DOM element containing the tabs + * @type {HTMLElement} + * @private + */ + this.tabsContainer = document.getElementById('tabs-container'); + + // Create inner wrapper for proper scrolling + if (this.tabsContainer && !this.tabsContainer.querySelector('.tabs-inner-wrapper')) { + const wrapper = document.createElement('div'); + wrapper.className = 'tabs-inner-wrapper'; + this.tabsContainer.appendChild(wrapper); + this.tabsInnerWrapper = wrapper; + } else { + this.tabsInnerWrapper = this.tabsContainer.querySelector('.tabs-inner-wrapper'); + } + + /** + * Welcome screen DOM element + * @type {HTMLElement} + * @private + */ + this.welcomeScreen = document.getElementById('welcome-screen'); + + /** + * Editor area DOM element + * @type {HTMLElement} + * @private + */ + this.editorArea = document.getElementById('editor-area'); + + /** + * Optional async callback fired before switching tabs. + * @type {(fromTabId: (string|null), toTabId: string) => Promise|null} + */ + this.onBeforeTabSwitch = null; + + /** + * Optional async callback fired before closing a tab. + * @type {(tabId: string) => Promise|null} + */ + this.onBeforeTabClose = null; + } + + /** + * Create a new tab + * @param {string} fileName - Tab file name + * @param {string} filePath - File path (optional) + * @param {string} content - File content + * @param {Object} fileInfo - File metadata + * @returns {string} - Tab ID + */ + createTab(fileName, filePath = null, content = '', fileInfo = {}) { + // Show editor area if this is the first tab + if (this.openTabs.size === 0) { + this.showEditor(); + } + + const tabId = 'tab_' + (++this.tabIdCounter); + + // Create tab data + this.openTabs.set(tabId, { + filePath: filePath, + content: content, + modified: false, + fileName: fileName, + fileInfo: fileInfo // Store file metadata + }); + + // Create tab element + const tabElement = document.createElement('div'); + tabElement.className = 'tab'; + tabElement.setAttribute('data-tab-id', tabId); + tabElement.setAttribute('data-file-path', filePath || ''); + + // Add warning indicator if file has encoding issues or is partial + const warningIndicator = (fileInfo.encodingWarning || fileInfo.isPartial) ? + `⚠️` : ''; + + tabElement.innerHTML = ` +
        ${escapeHtml(fileName)}${warningIndicator}
        +
        ×
        + `; + + tabElement.querySelector('.tab-close').addEventListener('click', (e) => this.closeTab(e, tabId)); + tabElement.addEventListener('click', (e) => { + if (!e.target.classList.contains('tab-close')) { + this.switchToTab(tabId); + } + }); + + const targetContainer = this.tabsInnerWrapper || this.tabsContainer; + targetContainer.appendChild(tabElement); + return tabId; + } + + /** + * Switch to a specific tab + * @param {string} tabId - Tab ID to switch to + */ + async switchToTab(tabId) { + // Save current tab content FIRST (before auto-save callback) if we have an active tab + if (this.activeTabId && this.openTabs.has(this.activeTabId)) { + const currentTab = this.openTabs.get(this.activeTabId); + currentTab.content = this.editorManager.getContent(); + } + + // Give consumers a chance to flush pending edits (e.g., auto-save) before switching. + if (typeof this.onBeforeTabSwitch === 'function') { + try { + await this.onBeforeTabSwitch(this.activeTabId, tabId); + } catch (err) { + console.warn('TabManager onBeforeTabSwitch failed:', err); + } + } + + // Update active tab + this.activeTabId = tabId; + const newTab = this.openTabs.get(tabId); + + if (newTab) { + // Update editor + await this.editorManager.setContent(newTab.content); + + // Set file type for syntax highlighting + if (newTab.fileName) { + await this.editorManager.setFileType(newTab.fileName); + } + + // Update tab appearance + document.querySelectorAll('.tab').forEach(tab => { + tab.classList.remove('active'); + }); + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + if (tabElement) { + tabElement.classList.add('active'); + } + + // Update file tree selection + if (newTab.filePath) { + document.querySelectorAll('.file-tree-item').forEach(el => el.classList.remove('selected')); + const fileTreeItem = document.querySelector(`[data-file-path="${newTab.filePath}"]`); + if (fileTreeItem) { + fileTreeItem.classList.add('selected'); + } + } + + // Show or hide the missing-file banner based on current flag. + // Also re-check existence asynchronously — file may have been restored. + this._syncMissingBanner(tabId, newTab.filePath); + if (newTab.filePath) { + this._recheckFileExists(tabId, newTab.filePath); + } + + // Emit tab switch event for other components + this.onTabSwitch(newTab); + } + } + + /** + * Close a tab + * @param {Event} event - Click event + * @param {string} tabId - Tab ID to close + */ + closeTab(event, tabId) { + event.stopPropagation(); + + // Fire and forget (UI click handler can't await). + void this.closeTabById(tabId); + } + + /** + * Close a tab by id (non-click workflows) + * @param {string} tabId - Tab ID to close + * @param {boolean} [force=false] - If true, close even if modified + * @returns {boolean} - True if closed, false if user canceled + */ + async closeTabById(tabId, force = false) { + const tab = this.openTabs.get(tabId); + if (!tab) return true; + + // Sync content from editor if closing the active tab + if (tabId === this.activeTabId) { + tab.content = this.editorManager.getContent(); + console.log('[TabManager] Synced content from editor for tab:', tab.fileName, 'modified:', tab.modified); + } + + // Give consumers a chance to flush pending edits (e.g., auto-save) before close confirmation. + if (typeof this.onBeforeTabClose === 'function') { + console.log('[TabManager] Calling onBeforeTabClose for:', tab.fileName); + try { + await this.onBeforeTabClose(tabId); + console.log('[TabManager] onBeforeTabClose completed for:', tab.fileName, 'modified now:', tab.modified); + } catch (err) { + console.warn('TabManager onBeforeTabClose failed:', err); + } + } + + // Re-fetch tab state in case auto-save cleaned it + const updatedTab = this.openTabs.get(tabId); + if (!updatedTab) return true; + + // Check if modified + if (updatedTab.modified && !force) { + console.log('[TabManager] Tab still modified after auto-save, prompting user:', updatedTab.fileName); + const result = confirm(`${updatedTab.fileName} has unsaved changes. Do you want to close it anyway?`); + if (!result) return false; + } + + // Remove tab element + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + if (tabElement) { + tabElement.remove(); + } + + // Remove from data + this.openTabs.delete(tabId); + this.missingFileTabs.delete(tabId); + + // If closing active tab, switch to another tab or show welcome screen + if (this.activeTabId === tabId) { + const remainingTabs = Array.from(this.openTabs.keys()); + if (remainingTabs.length > 0) { + await this.switchToTab(remainingTabs[remainingTabs.length - 1]); + } else { + this.activeTabId = null; + this.showWelcomeScreen(); + } + } + + return true; + } + + /** + * Get a tab by id. + * @param {string} tabId + * @returns {Object|null} + */ + getTab(tabId) { + return this.openTabs.get(tabId) || null; + } + + /** + * Mark tab as modified + * @param {string} tabId - Tab ID + */ + markTabModified(tabId) { + const tab = this.openTabs.get(tabId); + if (tab && !tab.modified) { + tab.modified = true; + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + if (tabElement) { + tabElement.classList.add('modified'); + } + } + } + + /** + * Mark tab as clean (not modified) + * @param {string} tabId - Tab ID + */ + markTabClean(tabId) { + const tab = this.openTabs.get(tabId); + if (tab && tab.modified) { + tab.modified = false; + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + if (tabElement) { + tabElement.classList.remove('modified'); + } + } + } + + /** + * Check if file is already open in a tab + * @param {string} filePath - File path to check + * @returns {string|null} - Tab ID if found, null otherwise + */ + findTabByPath(filePath) { + for (const [tabId, tab] of this.openTabs) { + if (tab.filePath === filePath) { + return tabId; + } + } + return null; + } + + /** + * Get current active tab + * @returns {Object|null} - Active tab data or null + */ + getActiveTab() { + return this.activeTabId ? this.openTabs.get(this.activeTabId) : null; + } + + /** + * Update tab file info (for file operations) + * @param {string} tabId - Tab ID + * @param {string} filePath - New file path + * @param {string} fileName - New file name + */ + updateTabFile(tabId, filePath, fileName) { + const tab = this.openTabs.get(tabId); + if (tab) { + tab.filePath = filePath; + tab.fileName = fileName; + + // Update tab label + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + if (tabElement) { + const labelElement = tabElement.querySelector('.tab-label'); + if (labelElement) { + labelElement.textContent = fileName; + } + tabElement.setAttribute('data-file-path', filePath); + } + } + } + + /** + * Show welcome screen + */ + showWelcomeScreen() { + this.welcomeScreen.style.display = 'flex'; + this.editorArea.style.display = 'none'; + this.tabsContainer.style.display = 'none'; + const banner = document.getElementById('partial-file-banner'); + if (banner) banner.classList.remove('visible'); + } + + /** + * Show editor + */ + showEditor() { + this.welcomeScreen.style.display = 'none'; + this.editorArea.style.display = 'flex'; + this.tabsContainer.style.display = 'flex'; + + // Trigger Monaco layout update after showing editor area. + // Multiple retries cover the case where Monaco hasn't initialized yet. + const triggerLayout = () => { + if (this.editorManager && this.editorManager.editor) { + this.editorManager.editor.layout(); + } + }; + setTimeout(triggerLayout, 50); + setTimeout(triggerLayout, 200); + setTimeout(triggerLayout, 500); + } + + /** + * Handle tab content changes (for modification tracking) + * @param {string} tabId - Tab ID + * @param {string} newContent - New content + */ + handleContentChange(tabId, newContent) { + const tab = this.openTabs.get(tabId); + if (tab && tab.content !== newContent) { + tab.content = newContent; + this.markTabModified(tabId); + } + } + + /** + * Callback for when tab switches (for other components to listen to) + * @param {Object} tabData - Tab data + */ + onTabSwitch(tabData) { + // Update file type in status bar if file type utils are available + if (window.updateFileTypeStatus) { + window.updateFileTypeStatus(tabData.fileName); + } + + // Update file status + this.updateFileStatus(tabData); + } + + /** + * Update file status indicator + * @param {Object} tabData - Tab data + */ + updateFileStatus(tabData) { + const fileStatusElement = document.getElementById('fileStatus'); + if (fileStatusElement && tabData.fileInfo) { + const { fileInfo } = tabData; + let statusText = 'UTF-8'; + let statusStyle = ''; + + if (fileInfo.encodingWarning) { + statusText = '⚠️ Non-UTF8'; + statusStyle = 'color: #f85149; cursor: pointer;'; + fileStatusElement.title = 'File may contain non-UTF8 characters'; + } else if (fileInfo.isPartial) { + const loadedKB = Math.round(fileInfo.loadedSize / 1024); + const totalKB = Math.round(fileInfo.totalSize / 1024); + statusText = `📄 Partial (${loadedKB}KB/${totalKB}KB)`; + statusStyle = 'color: #f0883e; cursor: pointer;'; + fileStatusElement.title = 'Click to load full file'; + fileStatusElement.onclick = () => this.onLoadFullFile(tabData.filePath); + } else { + statusText = 'UTF-8'; + statusStyle = ''; + fileStatusElement.onclick = null; + fileStatusElement.title = ''; + } + + fileStatusElement.textContent = statusText; + fileStatusElement.style.cssText = statusStyle; + } + + // Show or hide the partial-file banner + this._updatePartialBanner(tabData); + } + + _updatePartialBanner(tabData) { + const banner = document.getElementById('partial-file-banner'); + const msg = document.getElementById('partial-file-msg'); + const btn = document.getElementById('partial-load-more-btn'); + if (!banner) return; + + const fileInfo = tabData && tabData.fileInfo; + if (fileInfo && fileInfo.isPartial) { + const loadedMB = (fileInfo.loadedSize / (1024 * 1024)).toFixed(1); + const totalMB = (fileInfo.totalSize / (1024 * 1024)).toFixed(1); + if (msg) msg.textContent = `Showing first ${loadedMB} MB of ${totalMB} MB`; + if (btn) { + btn.disabled = false; + btn.onclick = () => this.onLoadNextChunk(tabData.filePath, fileInfo.loadedSize); + } + banner.classList.add('visible'); + } else { + banner.classList.remove('visible'); + if (btn) btn.onclick = null; + } + } + + /** + * Callback for loading full file (to be implemented by parent) + * @param {string} filePath - File path to load + */ + onLoadFullFile(filePath) { + // This will be set by the main UI controller + console.log('Load full file requested for:', filePath); + } + + /** + * Callback for loading the next chunk (to be implemented by parent) + * @param {string} filePath - File path + * @param {number} offset - Byte offset to read from + */ + onLoadNextChunk(filePath, offset) { + // This will be set by the main UI controller + console.log('Load next chunk requested:', filePath, 'offset:', offset); + } + + /** + * Mark or unmark a tab as having a missing backing file. + * Updates both the internal set and the tab element styling. + * @param {string} tabId + * @param {boolean} isMissing + */ + markTabMissing(tabId, isMissing) { + if (isMissing) { + this.missingFileTabs.add(tabId); + } else { + this.missingFileTabs.delete(tabId); + } + + const tabEl = document.querySelector(`[data-tab-id="${tabId}"]`); + if (!tabEl) return; + + if (isMissing) { + tabEl.classList.add('file-missing'); + // Add missing icon if not already present + const label = tabEl.querySelector('.tab-label'); + if (label && !label.querySelector('.tab-missing-icon')) { + const icon = document.createElement('span'); + icon.className = 'tab-missing-icon'; + icon.textContent = '⊘'; + icon.title = 'File not found on disk'; + label.insertBefore(icon, label.firstChild); + } + } else { + tabEl.classList.remove('file-missing'); + const icon = tabEl.querySelector('.tab-missing-icon'); + if (icon) icon.remove(); + } + + // If this is the active tab, sync the banner too + if (tabId === this.activeTabId) { + const tab = this.openTabs.get(tabId); + this._syncMissingBanner(tabId, tab ? tab.filePath : null); + } + } + + /** @returns {boolean} True if the tab's file is known to be missing */ + isTabFileMissing(tabId) { + return this.missingFileTabs.has(tabId); + } + + /** + * Show or hide the missing-file banner based on the tab's current missing state. + */ + _syncMissingBanner(tabId, filePath) { + const banner = document.getElementById('file-missing-banner'); + const msg = document.getElementById('file-missing-msg'); + if (!banner) return; + + if (this.missingFileTabs.has(tabId) && filePath) { + if (msg) msg.textContent = `File not found on disk: ${filePath}`; + banner.classList.add('visible'); + + // Wire close-tab button once (idempotent via replacing onclick) + const closeBtn = document.getElementById('file-missing-close-btn'); + if (closeBtn) { + closeBtn.onclick = () => { + const fakeEvent = { stopPropagation: () => {} }; + this.closeTab(fakeEvent, tabId); + }; + } + } else { + banner.classList.remove('visible'); + } + } + + /** + * Async re-check whether a file exists and update the missing flag accordingly. + * Silently no-ops if the file path is empty or IPC is unavailable. + */ + async _recheckFileExists(tabId, filePath) { + if (!filePath || !window.api) return; + try { + const res = await window.api.invoke('check-file-exists', filePath); + const nowMissing = !res.exists; + const wasMissing = this.missingFileTabs.has(tabId); + if (nowMissing !== wasMissing) { + this.markTabMissing(tabId, nowMissing); + } + } catch { + // IPC unavailable or permission denied — leave flag unchanged + } + } + + /** + * Create a new untitled file tab + * @returns {string} - New tab ID + */ + createNewFile() { + const fileName = 'untitled-' + (this.tabIdCounter + 1); + const tabId = this.createTab(fileName); + this.switchToTab(tabId); + this.editorManager.focus(); + return tabId; + } + + /** + * Switch to next tab + */ + switchToNextTab() { + const tabIds = Array.from(this.openTabs.keys()); + if (tabIds.length > 0 && this.activeTabId) { + const currentIndex = tabIds.indexOf(this.activeTabId); + const nextIndex = (currentIndex + 1) % tabIds.length; + this.switchToTab(tabIds[nextIndex]); + this.scrollTabIntoView(tabIds[nextIndex]); + } + } + + /** + * Switch to previous tab + */ + switchToPreviousTab() { + const tabIds = Array.from(this.openTabs.keys()); + if (tabIds.length > 0 && this.activeTabId) { + const currentIndex = tabIds.indexOf(this.activeTabId); + const previousIndex = (currentIndex - 1 + tabIds.length) % tabIds.length; + this.switchToTab(tabIds[previousIndex]); + this.scrollTabIntoView(tabIds[previousIndex]); + } + } + + /** + * Scroll a tab into view + * @param {string} tabId - Tab ID to scroll into view + */ + scrollTabIntoView(tabId) { + const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`); + if (tabElement && this.tabsContainer) { + // Get positions relative to the container + const containerRect = this.tabsContainer.getBoundingClientRect(); + const tabRect = tabElement.getBoundingClientRect(); + + // Calculate scroll position to center the tab + const scrollLeft = tabElement.offsetLeft - (this.tabsContainer.offsetWidth / 2) + (tabElement.offsetWidth / 2); + + // Smooth scroll within the tabs container only + this.tabsContainer.scrollTo({ + left: scrollLeft, + behavior: 'smooth' + }); + } + } + + /** + * Get all open tabs count + * @returns {number} - Number of open tabs + */ + getTabCount() { + return this.openTabs.size; + } +} + +if (typeof window !== 'undefined') { + window.TabManager = TabManager; +} +if (typeof module !== 'undefined' && module.exports) { + module.exports = TabManager; +} })(); \ No newline at end of file diff --git a/src/renderer/managers/TerminalManager.js b/src/renderer/managers/TerminalManager.js index 623fffe..fec4639 100644 --- a/src/renderer/managers/TerminalManager.js +++ b/src/renderer/managers/TerminalManager.js @@ -1,589 +1,589 @@ -/** - * TerminalManager - Integrated terminal panel for CTraceGUI. - * - * Manages multiple terminal sessions inside the bottom panel. - * Each session tracks its own cwd, command history, and shell type. - * Commands are executed via IPC (main process spawns the child process). - */ -class TerminalManager { - constructor() { - this.terminals = new Map(); // id -> terminal state - this.activeId = null; - this.counter = 0; - - this.shells = []; - this.currentShellId = null; - this.currentShellPath = null; - - this._dataUnsub = null; - this._doneUnsub = null; - this._shellDropdownOpen = false; - - // Autocomplete state - this._lastTabTime = 0; // for double-tap detection - } - - // ─── Init ──────────────────────────────────────────────────────────────── - - async init() { - try { - this.shells = await window.api.invoke('terminal-get-shells'); - } catch { - this.shells = [{ id: 'cmd', name: 'Command Prompt', icon: 'cmd' }]; - } - if (this.shells.length) { - this.currentShellId = this.shells[0].id; - this.currentShellPath = this.shells[0].path || null; - } - - // IPC listeners - this._dataUnsub = window.api.on('terminal-data', ({ terminalId, data }) => { - this._appendOutput(terminalId, data); - }); - this._doneUnsub = window.api.on('terminal-command-done', ({ terminalId, code }) => { - const term = this.terminals.get(terminalId); - if (!term) return; - term.running = false; - this._setInputEnabled(terminalId, true); - this._scrollBottom(terminalId); - }); - - // Close shell dropdown on outside click - document.addEventListener('click', (e) => { - if ( - this._shellDropdownOpen && - !e.target.closest('#terminal-shell-dropdown') && - !e.target.closest('#terminal-shell-btn') - ) { - this._closeShellDropdown(); - } - }); - - this._setupTerminalResizer(); - } - - // ─── Panel visibility ──────────────────────────────────────────────────── - - show() { - const panel = document.getElementById('terminalPanel'); - if (!panel) return; - panel.style.display = 'flex'; - panel.offsetHeight; // reflow - panel.classList.add('active'); - - // Auto-create first terminal - if (this.terminals.size === 0) { - this.createTerminal(); - } else if (this.activeId !== null) { - setTimeout(() => this._focusInput(this.activeId), 60); - } - } - - hide() { - const panel = document.getElementById('terminalPanel'); - if (!panel) return; - panel.classList.remove('active'); - setTimeout(() => { - if (!panel.classList.contains('active')) panel.style.display = 'none'; - }, 200); - } - - toggle() { - const panel = document.getElementById('terminalPanel'); - if (!panel) return; - if (panel.classList.contains('active')) this.hide(); - else this.show(); - } - - isVisible() { - const panel = document.getElementById('terminalPanel'); - return panel ? panel.classList.contains('active') : false; - } - - // ─── Terminal lifecycle ────────────────────────────────────────────────── - - async createTerminal(shellId, shellPath) { - const sid = shellId || this.currentShellId; - const spath = shellPath || this.currentShellPath; - const shell = this.shells.find(s => s.id === sid) || this.shells[0] || { id: 'cmd', name: 'Terminal' }; - - // Get initial cwd from the app's working directory - let cwd = ''; - try { cwd = await window.api.invoke('terminal-get-initial-cwd'); } catch { - try { cwd = await window.api.invoke('terminal-get-home'); } catch { cwd = ''; } - } - - const id = ++this.counter; - this.terminals.set(id, { - shell, - shellPath: spath, - cwd: cwd, - history: [], - historyIndex: -1, - running: false, - }); - - this._renderTab(id); - this._renderInstance(id); - this._activate(id); - - this._appendOutput(id, - `\u001b[32m${shell.name}\u001b[0m \u001b[2m${cwd}\u001b[0m\r\n`, - true - ); - return id; - } - - closeTerminal(id) { - const term = this.terminals.get(id); - if (!term) return; - - window.api.invoke('terminal-kill-current', id).catch(() => {}); - - document.getElementById(`terminal-tab-${id}`)?.remove(); - document.getElementById(`terminal-inst-${id}`)?.remove(); - this.terminals.delete(id); - - if (this.activeId === id) { - this.activeId = null; - const remaining = [...this.terminals.keys()]; - if (remaining.length) { - this._activate(remaining[remaining.length - 1]); - } else { - this.hide(); - } - } - } - - // ─── Rendering ─────────────────────────────────────────────────────────── - - _renderTab(id) { - const term = this.terminals.get(id); - const list = document.getElementById('terminal-tabs-list'); - if (!list) return; - - const tab = document.createElement('div'); - tab.className = 'terminal-tab'; - tab.id = `terminal-tab-${id}`; - tab.innerHTML = ` - - - - ${this._escHtml(term.shell.name)} - × - `; - - tab.addEventListener('click', (e) => { - if (e.target.closest('.terminal-tab-close')) { - e.stopPropagation(); - this.closeTerminal(id); - } else if (!e.target.closest('svg')) { - this._activate(id); - } - }); - - list.appendChild(tab); - } - - _renderInstance(id) { - const body = document.getElementById('terminal-body'); - if (!body) return; - const term = this.terminals.get(id); - - const el = document.createElement('div'); - el.className = 'terminal-instance'; - el.id = `terminal-inst-${id}`; - el.innerHTML = ` -
        -
        - - $ -
        - -
        - `; - body.appendChild(el); - - this._updateCwd(id); - - const input = el.querySelector(`#terminal-in-${id}`); - input.addEventListener('keydown', (e) => this._onKey(e, id)); - } - - _activate(id) { - if (this.activeId !== null) { - document.getElementById(`terminal-tab-${this.activeId}`)?.classList.remove('active'); - document.getElementById(`terminal-inst-${this.activeId}`)?.classList.remove('active'); - } - this.activeId = id; - document.getElementById(`terminal-tab-${id}`)?.classList.add('active'); - document.getElementById(`terminal-inst-${id}`)?.classList.add('active'); - setTimeout(() => this._focusInput(id), 40); - } - - _updateCwd(id) { - const term = this.terminals.get(id); - const el = document.getElementById(`terminal-cwd-${id}`); - if (!el || !term) return; - // Show full path - el.textContent = term.cwd || '~'; - el.title = term.cwd; - } - - // ─── Input handling ────────────────────────────────────────────────────── - - _onKey(e, id) { - const term = this.terminals.get(id); - if (!term) return; - - switch (e.key) { - case 'Enter': { - e.preventDefault(); - const raw = e.target.value.trim(); - if (!raw) return; - - // If a process is running, send input to it instead of executing a new command - if (term.running) { - window.api.invoke('terminal-send-input', { terminalId: id, input: raw }).catch(() => {}); - this._appendOutput(id, `${this._escHtml(raw)}\r\n`, true); - e.target.value = ''; - term.historyIndex = -1; - } else { - // Normal command execution - term.history.unshift(raw); - if (term.history.length > 100) term.history.pop(); - term.historyIndex = -1; - e.target.value = ''; - this._execute(id, raw); - } - break; - } - case 'Tab': { - e.preventDefault(); - this._onTab(e.target, id); - break; - } - case 'ArrowUp': { - e.preventDefault(); - if (term.historyIndex < term.history.length - 1) { - term.historyIndex++; - e.target.value = term.history[term.historyIndex] || ''; - } - break; - } - case 'ArrowDown': { - e.preventDefault(); - if (term.historyIndex > 0) { - term.historyIndex--; - e.target.value = term.history[term.historyIndex] || ''; - } else { - term.historyIndex = -1; - e.target.value = ''; - } - break; - } - case 'c': { - if (e.ctrlKey && term.running) { - e.preventDefault(); - window.api.invoke('terminal-kill-current', id).catch(() => {}); - this._appendOutput(id, '\u001b[31m^C\u001b[0m\r\n', true); - term.running = false; - this._setInputEnabled(id, true); - } - break; - } - case 'l': { - if (e.ctrlKey) { - e.preventDefault(); - const out = document.getElementById(`terminal-out-${id}`); - if (out) out.innerHTML = ''; - } - break; - } - } - } - - async _onTab(input, id) { - const term = this.terminals.get(id); - if (!term) return; - - const partial = input.value; - if (!partial.trim()) return; - - try { - const matches = await window.api.invoke('terminal-get-completions', { - cwd: term.cwd, - partial, - }); - - if (matches.length === 0) { - // No matches, do nothing - return; - } else if (matches.length === 1) { - // Single match: auto-complete - const trimmed = partial.trim(); - const lastSpace = trimmed.lastIndexOf(' '); - const prefix = lastSpace === -1 ? '' : trimmed.substring(0, lastSpace + 1); - input.value = prefix + matches[0].value; - } else { - // Multiple matches: show them - this._appendOutput(id, `\u001b[2m${matches.map(m => m.label).join(' ')}\u001b[0m\r\n`, true); - this._scrollBottom(id); - } - } catch {} - } - - async _execute(id, command) { - const term = this.terminals.get(id); - if (!term || term.running) return; - - // Echo the command - this._appendOutput(id, - `\u001b[2m${this._escPrompt(term.cwd)}\u001b[0m \u001b[1m${this._escHtml(command)}\u001b[0m\r\n`, - true - ); - - // Handle `cd` locally — update cwd, then bail (no subprocess needed) - const cdMatch = command.match(/^cd(?:\s+(.+))?$/i); - if (cdMatch) { - const target = (cdMatch[1] || '').trim().replace(/^["']|["']$/g, ''); - if (!target || target === '~') { - try { term.cwd = await window.api.invoke('terminal-get-home'); } catch {} - } else { - term.cwd = this._joinPath(term.cwd, target); - } - this._updateCwd(id); - return; - } - - // Handle `clear` / `cls` - if (/^(clear|cls)$/i.test(command)) { - const out = document.getElementById(`terminal-out-${id}`); - if (out) out.innerHTML = ''; - return; - } - - term.running = true; - this._setInputEnabled(id, false); - - try { - await window.api.invoke('terminal-execute', { - terminalId: id, - shellId: term.shell.id, - shellPath: term.shellPath || null, - command, - cwd: term.cwd, - }); - } catch (err) { - this._appendOutput(id, `\u001b[31mError: ${err.message}\u001b[0m\r\n`, true); - term.running = false; - this._setInputEnabled(id, true); - } - } - - _joinPath(base, rel) { - if (!rel) return base; - // Absolute paths (Windows or Unix) - if (/^[A-Za-z]:[\\/]/.test(rel) || rel.startsWith('/')) return rel; - if (rel === '..') { - const parts = base.replace(/\\/g, '/').split('/'); - parts.pop(); - return parts.join('/') || base; - } - const sep = base.includes('\\') ? '\\' : '/'; - return base.replace(/[\\/]+$/, '') + sep + rel; - } - - // ─── Output rendering ──────────────────────────────────────────────────── - - _appendOutput(id, text, raw = false) { - const out = document.getElementById(`terminal-out-${id}`); - if (!out) return; - - const span = document.createElement('span'); - span.innerHTML = this._ansiToHtml(text); - out.appendChild(span); - this._scrollBottom(id); - } - - _ansiToHtml(text) { - if (!text) return ''; - - // Normalise line endings - let s = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); - - // Strip all ANSI escape sequences except SGR color/style codes. - s = s.replace( - /\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, - (m) => /^\x1b\[[0-9;]*m$/.test(m) ? m : '' - ); - - // Escape HTML special chars (before inserting spans) - const escHtml = (t) => t - .replace(/&/g, '&') - .replace(//g, '>'); - - const fgMap = { - '30':'a-black','31':'a-red','32':'a-green','33':'a-yellow', - '34':'a-blue','35':'a-magenta','36':'a-cyan','37':'a-white', - '90':'a-Bblack','91':'a-Bred','92':'a-Bgreen','93':'a-Byellow', - '94':'a-Bblue','95':'a-Bmagenta','96':'a-Bcyan','97':'a-Bwhite', - }; - - let result = ''; - let openSpans = 0; - - // Split on SGR sequences (\x1b[...m) - const parts = s.split(/(\x1b\[[0-9;]*m)/g); - for (const part of parts) { - if (/^\x1b\[/.test(part)) { - // Close open spans on any SGR - while (openSpans > 0) { result += ''; openSpans--; } - const code = part.slice(2, -1); - if (code === '0' || code === '') continue; // reset - const classes = []; - for (const c of code.split(';')) { - if (c === '1') classes.push('a-bold'); - if (c === '2') classes.push('a-dim'); - if (fgMap[c]) classes.push(fgMap[c]); - } - if (classes.length) { result += ` 0) { result += ''; openSpans--; } - return result; - } - - // ─── Helpers ───────────────────────────────────────────────────────────── - - _scrollBottom(id) { - const out = document.getElementById(`terminal-out-${id}`); - if (out) out.scrollTop = out.scrollHeight; - } - - _setInputEnabled(id, enabled) { - const spinner = document.getElementById(`terminal-spinner-${id}`); - if (spinner) spinner.classList.toggle('visible', !enabled); - // Keep input always enabled so user can respond to interactive prompts (sudo, password, etc.) - } - - _focusInput(id) { - document.getElementById(`terminal-in-${id}`)?.focus(); - } - - _escHtml(str) { - return String(str) - .replace(/&/g, '&') - .replace(//g, '>'); - } - - _escPrompt(cwd) { - const parts = (cwd || '').replace(/\\/g, '/').split('/').filter(Boolean); - return parts.length ? parts[parts.length - 1] : '~'; - } - - // ─── Shell selector dropdown ───────────────────────────────────────────── - - toggleShellDropdown() { - if (this._shellDropdownOpen) { - this._closeShellDropdown(); - } else { - this._openShellDropdown(); - } - } - - _openShellDropdown() { - this._closeShellDropdown(); - - const btn = document.getElementById('terminal-shell-btn'); - if (!btn) return; - const rect = btn.getBoundingClientRect(); - - const dd = document.createElement('div'); - dd.className = 'terminal-shell-dropdown'; - dd.id = 'terminal-shell-dropdown'; - dd.style.cssText = `bottom:${window.innerHeight - rect.top + 4}px; left:${rect.left}px;`; - - dd.innerHTML = this.shells.map(s => ` -
        - - - - - ${this._escHtml(s.name)} -
        - `).join(''); - - dd.addEventListener('click', (e) => { - const item = e.target.closest('[data-id]'); - if (!item) return; - const sid = item.dataset.id; - const spath = item.dataset.path || null; - this.currentShellId = sid; - this.currentShellPath = spath; - const btnLabel = document.getElementById('terminal-shell-label'); - const s = this.shells.find(x => x.id === sid); - if (btnLabel && s) btnLabel.textContent = s.name; - this._closeShellDropdown(); - this.createTerminal(sid, spath || null); - }); - - document.body.appendChild(dd); - this._shellDropdownOpen = true; - } - - _closeShellDropdown() { - document.getElementById('terminal-shell-dropdown')?.remove(); - this._shellDropdownOpen = false; - } - - // ─── Vertical resize ───────────────────────────────────────────────────── - - _setupTerminalResizer() { - const handle = document.getElementById('terminal-resizer-top'); - const panel = document.getElementById('terminalPanel'); - if (!handle || !panel) return; - - let startY = 0; - let startH = 0; - - const onMove = (e) => { - const dy = startY - e.clientY; - const newH = Math.max(120, Math.min(window.innerHeight * 0.72, startH + dy)); - panel.style.height = newH + 'px'; - }; - const onUp = () => { - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - }; - - handle.addEventListener('mousedown', (e) => { - e.preventDefault(); - startY = e.clientY; - startH = panel.offsetHeight; - document.body.style.cursor = 'ns-resize'; - document.body.style.userSelect = 'none'; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); - }); - } - - - destroy() { - if (this._dataUnsub) window.api.removeListener('terminal-data', this._dataUnsub); - if (this._doneUnsub) window.api.removeListener('terminal-command-done', this._doneUnsub); - } -} +/** + * TerminalManager - Integrated terminal panel for CTraceGUI. + * + * Manages multiple terminal sessions inside the bottom panel. + * Each session tracks its own cwd, command history, and shell type. + * Commands are executed via IPC (main process spawns the child process). + */ +class TerminalManager { + constructor() { + this.terminals = new Map(); // id -> terminal state + this.activeId = null; + this.counter = 0; + + this.shells = []; + this.currentShellId = null; + this.currentShellPath = null; + + this._dataUnsub = null; + this._doneUnsub = null; + this._shellDropdownOpen = false; + + // Autocomplete state + this._lastTabTime = 0; // for double-tap detection + } + + // ─── Init ──────────────────────────────────────────────────────────────── + + async init() { + try { + this.shells = await window.api.invoke('terminal-get-shells'); + } catch { + this.shells = [{ id: 'cmd', name: 'Command Prompt', icon: 'cmd' }]; + } + if (this.shells.length) { + this.currentShellId = this.shells[0].id; + this.currentShellPath = this.shells[0].path || null; + } + + // IPC listeners + this._dataUnsub = window.api.on('terminal-data', ({ terminalId, data }) => { + this._appendOutput(terminalId, data); + }); + this._doneUnsub = window.api.on('terminal-command-done', ({ terminalId, code }) => { + const term = this.terminals.get(terminalId); + if (!term) return; + term.running = false; + this._setInputEnabled(terminalId, true); + this._scrollBottom(terminalId); + }); + + // Close shell dropdown on outside click + document.addEventListener('click', (e) => { + if ( + this._shellDropdownOpen && + !e.target.closest('#terminal-shell-dropdown') && + !e.target.closest('#terminal-shell-btn') + ) { + this._closeShellDropdown(); + } + }); + + this._setupTerminalResizer(); + } + + // ─── Panel visibility ──────────────────────────────────────────────────── + + show() { + const panel = document.getElementById('terminalPanel'); + if (!panel) return; + panel.style.display = 'flex'; + panel.offsetHeight; // reflow + panel.classList.add('active'); + + // Auto-create first terminal + if (this.terminals.size === 0) { + this.createTerminal(); + } else if (this.activeId !== null) { + setTimeout(() => this._focusInput(this.activeId), 60); + } + } + + hide() { + const panel = document.getElementById('terminalPanel'); + if (!panel) return; + panel.classList.remove('active'); + setTimeout(() => { + if (!panel.classList.contains('active')) panel.style.display = 'none'; + }, 200); + } + + toggle() { + const panel = document.getElementById('terminalPanel'); + if (!panel) return; + if (panel.classList.contains('active')) this.hide(); + else this.show(); + } + + isVisible() { + const panel = document.getElementById('terminalPanel'); + return panel ? panel.classList.contains('active') : false; + } + + // ─── Terminal lifecycle ────────────────────────────────────────────────── + + async createTerminal(shellId, shellPath) { + const sid = shellId || this.currentShellId; + const spath = shellPath || this.currentShellPath; + const shell = this.shells.find(s => s.id === sid) || this.shells[0] || { id: 'cmd', name: 'Terminal' }; + + // Get initial cwd from the app's working directory + let cwd = ''; + try { cwd = await window.api.invoke('terminal-get-initial-cwd'); } catch { + try { cwd = await window.api.invoke('terminal-get-home'); } catch { cwd = ''; } + } + + const id = ++this.counter; + this.terminals.set(id, { + shell, + shellPath: spath, + cwd: cwd, + history: [], + historyIndex: -1, + running: false, + }); + + this._renderTab(id); + this._renderInstance(id); + this._activate(id); + + this._appendOutput(id, + `\u001b[32m${shell.name}\u001b[0m \u001b[2m${cwd}\u001b[0m\r\n`, + true + ); + return id; + } + + closeTerminal(id) { + const term = this.terminals.get(id); + if (!term) return; + + window.api.invoke('terminal-kill-current', id).catch(() => {}); + + document.getElementById(`terminal-tab-${id}`)?.remove(); + document.getElementById(`terminal-inst-${id}`)?.remove(); + this.terminals.delete(id); + + if (this.activeId === id) { + this.activeId = null; + const remaining = [...this.terminals.keys()]; + if (remaining.length) { + this._activate(remaining[remaining.length - 1]); + } else { + this.hide(); + } + } + } + + // ─── Rendering ─────────────────────────────────────────────────────────── + + _renderTab(id) { + const term = this.terminals.get(id); + const list = document.getElementById('terminal-tabs-list'); + if (!list) return; + + const tab = document.createElement('div'); + tab.className = 'terminal-tab'; + tab.id = `terminal-tab-${id}`; + tab.innerHTML = ` + + + + ${this._escHtml(term.shell.name)} + × + `; + + tab.addEventListener('click', (e) => { + if (e.target.closest('.terminal-tab-close')) { + e.stopPropagation(); + this.closeTerminal(id); + } else if (!e.target.closest('svg')) { + this._activate(id); + } + }); + + list.appendChild(tab); + } + + _renderInstance(id) { + const body = document.getElementById('terminal-body'); + if (!body) return; + const term = this.terminals.get(id); + + const el = document.createElement('div'); + el.className = 'terminal-instance'; + el.id = `terminal-inst-${id}`; + el.innerHTML = ` +
        +
        + + $ +
        + +
        + `; + body.appendChild(el); + + this._updateCwd(id); + + const input = el.querySelector(`#terminal-in-${id}`); + input.addEventListener('keydown', (e) => this._onKey(e, id)); + } + + _activate(id) { + if (this.activeId !== null) { + document.getElementById(`terminal-tab-${this.activeId}`)?.classList.remove('active'); + document.getElementById(`terminal-inst-${this.activeId}`)?.classList.remove('active'); + } + this.activeId = id; + document.getElementById(`terminal-tab-${id}`)?.classList.add('active'); + document.getElementById(`terminal-inst-${id}`)?.classList.add('active'); + setTimeout(() => this._focusInput(id), 40); + } + + _updateCwd(id) { + const term = this.terminals.get(id); + const el = document.getElementById(`terminal-cwd-${id}`); + if (!el || !term) return; + // Show full path + el.textContent = term.cwd || '~'; + el.title = term.cwd; + } + + // ─── Input handling ────────────────────────────────────────────────────── + + _onKey(e, id) { + const term = this.terminals.get(id); + if (!term) return; + + switch (e.key) { + case 'Enter': { + e.preventDefault(); + const raw = e.target.value.trim(); + if (!raw) return; + + // If a process is running, send input to it instead of executing a new command + if (term.running) { + window.api.invoke('terminal-send-input', { terminalId: id, input: raw }).catch(() => {}); + this._appendOutput(id, `${this._escHtml(raw)}\r\n`, true); + e.target.value = ''; + term.historyIndex = -1; + } else { + // Normal command execution + term.history.unshift(raw); + if (term.history.length > 100) term.history.pop(); + term.historyIndex = -1; + e.target.value = ''; + this._execute(id, raw); + } + break; + } + case 'Tab': { + e.preventDefault(); + this._onTab(e.target, id); + break; + } + case 'ArrowUp': { + e.preventDefault(); + if (term.historyIndex < term.history.length - 1) { + term.historyIndex++; + e.target.value = term.history[term.historyIndex] || ''; + } + break; + } + case 'ArrowDown': { + e.preventDefault(); + if (term.historyIndex > 0) { + term.historyIndex--; + e.target.value = term.history[term.historyIndex] || ''; + } else { + term.historyIndex = -1; + e.target.value = ''; + } + break; + } + case 'c': { + if (e.ctrlKey && term.running) { + e.preventDefault(); + window.api.invoke('terminal-kill-current', id).catch(() => {}); + this._appendOutput(id, '\u001b[31m^C\u001b[0m\r\n', true); + term.running = false; + this._setInputEnabled(id, true); + } + break; + } + case 'l': { + if (e.ctrlKey) { + e.preventDefault(); + const out = document.getElementById(`terminal-out-${id}`); + if (out) out.innerHTML = ''; + } + break; + } + } + } + + async _onTab(input, id) { + const term = this.terminals.get(id); + if (!term) return; + + const partial = input.value; + if (!partial.trim()) return; + + try { + const matches = await window.api.invoke('terminal-get-completions', { + cwd: term.cwd, + partial, + }); + + if (matches.length === 0) { + // No matches, do nothing + return; + } else if (matches.length === 1) { + // Single match: auto-complete + const trimmed = partial.trim(); + const lastSpace = trimmed.lastIndexOf(' '); + const prefix = lastSpace === -1 ? '' : trimmed.substring(0, lastSpace + 1); + input.value = prefix + matches[0].value; + } else { + // Multiple matches: show them + this._appendOutput(id, `\u001b[2m${matches.map(m => m.label).join(' ')}\u001b[0m\r\n`, true); + this._scrollBottom(id); + } + } catch {} + } + + async _execute(id, command) { + const term = this.terminals.get(id); + if (!term || term.running) return; + + // Echo the command + this._appendOutput(id, + `\u001b[2m${this._escPrompt(term.cwd)}\u001b[0m \u001b[1m${this._escHtml(command)}\u001b[0m\r\n`, + true + ); + + // Handle `cd` locally — update cwd, then bail (no subprocess needed) + const cdMatch = command.match(/^cd(?:\s+(.+))?$/i); + if (cdMatch) { + const target = (cdMatch[1] || '').trim().replace(/^["']|["']$/g, ''); + if (!target || target === '~') { + try { term.cwd = await window.api.invoke('terminal-get-home'); } catch {} + } else { + term.cwd = this._joinPath(term.cwd, target); + } + this._updateCwd(id); + return; + } + + // Handle `clear` / `cls` + if (/^(clear|cls)$/i.test(command)) { + const out = document.getElementById(`terminal-out-${id}`); + if (out) out.innerHTML = ''; + return; + } + + term.running = true; + this._setInputEnabled(id, false); + + try { + await window.api.invoke('terminal-execute', { + terminalId: id, + shellId: term.shell.id, + shellPath: term.shellPath || null, + command, + cwd: term.cwd, + }); + } catch (err) { + this._appendOutput(id, `\u001b[31mError: ${err.message}\u001b[0m\r\n`, true); + term.running = false; + this._setInputEnabled(id, true); + } + } + + _joinPath(base, rel) { + if (!rel) return base; + // Absolute paths (Windows or Unix) + if (/^[A-Za-z]:[\\/]/.test(rel) || rel.startsWith('/')) return rel; + if (rel === '..') { + const parts = base.replace(/\\/g, '/').split('/'); + parts.pop(); + return parts.join('/') || base; + } + const sep = base.includes('\\') ? '\\' : '/'; + return base.replace(/[\\/]+$/, '') + sep + rel; + } + + // ─── Output rendering ──────────────────────────────────────────────────── + + _appendOutput(id, text, raw = false) { + const out = document.getElementById(`terminal-out-${id}`); + if (!out) return; + + const span = document.createElement('span'); + span.innerHTML = this._ansiToHtml(text); + out.appendChild(span); + this._scrollBottom(id); + } + + _ansiToHtml(text) { + if (!text) return ''; + + // Normalise line endings + let s = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + // Strip all ANSI escape sequences except SGR color/style codes. + s = s.replace( + /\x1b(?:\][^\x07\x1b]*(?:\x07|\x1b\\)|[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, + (m) => /^\x1b\[[0-9;]*m$/.test(m) ? m : '' + ); + + // Escape HTML special chars (before inserting spans) + const escHtml = (t) => t + .replace(/&/g, '&') + .replace(//g, '>'); + + const fgMap = { + '30':'a-black','31':'a-red','32':'a-green','33':'a-yellow', + '34':'a-blue','35':'a-magenta','36':'a-cyan','37':'a-white', + '90':'a-Bblack','91':'a-Bred','92':'a-Bgreen','93':'a-Byellow', + '94':'a-Bblue','95':'a-Bmagenta','96':'a-Bcyan','97':'a-Bwhite', + }; + + let result = ''; + let openSpans = 0; + + // Split on SGR sequences (\x1b[...m) + const parts = s.split(/(\x1b\[[0-9;]*m)/g); + for (const part of parts) { + if (/^\x1b\[/.test(part)) { + // Close open spans on any SGR + while (openSpans > 0) { result += ''; openSpans--; } + const code = part.slice(2, -1); + if (code === '0' || code === '') continue; // reset + const classes = []; + for (const c of code.split(';')) { + if (c === '1') classes.push('a-bold'); + if (c === '2') classes.push('a-dim'); + if (fgMap[c]) classes.push(fgMap[c]); + } + if (classes.length) { result += ` 0) { result += ''; openSpans--; } + return result; + } + + // ─── Helpers ───────────────────────────────────────────────────────────── + + _scrollBottom(id) { + const out = document.getElementById(`terminal-out-${id}`); + if (out) out.scrollTop = out.scrollHeight; + } + + _setInputEnabled(id, enabled) { + const spinner = document.getElementById(`terminal-spinner-${id}`); + if (spinner) spinner.classList.toggle('visible', !enabled); + // Keep input always enabled so user can respond to interactive prompts (sudo, password, etc.) + } + + _focusInput(id) { + document.getElementById(`terminal-in-${id}`)?.focus(); + } + + _escHtml(str) { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>'); + } + + _escPrompt(cwd) { + const parts = (cwd || '').replace(/\\/g, '/').split('/').filter(Boolean); + return parts.length ? parts[parts.length - 1] : '~'; + } + + // ─── Shell selector dropdown ───────────────────────────────────────────── + + toggleShellDropdown() { + if (this._shellDropdownOpen) { + this._closeShellDropdown(); + } else { + this._openShellDropdown(); + } + } + + _openShellDropdown() { + this._closeShellDropdown(); + + const btn = document.getElementById('terminal-shell-btn'); + if (!btn) return; + const rect = btn.getBoundingClientRect(); + + const dd = document.createElement('div'); + dd.className = 'terminal-shell-dropdown'; + dd.id = 'terminal-shell-dropdown'; + dd.style.cssText = `bottom:${window.innerHeight - rect.top + 4}px; left:${rect.left}px;`; + + dd.innerHTML = this.shells.map(s => ` +
        + + + + + ${this._escHtml(s.name)} +
        + `).join(''); + + dd.addEventListener('click', (e) => { + const item = e.target.closest('[data-id]'); + if (!item) return; + const sid = item.dataset.id; + const spath = item.dataset.path || null; + this.currentShellId = sid; + this.currentShellPath = spath; + const btnLabel = document.getElementById('terminal-shell-label'); + const s = this.shells.find(x => x.id === sid); + if (btnLabel && s) btnLabel.textContent = s.name; + this._closeShellDropdown(); + this.createTerminal(sid, spath || null); + }); + + document.body.appendChild(dd); + this._shellDropdownOpen = true; + } + + _closeShellDropdown() { + document.getElementById('terminal-shell-dropdown')?.remove(); + this._shellDropdownOpen = false; + } + + // ─── Vertical resize ───────────────────────────────────────────────────── + + _setupTerminalResizer() { + const handle = document.getElementById('terminal-resizer-top'); + const panel = document.getElementById('terminalPanel'); + if (!handle || !panel) return; + + let startY = 0; + let startH = 0; + + const onMove = (e) => { + const dy = startY - e.clientY; + const newH = Math.max(120, Math.min(window.innerHeight * 0.72, startH + dy)); + panel.style.height = newH + 'px'; + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + }; + + handle.addEventListener('mousedown', (e) => { + e.preventDefault(); + startY = e.clientY; + startH = panel.offsetHeight; + document.body.style.cursor = 'ns-resize'; + document.body.style.userSelect = 'none'; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + } + + + destroy() { + if (this._dataUnsub) window.api.removeListener('terminal-data', this._dataUnsub); + if (this._doneUnsub) window.api.removeListener('terminal-command-done', this._doneUnsub); + } +} diff --git a/src/renderer/managers/VisualyzerManager.js b/src/renderer/managers/VisualyzerManager.js index 43a955c..088ef55 100644 --- a/src/renderer/managers/VisualyzerManager.js +++ b/src/renderer/managers/VisualyzerManager.js @@ -1,956 +1,956 @@ -;(function() { -/** - * Visualyzer Manager - Interactive Graph Visualization - * Uses D3.js for force-directed graph rendering - */ - -class VisualyzerManager { - constructor() { - this.currentGraph = null; - this.parsedData = null; - this.svg = null; - this.simulation = null; - this.selectedNode = null; - this.expandedNodes = new Set(); - this.visibleNodes = new Set(); - this.visibleEdges = new Set(); - - // Color mapping for node types - this.typeColors = { - 'container': { fill: '#ffa657', stroke: '#f0883e', label: 'Container' }, - 'F': { fill: '#58a6ff', stroke: '#1f6feb', label: 'Function' }, - 'O': { fill: '#56d364', stroke: '#238636', label: 'Object' }, - 'default': { fill: '#8b949e', stroke: '#484f58', label: 'Other' } - }; - - this.initializeElements(); - this.setupEventListeners(); - } - - /** - * Get node color based on type - * @param {Object} node - Node data - * @returns {Object} Color object with fill and stroke - */ - getNodeColor(node) { - if (node.isContainer) { - return this.typeColors['container']; - } - if (node.type && this.typeColors[node.type]) { - return this.typeColors[node.type]; - } - return this.typeColors['default']; - } - - /** - * Initialize DOM elements - */ - initializeElements() { - this.dropzone = document.querySelector('.visualyzer-dropzone'); - this.canvas = document.querySelector('.visualyzer-canvas'); - this.controls = document.querySelector('.visualyzer-controls'); - this.info = document.querySelector('.visualyzer-info'); - this.filename = document.querySelector('.visualyzer-filename'); - this.stats = document.querySelector('.visualyzer-stats'); - } - - /** - * Setup event listeners - */ - setupEventListeners() { - // Drag and drop - this.dropzone.addEventListener('dragover', (e) => { - e.preventDefault(); - this.dropzone.classList.add('dragover'); - }); - - this.dropzone.addEventListener('dragleave', () => { - this.dropzone.classList.remove('dragover'); - }); - - this.dropzone.addEventListener('drop', (e) => { - e.preventDefault(); - this.dropzone.classList.remove('dragover'); - - const file = e.dataTransfer.files[0]; - if (file && file.name.endsWith('.dot')) { - this.handleFile(file); - } else { - alert('Please drop a .dot file'); - } - }); - - // File input - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = '.dot'; - fileInput.style.display = 'none'; - fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - this.handleFile(file); - } - }); - document.body.appendChild(fileInput); - - this.dropzone.addEventListener('click', () => { - fileInput.click(); - }); - } - - /** - * Handle dropped or selected file - * @param {File} file - DOT file - * @returns {Promise} - */ - async handleFile(file) { - try { - const content = await this.readFile(file); - await this.renderGraph(content, file.name); - } catch (error) { - this.showError(`Error rendering graph: ${error.message}`); - console.error('Error:', error); - } - } - - /** - * Read file content - * @param {File} file - File to read - * @returns {Promise} File content - */ - readFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => resolve(e.target.result); - reader.onerror = (e) => reject(new Error('Failed to read file')); - reader.readAsText(file); - }); - } - - /** - * Parse DOT content into graph data - * @param {string} dotContent - DOT file content - * @returns {Object} Parsed graph data - */ - parseDotContent(dotContent) { - const nodes = new Map(); - const edges = []; - - // Extract node definitions with labels - const nodeRegex = /(\w+)\s*\[label="({[^"]+}|[^"]+)"\]/g; - let match; - - while ((match = nodeRegex.exec(dotContent)) !== null) { - const nodeId = match[1]; - const label = match[2]; - - // Parse label - const parsedLabel = this.parseLabel(label); - - // Create main node - nodes.set(nodeId, { - id: nodeId, - label: parsedLabel.title, - isContainer: parsedLabel.items.length > 0, - type: null, - address: null - }); - - // Create child nodes from fields - parsedLabel.items.forEach((field, index) => { - const childId = `${nodeId}_field_${index}`; - nodes.set(childId, { - id: childId, - label: field.name, - isContainer: false, - type: field.address, - address: field.value - }); - - // Create edge from parent to child - edges.push({ - source: nodeId, - target: childId - }); - }); - } - - // Extract explicit edges (connections) - const edgeRegex = /(\w+)\s*->\s*(\w+)/g; - while ((match = edgeRegex.exec(dotContent)) !== null) { - edges.push({ - source: match[1], - target: match[2] - }); - } - - return { - nodes: Array.from(nodes.values()), - edges: edges - }; - } - - /** - * Parse label into structured data - * @param {string} label - Node label - * @returns {Object} Parsed label data - */ - /** - * Parse node label text - * @param {string} label - Label text to parse - * @returns {Object} Parsed label with title and fields - */ - parseLabel(label) { - // Check if it's a record-style label - if (label.startsWith('{') && label.endsWith('}')) { - return this.parseRecordLabel(label); - } - - // Simple label - return { - title: label, - items: [] - }; - } - - /** - * Parse record-style label into structured data - * @param {string} label - Record label like "{Title|{field1|value1}|{field2|value2}}" - * @returns {Object} Parsed label data - */ - /** - * Parse record-style label into structured fields - * @param {string} label - Record label text - * @returns {Object} Object with title and fields array - */ - parseRecordLabel(label) { - // Remove outer braces - let content = label.slice(1, -1); - - // Split by top-level pipes (not inside braces) - const parts = []; - let depth = 0; - let current = ''; - - for (let i = 0; i < content.length; i++) { - const char = content[i]; - - if (char === '\\' && i + 1 < content.length) { - // Skip escaped character - current += content[i + 1]; - i++; - continue; - } - - if (char === '{') depth++; - else if (char === '}') depth--; - else if (char === '|' && depth === 0) { - parts.push(current); - current = ''; - continue; - } - - current += char; - } - if (current) parts.push(current); - - const title = parts[0] || 'Unknown'; - const items = []; - - // Parse remaining parts as field records - for (let i = 1; i < parts.length; i++) { - const part = parts[i].trim(); - if (part.startsWith('{') && part.endsWith('}')) { - const fieldContent = part.slice(1, -1); - const fieldParts = fieldContent.split('|').map(p => p.trim()); - - items.push({ - name: fieldParts[0] || '', - address: fieldParts[1] || '', - value: fieldParts[2] || '' - }); - } - } - - return { title, items }; - } - - /** - * Render interactive graph using D3.js - * @param {string} dotContent - DOT file content - * @param {string} filename - File name - */ - async renderGraph(dotContent, filename) { - try { - // Check if D3 is loaded - if (typeof d3 === 'undefined') { - throw new Error('D3.js library not loaded. Please check your internet connection.'); - } - - // Basic validation - const trimmedContent = dotContent.trim(); - if (!trimmedContent) { - throw new Error('DOT file is empty'); - } - - // Parse DOT content - const graphData = this.parseDotContent(dotContent); - - if (graphData.nodes.length === 0) { - throw new Error('No nodes found in DOT file'); - } - - this.parsedData = graphData; - this.currentGraph = dotContent; - - // Create D3 force-directed graph - this.createForceGraph(graphData, filename); - - // Show controls and info - this.dropzone.style.display = 'none'; - this.controls.style.display = 'flex'; - this.info.style.display = 'flex'; - - // Update info - this.filename.textContent = filename; - this.stats.textContent = `${graphData.nodes.length} nodes, ${graphData.edges.length} connections`; - - } catch (error) { - throw error; - } - } - - /** - * Create force-directed graph with D3.js - * @param {Object} data - Graph data - * @param {string} filename - File name - */ - /** - * Create D3.js force-directed graph visualization - * @param {Object} data - Graph data with nodes and edges - * @param {string} filename - Name of the DOT file - * @returns {void} - */ - createForceGraph(data, filename) { - // Clear canvas - this.canvas.innerHTML = ''; - - // Store full graph data - this.fullGraphData = data; - - // Find root nodes (nodes with no incoming edges) - const childNodeIds = new Set(data.edges.map(e => e.target)); - const rootNodes = data.nodes.filter(n => !childNodeIds.has(n.id)); - - // If no clear root, use first few nodes - if (rootNodes.length === 0) { - rootNodes.push(...data.nodes.slice(0, Math.min(3, data.nodes.length))); - } - - // Initialize visible nodes with roots - this.visibleNodes.clear(); - rootNodes.forEach(n => this.visibleNodes.add(n.id)); - - // Get canvas dimensions - const width = this.canvas.clientWidth; - const height = this.canvas.clientHeight; - - // Create SVG - const svg = d3.select(this.canvas) - .append('svg') - .attr('width', width) - .attr('height', height) - .attr('viewBox', [0, 0, width, height]); - - // Add zoom behavior - const g = svg.append('g'); - - const zoom = d3.zoom() - .scaleExtent([0.1, 4]) - .on('zoom', (event) => { - g.attr('transform', event.transform); - }); - - svg.call(zoom); - - // Store zoom and svg references - this.zoom = zoom; - this.svg = svg; - - // Create arrow markers for edges - svg.append('defs').append('marker') - .attr('id', 'arrowhead') - .attr('viewBox', '-0 -5 10 10') - .attr('refX', 25) - .attr('refY', 0) - .attr('orient', 'auto') - .attr('markerWidth', 8) - .attr('markerHeight', 8) - .append('svg:path') - .attr('d', 'M 0,-5 L 10 ,0 L 0,5') - .attr('fill', '#848d97'); - - // Store references for updates - this.g = g; - this.width = width; - this.height = height; - - // Create legend - this.createLegend(svg); - - // Initial render with only visible nodes - this.updateGraph(); - } - - /** - * Create legend for node types - * @param {Object} svg - SVG element - */ - /** - * Create legend showing node type colors - * @param {Object} svg - D3.js SVG selection - * @returns {void} - */ - createLegend(svg) { - const legend = svg.append('g') - .attr('class', 'legend') - .attr('transform', `translate(20, ${this.height - 120})`); - - // Background - legend.append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', 120) - .attr('height', 100) - .attr('fill', '#161b22') - .attr('stroke', '#30363d') - .attr('stroke-width', 1) - .attr('rx', 4); - - // Title - legend.append('text') - .attr('x', 10) - .attr('y', 18) - .attr('fill', '#f0f6fc') - .attr('font-size', '12px') - .attr('font-weight', 'bold') - .text('Node Types'); - - // Legend items - const items = [ - { type: 'container', label: 'Container' }, - { type: 'F', label: 'Function' }, - { type: 'O', label: 'Object' }, - { type: 'default', label: 'Other' } - ]; - - items.forEach((item, index) => { - const yPos = 35 + index * 20; - const colors = this.typeColors[item.type]; - - // Circle - legend.append('circle') - .attr('cx', 15) - .attr('cy', yPos) - .attr('r', 6) - .attr('fill', colors.fill) - .attr('stroke', colors.stroke) - .attr('stroke-width', 1.5); - - // Label - legend.append('text') - .attr('x', 28) - .attr('y', yPos + 4) - .attr('fill', '#c9d1d9') - .attr('font-size', '11px') - .text(item.label); - }); - } - - /** - * Update graph to show only visible nodes and edges - */ - /** - * Update graph visualization with current visible nodes and edges - * @returns {void} - */ - updateGraph() { - const data = this.fullGraphData; - - // Filter visible nodes - const visibleNodesData = data.nodes.filter(n => this.visibleNodes.has(n.id)); - - // Filter visible edges (both source and target must be visible) - const visibleEdgesData = data.edges.filter(e => - this.visibleNodes.has(e.source.id || e.source) && - this.visibleNodes.has(e.target.id || e.target) - ); - - // Create or update force simulation - if (this.simulation) { - this.simulation.stop(); - } - - this.simulation = d3.forceSimulation(visibleNodesData) - .force('link', d3.forceLink(visibleEdgesData) - .id(d => d.id) - .distance(150)) - .force('charge', d3.forceManyBody().strength(-400)) - .force('center', d3.forceCenter(this.width / 2, this.height / 2)) - .force('collision', d3.forceCollide().radius(40)); - - // Update edges - const link = this.g.selectAll('line') - .data(visibleEdgesData, d => `${d.source.id || d.source}-${d.target.id || d.target}`); - - link.exit().remove(); - - const linkEnter = link.enter().append('line') - .attr('stroke', '#848d97') - .attr('stroke-width', 2) - .attr('marker-end', 'url(#arrowhead)') - .attr('opacity', 0); - - const linkAll = linkEnter.merge(link); - - linkAll.transition() - .duration(300) - .attr('opacity', 1); - - // Update nodes - const node = this.g.selectAll('g.node') - .data(visibleNodesData, d => d.id); - - node.exit().remove(); - - const nodeEnter = node.enter().append('g') - .attr('class', 'node') - .call(d3.drag() - .on('start', (event, d) => this.dragStarted(event, d, this.simulation)) - .on('drag', (event, d) => this.dragged(event, d)) - .on('end', (event, d) => this.dragEnded(event, d, this.simulation))); - - // Add circles to new nodes - nodeEnter.append('circle') - .attr('r', 0) - .attr('fill', d => this.getNodeColor(d).fill) - .attr('stroke', d => this.getNodeColor(d).stroke) - .attr('stroke-width', 2); - - // Add labels to new nodes - nodeEnter.append('text') - .text(d => d.label.length > 20 ? d.label.substring(0, 17) + '...' : d.label) - .attr('x', 0) - .attr('y', 35) - .attr('text-anchor', 'middle') - .attr('fill', '#f0f6fc') - .attr('font-size', '12px') - .style('pointer-events', 'none') - .attr('opacity', 0); - - // Add expand indicator for nodes with children - nodeEnter.each((d, i, nodes) => { - const hasChildren = this.hasUnexpandedChildren(d); - - if (hasChildren) { - // Green + for expandable nodes - d3.select(nodes[i]).append('circle') - .attr('class', 'expand-indicator') - .attr('r', 6) - .attr('cx', 18) - .attr('cy', -18) - .attr('fill', '#238636') - .attr('stroke', '#2ea043') - .attr('stroke-width', 1) - .attr('opacity', 0); - - d3.select(nodes[i]).append('text') - .attr('class', 'expand-icon') - .attr('x', 18) - .attr('y', -14) - .attr('text-anchor', 'middle') - .attr('fill', 'white') - .attr('font-size', '10px') - .attr('font-weight', 'bold') - .style('pointer-events', 'none') - .text('+') - .attr('opacity', 0); - } - }); - - const nodeAll = nodeEnter.merge(node); - - // Animate circles - nodeAll.select('circle:not(.expand-indicator)') - .transition() - .duration(300) - .attr('r', d => this.expandedNodes.has(d.id) ? 24 : 20) - .attr('fill', d => { - if (this.expandedNodes.has(d.id)) return '#a371f7'; // Purple when expanded - return this.getNodeColor(d).fill; - }) - .attr('stroke', d => { - if (this.expandedNodes.has(d.id)) return '#8957e5'; - return this.getNodeColor(d).stroke; - }); - - // Animate labels - nodeAll.select('text:not(.expand-icon)') - .transition() - .duration(300) - .attr('opacity', 1); - - // Update expand indicators - nodeAll.each((d, i, nodes) => { - const hasChildren = this.hasUnexpandedChildren(d); - const nodeGroup = d3.select(nodes[i]); - - if (hasChildren) { - nodeGroup.select('.expand-indicator') - .transition() - .duration(300) - .attr('opacity', 1); - - nodeGroup.select('.expand-icon') - .transition() - .duration(300) - .attr('opacity', 1); - } else { - // No children, remove indicators - nodeGroup.select('.expand-indicator').remove(); - nodeGroup.select('.expand-icon').remove(); - } - }); - - // Add click handler to nodes - nodeAll.on('click', (event, d) => { - event.stopPropagation(); - this.toggleNodeExpansion(d); - }); - - // Add hover effects - nodeAll.on('mouseenter', function(event, d) { - if (!d._expanded) { - const baseColor = d3.select(this).select('circle:not(.expand-indicator)').attr('fill'); - d3.select(this).select('circle:not(.expand-indicator)') - .attr('r', 24); - } - }); - - nodeAll.on('mouseleave', function(event, d) { - if (!d._expanded) { - d3.select(this).select('circle:not(.expand-indicator)') - .attr('r', 20); - } - }); - - // Update positions on simulation tick - this.simulation.on('tick', () => { - linkAll - .attr('x1', d => d.source.x) - .attr('y1', d => d.source.y) - .attr('x2', d => d.target.x) - .attr('y2', d => d.target.y); - - nodeAll.attr('transform', d => `translate(${d.x},${d.y})`); - }); - } - - /** - * Check if node has unexpanded children - * @param {Object} node - Node to check - * @returns {boolean} Has unexpanded children - */ - /** - * Check if node has children that haven't been expanded - * @param {Object} node - Node to check - * @returns {boolean} True if node has unexpanded children - */ - hasUnexpandedChildren(node) { - const children = this.fullGraphData.edges - .filter(e => (e.source.id || e.source) === node.id) - .map(e => e.target.id || e.target); - - return children.some(childId => !this.visibleNodes.has(childId)); - } - - /** - * Toggle node expansion - * @param {Object} node - Node to toggle - */ - /** - * Toggle node expansion state (expand/collapse children) - * @param {Object} node - Node to toggle - * @returns {void} - */ - toggleNodeExpansion(node) { - const hasChildren = this.hasUnexpandedChildren(node); - - // If node has children, expand/collapse - if (hasChildren || this.expandedNodes.has(node.id)) { - if (this.expandedNodes.has(node.id)) { - // Collapse: remove children - this.collapseNode(node); - } else { - // Expand: show children - this.expandNode(node); - } - - // Update the graph - this.updateGraph(); - } - // If node is a leaf (no children), show its metadata - else if (node.type || node.address) { - this.showNodeMetadata(node); - } - - // Update stats - const visibleCount = this.visibleNodes.size; - const totalCount = this.fullGraphData.nodes.length; - this.stats.textContent = `Showing ${visibleCount} of ${totalCount} nodes`; - } - - /** - * Expand node to show its children - * @param {Object} node - Node to expand - */ - /** - * Expand node to show its children - * @param {Object} node - Node to expand - * @returns {void} - */ - expandNode(node) { - this.expandedNodes.add(node.id); - node._expanded = true; - - // Find all children - const children = this.fullGraphData.edges - .filter(e => (e.source.id || e.source) === node.id) - .map(e => e.target.id || e.target); - - // Add children to visible nodes - children.forEach(childId => this.visibleNodes.add(childId)); - } - - /** - * Collapse node to hide its children - * @param {Object} node - Node to collapse - */ - /** - * Collapse node and hide its children recursively - * @param {Object} node - Node to collapse - * @returns {void} - */ - collapseNode(node) { - this.expandedNodes.delete(node.id); - node._expanded = false; - - // Find all descendants (recursive) - const toRemove = new Set(); - const findDescendants = (nodeId) => { - const children = this.fullGraphData.edges - .filter(e => (e.source.id || e.source) === nodeId) - .map(e => e.target.id || e.target); - - children.forEach(childId => { - toRemove.add(childId); - if (this.expandedNodes.has(childId)) { - findDescendants(childId); - } - }); - }; - - findDescendants(node.id); - - // Remove descendants from visible nodes - toRemove.forEach(nodeId => { - this.visibleNodes.delete(nodeId); - this.expandedNodes.delete(nodeId); - }); - } - - /** - * Show node metadata (type and address) as overlay - * @param {Object} node - Node data - */ - /** - * Display node metadata in the info panel - * @param {Object} node - Node whose metadata to display - * @returns {void} - */ - showNodeMetadata(node) { - // Remove existing tooltip - const existingTooltip = this.canvas.querySelector('.node-metadata-tooltip'); - if (existingTooltip) { - existingTooltip.remove(); - } - - // Create tooltip - const tooltip = document.createElement('div'); - tooltip.className = 'node-metadata-tooltip'; - - const title = document.createElement('div'); - title.className = 'tooltip-title'; - title.textContent = node.label; - tooltip.appendChild(title); - - if (node.type) { - const typeDiv = document.createElement('div'); - typeDiv.className = 'tooltip-field'; - typeDiv.innerHTML = `Type: ${escapeHtml(node.type)}`; - tooltip.appendChild(typeDiv); - } - - if (node.address) { - const addrDiv = document.createElement('div'); - addrDiv.className = 'tooltip-field'; - addrDiv.innerHTML = `Address: ${escapeHtml(node.address)}`; - tooltip.appendChild(addrDiv); - } - - this.canvas.appendChild(tooltip); - - // Auto-dismiss after 5 seconds - setTimeout(() => { - if (tooltip.parentNode) { - tooltip.remove(); - } - }, 5000); - } - - /** - * Drag event handlers for D3 - */ - /** - * Handle drag start event for node - * @param {Object} event - D3 drag event - * @param {Object} d - Node data - * @param {Object} simulation - D3 simulation - * @returns {void} - */ - dragStarted(event, d, simulation) { - if (!event.active) simulation.alphaTarget(0.3).restart(); - d.fx = d.x; - d.fy = d.y; - } - - /** - * Handle drag event for node - * @param {Object} event - D3 drag event - * @param {Object} d - Node data - * @returns {void} - */ - dragged(event, d) { - d.fx = event.x; - d.fy = event.y; - } - - /** - * Handle drag end event for node - * @param {Object} event - D3 drag event - * @param {Object} d - Node data - * @param {Object} simulation - D3 simulation - * @returns {void} - */ - dragEnded(event, d, simulation) { - if (!event.active) simulation.alphaTarget(0); - d.fx = null; - d.fy = null; - } - - /** - * Zoom controls - */ - /** - * Zoom in on the graph - * @returns {void} - */ - zoomIn() { - if (!this.svg || !this.zoom) return; - this.svg.transition().duration(300).call( - this.zoom.scaleBy, - 1.3 - ); - } - - /** - * Zoom out on the graph - * @returns {void} - */ - zoomOut() { - if (!this.svg || !this.zoom) return; - this.svg.transition().duration(300).call( - this.zoom.scaleBy, - 0.7 - ); - } - - /** - * Reset zoom to default level - * @returns {void} - */ - resetZoom() { - if (!this.svg || !this.zoom) return; - this.svg.transition().duration(300).call( - this.zoom.transform, - d3.zoomIdentity - ); - } - - /** - * Clear current visualization - */ - /** - * Clear the graph and return to dropzone - * @returns {void} - */ - clear() { - this.canvas.innerHTML = ''; - this.currentGraph = null; - this.parsedData = null; - this.fullGraphData = null; - this.svg = null; - this.zoom = null; - this.expandedNodes.clear(); - this.visibleNodes.clear(); - this.visibleEdges.clear(); - - if (this.simulation) { - this.simulation.stop(); - this.simulation = null; - } - - // Show dropzone again - if (this.dropzone) { - this.dropzone.style.display = 'flex'; - } - - // Hide info panel - if (this.info) { - this.info.style.display = 'none'; - this.info.classList.remove('error'); - } - } - - /** - * Show error message - * @param {string} message - Error message - */ - /** - * Display error message to user - * @param {string} message - Error message to display - * @returns {void} - */ - showError(message) { - this.info.classList.add('error'); - this.stats.textContent = message; - } -} - -if (typeof window !== 'undefined') { - window.VisualyzerManager = VisualyzerManager; -} -if (typeof module !== 'undefined' && module.exports) { - module.exports = VisualyzerManager; -} -})(); +;(function() { +/** + * Visualyzer Manager - Interactive Graph Visualization + * Uses D3.js for force-directed graph rendering + */ + +class VisualyzerManager { + constructor() { + this.currentGraph = null; + this.parsedData = null; + this.svg = null; + this.simulation = null; + this.selectedNode = null; + this.expandedNodes = new Set(); + this.visibleNodes = new Set(); + this.visibleEdges = new Set(); + + // Color mapping for node types + this.typeColors = { + 'container': { fill: '#ffa657', stroke: '#f0883e', label: 'Container' }, + 'F': { fill: '#58a6ff', stroke: '#1f6feb', label: 'Function' }, + 'O': { fill: '#56d364', stroke: '#238636', label: 'Object' }, + 'default': { fill: '#8b949e', stroke: '#484f58', label: 'Other' } + }; + + this.initializeElements(); + this.setupEventListeners(); + } + + /** + * Get node color based on type + * @param {Object} node - Node data + * @returns {Object} Color object with fill and stroke + */ + getNodeColor(node) { + if (node.isContainer) { + return this.typeColors['container']; + } + if (node.type && this.typeColors[node.type]) { + return this.typeColors[node.type]; + } + return this.typeColors['default']; + } + + /** + * Initialize DOM elements + */ + initializeElements() { + this.dropzone = document.querySelector('.visualyzer-dropzone'); + this.canvas = document.querySelector('.visualyzer-canvas'); + this.controls = document.querySelector('.visualyzer-controls'); + this.info = document.querySelector('.visualyzer-info'); + this.filename = document.querySelector('.visualyzer-filename'); + this.stats = document.querySelector('.visualyzer-stats'); + } + + /** + * Setup event listeners + */ + setupEventListeners() { + // Drag and drop + this.dropzone.addEventListener('dragover', (e) => { + e.preventDefault(); + this.dropzone.classList.add('dragover'); + }); + + this.dropzone.addEventListener('dragleave', () => { + this.dropzone.classList.remove('dragover'); + }); + + this.dropzone.addEventListener('drop', (e) => { + e.preventDefault(); + this.dropzone.classList.remove('dragover'); + + const file = e.dataTransfer.files[0]; + if (file && file.name.endsWith('.dot')) { + this.handleFile(file); + } else { + alert('Please drop a .dot file'); + } + }); + + // File input + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.dot'; + fileInput.style.display = 'none'; + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + this.handleFile(file); + } + }); + document.body.appendChild(fileInput); + + this.dropzone.addEventListener('click', () => { + fileInput.click(); + }); + } + + /** + * Handle dropped or selected file + * @param {File} file - DOT file + * @returns {Promise} + */ + async handleFile(file) { + try { + const content = await this.readFile(file); + await this.renderGraph(content, file.name); + } catch (error) { + this.showError(`Error rendering graph: ${error.message}`); + console.error('Error:', error); + } + } + + /** + * Read file content + * @param {File} file - File to read + * @returns {Promise} File content + */ + readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => resolve(e.target.result); + reader.onerror = (e) => reject(new Error('Failed to read file')); + reader.readAsText(file); + }); + } + + /** + * Parse DOT content into graph data + * @param {string} dotContent - DOT file content + * @returns {Object} Parsed graph data + */ + parseDotContent(dotContent) { + const nodes = new Map(); + const edges = []; + + // Extract node definitions with labels + const nodeRegex = /(\w+)\s*\[label="({[^"]+}|[^"]+)"\]/g; + let match; + + while ((match = nodeRegex.exec(dotContent)) !== null) { + const nodeId = match[1]; + const label = match[2]; + + // Parse label + const parsedLabel = this.parseLabel(label); + + // Create main node + nodes.set(nodeId, { + id: nodeId, + label: parsedLabel.title, + isContainer: parsedLabel.items.length > 0, + type: null, + address: null + }); + + // Create child nodes from fields + parsedLabel.items.forEach((field, index) => { + const childId = `${nodeId}_field_${index}`; + nodes.set(childId, { + id: childId, + label: field.name, + isContainer: false, + type: field.address, + address: field.value + }); + + // Create edge from parent to child + edges.push({ + source: nodeId, + target: childId + }); + }); + } + + // Extract explicit edges (connections) + const edgeRegex = /(\w+)\s*->\s*(\w+)/g; + while ((match = edgeRegex.exec(dotContent)) !== null) { + edges.push({ + source: match[1], + target: match[2] + }); + } + + return { + nodes: Array.from(nodes.values()), + edges: edges + }; + } + + /** + * Parse label into structured data + * @param {string} label - Node label + * @returns {Object} Parsed label data + */ + /** + * Parse node label text + * @param {string} label - Label text to parse + * @returns {Object} Parsed label with title and fields + */ + parseLabel(label) { + // Check if it's a record-style label + if (label.startsWith('{') && label.endsWith('}')) { + return this.parseRecordLabel(label); + } + + // Simple label + return { + title: label, + items: [] + }; + } + + /** + * Parse record-style label into structured data + * @param {string} label - Record label like "{Title|{field1|value1}|{field2|value2}}" + * @returns {Object} Parsed label data + */ + /** + * Parse record-style label into structured fields + * @param {string} label - Record label text + * @returns {Object} Object with title and fields array + */ + parseRecordLabel(label) { + // Remove outer braces + let content = label.slice(1, -1); + + // Split by top-level pipes (not inside braces) + const parts = []; + let depth = 0; + let current = ''; + + for (let i = 0; i < content.length; i++) { + const char = content[i]; + + if (char === '\\' && i + 1 < content.length) { + // Skip escaped character + current += content[i + 1]; + i++; + continue; + } + + if (char === '{') depth++; + else if (char === '}') depth--; + else if (char === '|' && depth === 0) { + parts.push(current); + current = ''; + continue; + } + + current += char; + } + if (current) parts.push(current); + + const title = parts[0] || 'Unknown'; + const items = []; + + // Parse remaining parts as field records + for (let i = 1; i < parts.length; i++) { + const part = parts[i].trim(); + if (part.startsWith('{') && part.endsWith('}')) { + const fieldContent = part.slice(1, -1); + const fieldParts = fieldContent.split('|').map(p => p.trim()); + + items.push({ + name: fieldParts[0] || '', + address: fieldParts[1] || '', + value: fieldParts[2] || '' + }); + } + } + + return { title, items }; + } + + /** + * Render interactive graph using D3.js + * @param {string} dotContent - DOT file content + * @param {string} filename - File name + */ + async renderGraph(dotContent, filename) { + try { + // Check if D3 is loaded + if (typeof d3 === 'undefined') { + throw new Error('D3.js library not loaded. Please check your internet connection.'); + } + + // Basic validation + const trimmedContent = dotContent.trim(); + if (!trimmedContent) { + throw new Error('DOT file is empty'); + } + + // Parse DOT content + const graphData = this.parseDotContent(dotContent); + + if (graphData.nodes.length === 0) { + throw new Error('No nodes found in DOT file'); + } + + this.parsedData = graphData; + this.currentGraph = dotContent; + + // Create D3 force-directed graph + this.createForceGraph(graphData, filename); + + // Show controls and info + this.dropzone.style.display = 'none'; + this.controls.style.display = 'flex'; + this.info.style.display = 'flex'; + + // Update info + this.filename.textContent = filename; + this.stats.textContent = `${graphData.nodes.length} nodes, ${graphData.edges.length} connections`; + + } catch (error) { + throw error; + } + } + + /** + * Create force-directed graph with D3.js + * @param {Object} data - Graph data + * @param {string} filename - File name + */ + /** + * Create D3.js force-directed graph visualization + * @param {Object} data - Graph data with nodes and edges + * @param {string} filename - Name of the DOT file + * @returns {void} + */ + createForceGraph(data, filename) { + // Clear canvas + this.canvas.innerHTML = ''; + + // Store full graph data + this.fullGraphData = data; + + // Find root nodes (nodes with no incoming edges) + const childNodeIds = new Set(data.edges.map(e => e.target)); + const rootNodes = data.nodes.filter(n => !childNodeIds.has(n.id)); + + // If no clear root, use first few nodes + if (rootNodes.length === 0) { + rootNodes.push(...data.nodes.slice(0, Math.min(3, data.nodes.length))); + } + + // Initialize visible nodes with roots + this.visibleNodes.clear(); + rootNodes.forEach(n => this.visibleNodes.add(n.id)); + + // Get canvas dimensions + const width = this.canvas.clientWidth; + const height = this.canvas.clientHeight; + + // Create SVG + const svg = d3.select(this.canvas) + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]); + + // Add zoom behavior + const g = svg.append('g'); + + const zoom = d3.zoom() + .scaleExtent([0.1, 4]) + .on('zoom', (event) => { + g.attr('transform', event.transform); + }); + + svg.call(zoom); + + // Store zoom and svg references + this.zoom = zoom; + this.svg = svg; + + // Create arrow markers for edges + svg.append('defs').append('marker') + .attr('id', 'arrowhead') + .attr('viewBox', '-0 -5 10 10') + .attr('refX', 25) + .attr('refY', 0) + .attr('orient', 'auto') + .attr('markerWidth', 8) + .attr('markerHeight', 8) + .append('svg:path') + .attr('d', 'M 0,-5 L 10 ,0 L 0,5') + .attr('fill', '#848d97'); + + // Store references for updates + this.g = g; + this.width = width; + this.height = height; + + // Create legend + this.createLegend(svg); + + // Initial render with only visible nodes + this.updateGraph(); + } + + /** + * Create legend for node types + * @param {Object} svg - SVG element + */ + /** + * Create legend showing node type colors + * @param {Object} svg - D3.js SVG selection + * @returns {void} + */ + createLegend(svg) { + const legend = svg.append('g') + .attr('class', 'legend') + .attr('transform', `translate(20, ${this.height - 120})`); + + // Background + legend.append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', 120) + .attr('height', 100) + .attr('fill', '#161b22') + .attr('stroke', '#30363d') + .attr('stroke-width', 1) + .attr('rx', 4); + + // Title + legend.append('text') + .attr('x', 10) + .attr('y', 18) + .attr('fill', '#f0f6fc') + .attr('font-size', '12px') + .attr('font-weight', 'bold') + .text('Node Types'); + + // Legend items + const items = [ + { type: 'container', label: 'Container' }, + { type: 'F', label: 'Function' }, + { type: 'O', label: 'Object' }, + { type: 'default', label: 'Other' } + ]; + + items.forEach((item, index) => { + const yPos = 35 + index * 20; + const colors = this.typeColors[item.type]; + + // Circle + legend.append('circle') + .attr('cx', 15) + .attr('cy', yPos) + .attr('r', 6) + .attr('fill', colors.fill) + .attr('stroke', colors.stroke) + .attr('stroke-width', 1.5); + + // Label + legend.append('text') + .attr('x', 28) + .attr('y', yPos + 4) + .attr('fill', '#c9d1d9') + .attr('font-size', '11px') + .text(item.label); + }); + } + + /** + * Update graph to show only visible nodes and edges + */ + /** + * Update graph visualization with current visible nodes and edges + * @returns {void} + */ + updateGraph() { + const data = this.fullGraphData; + + // Filter visible nodes + const visibleNodesData = data.nodes.filter(n => this.visibleNodes.has(n.id)); + + // Filter visible edges (both source and target must be visible) + const visibleEdgesData = data.edges.filter(e => + this.visibleNodes.has(e.source.id || e.source) && + this.visibleNodes.has(e.target.id || e.target) + ); + + // Create or update force simulation + if (this.simulation) { + this.simulation.stop(); + } + + this.simulation = d3.forceSimulation(visibleNodesData) + .force('link', d3.forceLink(visibleEdgesData) + .id(d => d.id) + .distance(150)) + .force('charge', d3.forceManyBody().strength(-400)) + .force('center', d3.forceCenter(this.width / 2, this.height / 2)) + .force('collision', d3.forceCollide().radius(40)); + + // Update edges + const link = this.g.selectAll('line') + .data(visibleEdgesData, d => `${d.source.id || d.source}-${d.target.id || d.target}`); + + link.exit().remove(); + + const linkEnter = link.enter().append('line') + .attr('stroke', '#848d97') + .attr('stroke-width', 2) + .attr('marker-end', 'url(#arrowhead)') + .attr('opacity', 0); + + const linkAll = linkEnter.merge(link); + + linkAll.transition() + .duration(300) + .attr('opacity', 1); + + // Update nodes + const node = this.g.selectAll('g.node') + .data(visibleNodesData, d => d.id); + + node.exit().remove(); + + const nodeEnter = node.enter().append('g') + .attr('class', 'node') + .call(d3.drag() + .on('start', (event, d) => this.dragStarted(event, d, this.simulation)) + .on('drag', (event, d) => this.dragged(event, d)) + .on('end', (event, d) => this.dragEnded(event, d, this.simulation))); + + // Add circles to new nodes + nodeEnter.append('circle') + .attr('r', 0) + .attr('fill', d => this.getNodeColor(d).fill) + .attr('stroke', d => this.getNodeColor(d).stroke) + .attr('stroke-width', 2); + + // Add labels to new nodes + nodeEnter.append('text') + .text(d => d.label.length > 20 ? d.label.substring(0, 17) + '...' : d.label) + .attr('x', 0) + .attr('y', 35) + .attr('text-anchor', 'middle') + .attr('fill', '#f0f6fc') + .attr('font-size', '12px') + .style('pointer-events', 'none') + .attr('opacity', 0); + + // Add expand indicator for nodes with children + nodeEnter.each((d, i, nodes) => { + const hasChildren = this.hasUnexpandedChildren(d); + + if (hasChildren) { + // Green + for expandable nodes + d3.select(nodes[i]).append('circle') + .attr('class', 'expand-indicator') + .attr('r', 6) + .attr('cx', 18) + .attr('cy', -18) + .attr('fill', '#238636') + .attr('stroke', '#2ea043') + .attr('stroke-width', 1) + .attr('opacity', 0); + + d3.select(nodes[i]).append('text') + .attr('class', 'expand-icon') + .attr('x', 18) + .attr('y', -14) + .attr('text-anchor', 'middle') + .attr('fill', 'white') + .attr('font-size', '10px') + .attr('font-weight', 'bold') + .style('pointer-events', 'none') + .text('+') + .attr('opacity', 0); + } + }); + + const nodeAll = nodeEnter.merge(node); + + // Animate circles + nodeAll.select('circle:not(.expand-indicator)') + .transition() + .duration(300) + .attr('r', d => this.expandedNodes.has(d.id) ? 24 : 20) + .attr('fill', d => { + if (this.expandedNodes.has(d.id)) return '#a371f7'; // Purple when expanded + return this.getNodeColor(d).fill; + }) + .attr('stroke', d => { + if (this.expandedNodes.has(d.id)) return '#8957e5'; + return this.getNodeColor(d).stroke; + }); + + // Animate labels + nodeAll.select('text:not(.expand-icon)') + .transition() + .duration(300) + .attr('opacity', 1); + + // Update expand indicators + nodeAll.each((d, i, nodes) => { + const hasChildren = this.hasUnexpandedChildren(d); + const nodeGroup = d3.select(nodes[i]); + + if (hasChildren) { + nodeGroup.select('.expand-indicator') + .transition() + .duration(300) + .attr('opacity', 1); + + nodeGroup.select('.expand-icon') + .transition() + .duration(300) + .attr('opacity', 1); + } else { + // No children, remove indicators + nodeGroup.select('.expand-indicator').remove(); + nodeGroup.select('.expand-icon').remove(); + } + }); + + // Add click handler to nodes + nodeAll.on('click', (event, d) => { + event.stopPropagation(); + this.toggleNodeExpansion(d); + }); + + // Add hover effects + nodeAll.on('mouseenter', function(event, d) { + if (!d._expanded) { + const baseColor = d3.select(this).select('circle:not(.expand-indicator)').attr('fill'); + d3.select(this).select('circle:not(.expand-indicator)') + .attr('r', 24); + } + }); + + nodeAll.on('mouseleave', function(event, d) { + if (!d._expanded) { + d3.select(this).select('circle:not(.expand-indicator)') + .attr('r', 20); + } + }); + + // Update positions on simulation tick + this.simulation.on('tick', () => { + linkAll + .attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y); + + nodeAll.attr('transform', d => `translate(${d.x},${d.y})`); + }); + } + + /** + * Check if node has unexpanded children + * @param {Object} node - Node to check + * @returns {boolean} Has unexpanded children + */ + /** + * Check if node has children that haven't been expanded + * @param {Object} node - Node to check + * @returns {boolean} True if node has unexpanded children + */ + hasUnexpandedChildren(node) { + const children = this.fullGraphData.edges + .filter(e => (e.source.id || e.source) === node.id) + .map(e => e.target.id || e.target); + + return children.some(childId => !this.visibleNodes.has(childId)); + } + + /** + * Toggle node expansion + * @param {Object} node - Node to toggle + */ + /** + * Toggle node expansion state (expand/collapse children) + * @param {Object} node - Node to toggle + * @returns {void} + */ + toggleNodeExpansion(node) { + const hasChildren = this.hasUnexpandedChildren(node); + + // If node has children, expand/collapse + if (hasChildren || this.expandedNodes.has(node.id)) { + if (this.expandedNodes.has(node.id)) { + // Collapse: remove children + this.collapseNode(node); + } else { + // Expand: show children + this.expandNode(node); + } + + // Update the graph + this.updateGraph(); + } + // If node is a leaf (no children), show its metadata + else if (node.type || node.address) { + this.showNodeMetadata(node); + } + + // Update stats + const visibleCount = this.visibleNodes.size; + const totalCount = this.fullGraphData.nodes.length; + this.stats.textContent = `Showing ${visibleCount} of ${totalCount} nodes`; + } + + /** + * Expand node to show its children + * @param {Object} node - Node to expand + */ + /** + * Expand node to show its children + * @param {Object} node - Node to expand + * @returns {void} + */ + expandNode(node) { + this.expandedNodes.add(node.id); + node._expanded = true; + + // Find all children + const children = this.fullGraphData.edges + .filter(e => (e.source.id || e.source) === node.id) + .map(e => e.target.id || e.target); + + // Add children to visible nodes + children.forEach(childId => this.visibleNodes.add(childId)); + } + + /** + * Collapse node to hide its children + * @param {Object} node - Node to collapse + */ + /** + * Collapse node and hide its children recursively + * @param {Object} node - Node to collapse + * @returns {void} + */ + collapseNode(node) { + this.expandedNodes.delete(node.id); + node._expanded = false; + + // Find all descendants (recursive) + const toRemove = new Set(); + const findDescendants = (nodeId) => { + const children = this.fullGraphData.edges + .filter(e => (e.source.id || e.source) === nodeId) + .map(e => e.target.id || e.target); + + children.forEach(childId => { + toRemove.add(childId); + if (this.expandedNodes.has(childId)) { + findDescendants(childId); + } + }); + }; + + findDescendants(node.id); + + // Remove descendants from visible nodes + toRemove.forEach(nodeId => { + this.visibleNodes.delete(nodeId); + this.expandedNodes.delete(nodeId); + }); + } + + /** + * Show node metadata (type and address) as overlay + * @param {Object} node - Node data + */ + /** + * Display node metadata in the info panel + * @param {Object} node - Node whose metadata to display + * @returns {void} + */ + showNodeMetadata(node) { + // Remove existing tooltip + const existingTooltip = this.canvas.querySelector('.node-metadata-tooltip'); + if (existingTooltip) { + existingTooltip.remove(); + } + + // Create tooltip + const tooltip = document.createElement('div'); + tooltip.className = 'node-metadata-tooltip'; + + const title = document.createElement('div'); + title.className = 'tooltip-title'; + title.textContent = node.label; + tooltip.appendChild(title); + + if (node.type) { + const typeDiv = document.createElement('div'); + typeDiv.className = 'tooltip-field'; + typeDiv.innerHTML = `Type: ${escapeHtml(node.type)}`; + tooltip.appendChild(typeDiv); + } + + if (node.address) { + const addrDiv = document.createElement('div'); + addrDiv.className = 'tooltip-field'; + addrDiv.innerHTML = `Address: ${escapeHtml(node.address)}`; + tooltip.appendChild(addrDiv); + } + + this.canvas.appendChild(tooltip); + + // Auto-dismiss after 5 seconds + setTimeout(() => { + if (tooltip.parentNode) { + tooltip.remove(); + } + }, 5000); + } + + /** + * Drag event handlers for D3 + */ + /** + * Handle drag start event for node + * @param {Object} event - D3 drag event + * @param {Object} d - Node data + * @param {Object} simulation - D3 simulation + * @returns {void} + */ + dragStarted(event, d, simulation) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + } + + /** + * Handle drag event for node + * @param {Object} event - D3 drag event + * @param {Object} d - Node data + * @returns {void} + */ + dragged(event, d) { + d.fx = event.x; + d.fy = event.y; + } + + /** + * Handle drag end event for node + * @param {Object} event - D3 drag event + * @param {Object} d - Node data + * @param {Object} simulation - D3 simulation + * @returns {void} + */ + dragEnded(event, d, simulation) { + if (!event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + /** + * Zoom controls + */ + /** + * Zoom in on the graph + * @returns {void} + */ + zoomIn() { + if (!this.svg || !this.zoom) return; + this.svg.transition().duration(300).call( + this.zoom.scaleBy, + 1.3 + ); + } + + /** + * Zoom out on the graph + * @returns {void} + */ + zoomOut() { + if (!this.svg || !this.zoom) return; + this.svg.transition().duration(300).call( + this.zoom.scaleBy, + 0.7 + ); + } + + /** + * Reset zoom to default level + * @returns {void} + */ + resetZoom() { + if (!this.svg || !this.zoom) return; + this.svg.transition().duration(300).call( + this.zoom.transform, + d3.zoomIdentity + ); + } + + /** + * Clear current visualization + */ + /** + * Clear the graph and return to dropzone + * @returns {void} + */ + clear() { + this.canvas.innerHTML = ''; + this.currentGraph = null; + this.parsedData = null; + this.fullGraphData = null; + this.svg = null; + this.zoom = null; + this.expandedNodes.clear(); + this.visibleNodes.clear(); + this.visibleEdges.clear(); + + if (this.simulation) { + this.simulation.stop(); + this.simulation = null; + } + + // Show dropzone again + if (this.dropzone) { + this.dropzone.style.display = 'flex'; + } + + // Hide info panel + if (this.info) { + this.info.style.display = 'none'; + this.info.classList.remove('error'); + } + } + + /** + * Show error message + * @param {string} message - Error message + */ + /** + * Display error message to user + * @param {string} message - Error message to display + * @returns {void} + */ + showError(message) { + this.info.classList.add('error'); + this.stats.textContent = message; + } +} + +if (typeof window !== 'undefined') { + window.VisualyzerManager = VisualyzerManager; +} +if (typeof module !== 'undefined' && module.exports) { + module.exports = VisualyzerManager; +} +})(); diff --git a/src/renderer/utils/debounce.js b/src/renderer/utils/debounce.js index d1427c0..084ff21 100644 --- a/src/renderer/utils/debounce.js +++ b/src/renderer/utils/debounce.js @@ -1,50 +1,50 @@ -;(function() { -/** - * Creates a debounced wrapper around a function. - * - * Returns an object with three methods: - * .call(...args) — schedule fn to run after `delay` ms (resets the timer on each call) - * .cancel() — cancel any pending invocation without calling fn - * .flush(...args) — cancel any pending invocation and call fn immediately - * - * Uses only Node.js / browser built-ins (setTimeout / clearTimeout). - * Safe to call .cancel() or .flush() even when no timer is pending. - * - * @param {Function} fn - The function to debounce. - * @param {number} delay - Debounce delay in milliseconds. - * @returns {{ call: Function, cancel: Function, flush: Function }} - */ -function debounce(fn, delay) { - let timer = null; - - return { - call(...args) { - if (timer !== null) { - clearTimeout(timer); - } - timer = setTimeout(() => { - timer = null; - fn(...args); - }, delay); - }, - - cancel() { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - }, - - flush(...args) { - if (timer !== null) { - clearTimeout(timer); - timer = null; - } - fn(...args); - } - }; -} - -if (typeof window !== 'undefined') window.debounce = debounce; -if (typeof module !== 'undefined' && module.exports) module.exports = debounce; -})(); +;(function() { +/** + * Creates a debounced wrapper around a function. + * + * Returns an object with three methods: + * .call(...args) — schedule fn to run after `delay` ms (resets the timer on each call) + * .cancel() — cancel any pending invocation without calling fn + * .flush(...args) — cancel any pending invocation and call fn immediately + * + * Uses only Node.js / browser built-ins (setTimeout / clearTimeout). + * Safe to call .cancel() or .flush() even when no timer is pending. + * + * @param {Function} fn - The function to debounce. + * @param {number} delay - Debounce delay in milliseconds. + * @returns {{ call: Function, cancel: Function, flush: Function }} + */ +function debounce(fn, delay) { + let timer = null; + + return { + call(...args) { + if (timer !== null) { + clearTimeout(timer); + } + timer = setTimeout(() => { + timer = null; + fn(...args); + }, delay); + }, + + cancel() { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + }, + + flush(...args) { + if (timer !== null) { + clearTimeout(timer); + timer = null; + } + fn(...args); + } + }; +} + +if (typeof window !== 'undefined') window.debounce = debounce; +if (typeof module !== 'undefined' && module.exports) module.exports = debounce; +})(); diff --git a/src/renderer/utils/emojiTest.js b/src/renderer/utils/emojiTest.js index 35d38a8..31ba944 100644 --- a/src/renderer/utils/emojiTest.js +++ b/src/renderer/utils/emojiTest.js @@ -1,43 +1,43 @@ -// Emoji test utility for cross-platform rendering -const _platform = (typeof window !== 'undefined' && window.api && window.api.platform) - ? window.api.platform - : (typeof require === 'function' ? require('os').platform() : 'unknown'); - -/** - * Test emoji rendering and font availability - */ -function testEmojiSupport() { - const platform = _platform; - const testEmojis = ['📁', '📄', '🔄', '🔍', '⚙️', '💾', '🎨', '🐍', '☕', '🦀']; - - console.log(`Testing emoji support on ${platform}:`); - console.log('Test emojis:', testEmojis.join(' ')); - - // Test if system has color emoji support - const emojiTestDiv = document.createElement('div'); - emojiTestDiv.style.fontFamily = 'Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji'; - emojiTestDiv.textContent = '📁'; - document.body.appendChild(emojiTestDiv); - - // Check computed style - const computedStyle = window.getComputedStyle(emojiTestDiv); - console.log('Font family resolved to:', computedStyle.fontFamily); - - document.body.removeChild(emojiTestDiv); - - return { - platform: platform, - supportedEmojis: testEmojis, - fontFamily: computedStyle.fontFamily - }; -} - -// Export for use in development -if (typeof module !== 'undefined' && module.exports) { - module.exports = { testEmojiSupport }; -} - -// Auto-run in browser environment -if (typeof window !== 'undefined') { - window.testEmojiSupport = testEmojiSupport; +// Emoji test utility for cross-platform rendering +const _platform = (typeof window !== 'undefined' && window.api && window.api.platform) + ? window.api.platform + : (typeof require === 'function' ? require('os').platform() : 'unknown'); + +/** + * Test emoji rendering and font availability + */ +function testEmojiSupport() { + const platform = _platform; + const testEmojis = ['📁', '📄', '🔄', '🔍', '⚙️', '💾', '🎨', '🐍', '☕', '🦀']; + + console.log(`Testing emoji support on ${platform}:`); + console.log('Test emojis:', testEmojis.join(' ')); + + // Test if system has color emoji support + const emojiTestDiv = document.createElement('div'); + emojiTestDiv.style.fontFamily = 'Noto Color Emoji, Segoe UI Emoji, Apple Color Emoji'; + emojiTestDiv.textContent = '📁'; + document.body.appendChild(emojiTestDiv); + + // Check computed style + const computedStyle = window.getComputedStyle(emojiTestDiv); + console.log('Font family resolved to:', computedStyle.fontFamily); + + document.body.removeChild(emojiTestDiv); + + return { + platform: platform, + supportedEmojis: testEmojis, + fontFamily: computedStyle.fontFamily + }; +} + +// Export for use in development +if (typeof module !== 'undefined' && module.exports) { + module.exports = { testEmojiSupport }; +} + +// Auto-run in browser environment +if (typeof window !== 'undefined') { + window.testEmojiSupport = testEmojiSupport; } \ No newline at end of file diff --git a/src/renderer/utils/fileTypeUtils.js b/src/renderer/utils/fileTypeUtils.js index 5a4ca23..8a4124a 100644 --- a/src/renderer/utils/fileTypeUtils.js +++ b/src/renderer/utils/fileTypeUtils.js @@ -1,123 +1,123 @@ -;(function() { -/** - * Detect file type based on file extension - * @param {string} filename - The filename to analyze - * @returns {string} - The detected file type - */ -function detectFileType(filename) { - if (!filename) return "Plain Text"; - const lower = filename.toLowerCase(); - - if (lower === "cmakelists.txt" || lower.endsWith(".cmake")) return "CMake"; - if (lower.endsWith(".c")) return "C"; - if (lower.endsWith(".cpp") || lower.endsWith(".cc") || lower.endsWith(".cxx")) return "C++"; - if (lower.endsWith(".h") || lower.endsWith(".hpp") || lower.endsWith(".hh") || lower.endsWith(".hxx")) return "C/C++ Header"; - if (lower.endsWith(".js")) return "JavaScript"; - if (lower.endsWith(".ts")) return "TypeScript"; - if (lower.endsWith(".py")) return "Python"; - if (lower.endsWith(".json")) return "JSON"; - if (lower.endsWith(".md")) return "Markdown"; - if (lower.endsWith(".txt")) return "Plain Text"; - - return "Plain Text"; -} - -/** - * Update file type status in the status bar - * @param {string} filename - The filename to analyze - */ -function updateFileTypeStatus(filename) { - const type = detectFileType(filename); - const el = document.getElementById("fileType"); - if (el) el.textContent = type; -} - -/** - * Get file icon based on extension - * @param {string} filename - The filename - * @returns {string} - Unicode emoji for file icon - */ -function getFileIcon(filename) { - const ext = filename.split('.').pop().toLowerCase(); - // Icons ship with the app (assets/icons/devicon, MIT) so the file tree works - // offline and the page can keep a strict img-src policy. - const iconMap = { - js: '../assets/icons/devicon/javascript-original.svg', - ts: '../assets/icons/devicon/typescript-original.svg', - html: '../assets/icons/devicon/html5-original.svg', - css: '../assets/icons/devicon/css3-original.svg', - py: '../assets/icons/devicon/python-original.svg', - cpp: '../assets/icons/devicon/cplusplus-original.svg', - c: '../assets/icons/devicon/c-original.svg', - cc: '../assets/icons/devicon/cplusplus-original.svg', - cxx: '../assets/icons/devicon/cplusplus-original.svg', - h: '../assets/icons/devicon/c-original.svg', - java: '../assets/icons/devicon/java-original.svg', - php: '../assets/icons/devicon/php-original.svg', - go: '../assets/icons/devicon/go-original.svg', - rs: '../assets/icons/devicon/rust-plain.svg', - md: '../assets/icons/devicon/markdown-original.svg', - json: '../assets/icons/devicon/json-original.svg', - sh: '../assets/icons/devicon/bash-original.svg' - }; - const fallbackIconMap = { - js: '🟨', - ts: '🔷', - html: '🟧', - css: '🎨', - json: '📋', - md: '📝', - py: '🐍', - cpp: '⚙️', - c: '⚙️', - h: '📄', - java: '☕', - php: '🐘', - rb: '💎', - go: '🐹', - rs: '🦀', - cc: '⚙️', - cxx: '⚙️', - hpp: '📄', - hh: '📄', - hxx: '📄' - }; - - if (iconMap[ext]) { - const safeExt = ext.replace(/[^a-z0-9]/gi, ''); - return `${safeExt} icon`; - } - - return fallbackIconMap[ext] || '📄'; -} - -/** - * Format file size for display - * @param {number} bytes - File size in bytes - * @returns {string} - Formatted file size - */ -function formatFileSize(bytes) { - if (bytes === 0) return '0 Bytes'; - const k = 1024; - const sizes = ['Bytes', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; -} - -// Make functions available globally for backward compatibility -if (typeof window !== 'undefined') { - window.detectFileType = detectFileType; - window.updateFileTypeStatus = updateFileTypeStatus; - window.getFileIcon = getFileIcon; - window.formatFileSize = formatFileSize; -} - -if (typeof module !== 'undefined' && module.exports) { - module.exports = { - detectFileType, - updateFileTypeStatus, - getFileIcon, - formatFileSize - }; -} +;(function() { +/** + * Detect file type based on file extension + * @param {string} filename - The filename to analyze + * @returns {string} - The detected file type + */ +function detectFileType(filename) { + if (!filename) return "Plain Text"; + const lower = filename.toLowerCase(); + + if (lower === "cmakelists.txt" || lower.endsWith(".cmake")) return "CMake"; + if (lower.endsWith(".c")) return "C"; + if (lower.endsWith(".cpp") || lower.endsWith(".cc") || lower.endsWith(".cxx")) return "C++"; + if (lower.endsWith(".h") || lower.endsWith(".hpp") || lower.endsWith(".hh") || lower.endsWith(".hxx")) return "C/C++ Header"; + if (lower.endsWith(".js")) return "JavaScript"; + if (lower.endsWith(".ts")) return "TypeScript"; + if (lower.endsWith(".py")) return "Python"; + if (lower.endsWith(".json")) return "JSON"; + if (lower.endsWith(".md")) return "Markdown"; + if (lower.endsWith(".txt")) return "Plain Text"; + + return "Plain Text"; +} + +/** + * Update file type status in the status bar + * @param {string} filename - The filename to analyze + */ +function updateFileTypeStatus(filename) { + const type = detectFileType(filename); + const el = document.getElementById("fileType"); + if (el) el.textContent = type; +} + +/** + * Get file icon based on extension + * @param {string} filename - The filename + * @returns {string} - Unicode emoji for file icon + */ +function getFileIcon(filename) { + const ext = filename.split('.').pop().toLowerCase(); + // Icons ship with the app (assets/icons/devicon, MIT) so the file tree works + // offline and the page can keep a strict img-src policy. + const iconMap = { + js: '../assets/icons/devicon/javascript-original.svg', + ts: '../assets/icons/devicon/typescript-original.svg', + html: '../assets/icons/devicon/html5-original.svg', + css: '../assets/icons/devicon/css3-original.svg', + py: '../assets/icons/devicon/python-original.svg', + cpp: '../assets/icons/devicon/cplusplus-original.svg', + c: '../assets/icons/devicon/c-original.svg', + cc: '../assets/icons/devicon/cplusplus-original.svg', + cxx: '../assets/icons/devicon/cplusplus-original.svg', + h: '../assets/icons/devicon/c-original.svg', + java: '../assets/icons/devicon/java-original.svg', + php: '../assets/icons/devicon/php-original.svg', + go: '../assets/icons/devicon/go-original.svg', + rs: '../assets/icons/devicon/rust-plain.svg', + md: '../assets/icons/devicon/markdown-original.svg', + json: '../assets/icons/devicon/json-original.svg', + sh: '../assets/icons/devicon/bash-original.svg' + }; + const fallbackIconMap = { + js: '🟨', + ts: '🔷', + html: '🟧', + css: '🎨', + json: '📋', + md: '📝', + py: '🐍', + cpp: '⚙️', + c: '⚙️', + h: '📄', + java: '☕', + php: '🐘', + rb: '💎', + go: '🐹', + rs: '🦀', + cc: '⚙️', + cxx: '⚙️', + hpp: '📄', + hh: '📄', + hxx: '📄' + }; + + if (iconMap[ext]) { + const safeExt = ext.replace(/[^a-z0-9]/gi, ''); + return `${safeExt} icon`; + } + + return fallbackIconMap[ext] || '📄'; +} + +/** + * Format file size for display + * @param {number} bytes - File size in bytes + * @returns {string} - Formatted file size + */ +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +// Make functions available globally for backward compatibility +if (typeof window !== 'undefined') { + window.detectFileType = detectFileType; + window.updateFileTypeStatus = updateFileTypeStatus; + window.getFileIcon = getFileIcon; + window.formatFileSize = formatFileSize; +} + +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + detectFileType, + updateFileTypeStatus, + getFileIcon, + formatFileSize + }; +} })(); \ No newline at end of file diff --git a/src/renderer/utils/syntax-config.json b/src/renderer/utils/syntax-config.json index 8a327cb..31e33aa 100644 --- a/src/renderer/utils/syntax-config.json +++ b/src/renderer/utils/syntax-config.json @@ -1,80 +1,80 @@ -{ - "c": { - "name": "C", - "fileTypes": ["C", "C/C++ Header"], - "keywords": [ - "auto", "break", "case", "char", "const", "continue", "default", "do", - "double", "else", "enum", "extern", "float", "for", "goto", "if", - "int", "long", "register", "return", "short", "signed", "sizeof", "static", - "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while" - ], - "types": [ - "int8_t", "int16_t", "int32_t", "int64_t", - "uint8_t", "uint16_t", "uint32_t", "uint64_t", - "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", - "FILE", "nullptr_t" - ], - "preprocessor": [ - "define", "undef", "include", "ifdef", "ifndef", "if", "elif", "else", "endif", - "error", "pragma", "line" - ], - "colors": { - "comment": "#6a9955", - "string": "#ce9178", - "preprocessor": "#c586c0", - "number": "#b5cea8", - "keyword": "#569cd6", - "type": "#4ec9b0", - "function": "#dcdcaa", - "constant": "#4fc1ff", - "identifier": "#9cdcfe", - "operator": "#d4d4d4", - "plain": "#d4d4d4" - } - }, - "cpp": { - "name": "C++", - "fileTypes": ["C++", "C/C++ Header"], - "keywords": [ - "auto", "break", "case", "char", "const", "continue", "default", "do", - "double", "else", "enum", "extern", "float", "for", "goto", "if", - "int", "long", "register", "return", "short", "signed", "sizeof", "static", - "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", - "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", - "atomic_noexcept", "bitand", "bitor", "bool", "catch", "char8_t", "char16_t", - "char32_t", "class", "compl", "concept", "const_cast", "consteval", "constexpr", - "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", "dynamic_cast", - "explicit", "export", "false", "friend", "inline", "mutable", "namespace", "new", - "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", - "protected", "public", "reflexpr", "reinterpret_cast", "requires", "static_assert", - "static_cast", "synchronized", "template", "this", "thread_local", "throw", "true", - "try", "typeid", "typename", "using", "virtual", "wchar_t", "xor", "xor_eq" - ], - "types": [ - "int8_t", "int16_t", "int32_t", "int64_t", - "uint8_t", "uint16_t", "uint32_t", "uint64_t", - "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", - "string", "vector", "map", "set", "list", "deque", "queue", - "stack", "array", "unordered_map", "unordered_set", - "shared_ptr", "unique_ptr", "weak_ptr", "optional", "variant", - "FILE", "nullptr_t", "std", "iostream", "fstream" - ], - "preprocessor": [ - "define", "undef", "include", "ifdef", "ifndef", "if", "elif", "else", "endif", - "error", "pragma", "line" - ], - "colors": { - "comment": "#6a9955", - "string": "#ce9178", - "preprocessor": "#c586c0", - "number": "#b5cea8", - "keyword": "#569cd6", - "type": "#4ec9b0", - "function": "#dcdcaa", - "constant": "#4fc1ff", - "identifier": "#9cdcfe", - "operator": "#d4d4d4", - "plain": "#d4d4d4" - } - } -} +{ + "c": { + "name": "C", + "fileTypes": ["C", "C/C++ Header"], + "keywords": [ + "auto", "break", "case", "char", "const", "continue", "default", "do", + "double", "else", "enum", "extern", "float", "for", "goto", "if", + "int", "long", "register", "return", "short", "signed", "sizeof", "static", + "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while" + ], + "types": [ + "int8_t", "int16_t", "int32_t", "int64_t", + "uint8_t", "uint16_t", "uint32_t", "uint64_t", + "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", + "FILE", "nullptr_t" + ], + "preprocessor": [ + "define", "undef", "include", "ifdef", "ifndef", "if", "elif", "else", "endif", + "error", "pragma", "line" + ], + "colors": { + "comment": "#6a9955", + "string": "#ce9178", + "preprocessor": "#c586c0", + "number": "#b5cea8", + "keyword": "#569cd6", + "type": "#4ec9b0", + "function": "#dcdcaa", + "constant": "#4fc1ff", + "identifier": "#9cdcfe", + "operator": "#d4d4d4", + "plain": "#d4d4d4" + } + }, + "cpp": { + "name": "C++", + "fileTypes": ["C++", "C/C++ Header"], + "keywords": [ + "auto", "break", "case", "char", "const", "continue", "default", "do", + "double", "else", "enum", "extern", "float", "for", "goto", "if", + "int", "long", "register", "return", "short", "signed", "sizeof", "static", + "struct", "switch", "typedef", "union", "unsigned", "void", "volatile", "while", + "alignas", "alignof", "and", "and_eq", "asm", "atomic_cancel", "atomic_commit", + "atomic_noexcept", "bitand", "bitor", "bool", "catch", "char8_t", "char16_t", + "char32_t", "class", "compl", "concept", "const_cast", "consteval", "constexpr", + "constinit", "co_await", "co_return", "co_yield", "decltype", "delete", "dynamic_cast", + "explicit", "export", "false", "friend", "inline", "mutable", "namespace", "new", + "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq", "private", + "protected", "public", "reflexpr", "reinterpret_cast", "requires", "static_assert", + "static_cast", "synchronized", "template", "this", "thread_local", "throw", "true", + "try", "typeid", "typename", "using", "virtual", "wchar_t", "xor", "xor_eq" + ], + "types": [ + "int8_t", "int16_t", "int32_t", "int64_t", + "uint8_t", "uint16_t", "uint32_t", "uint64_t", + "size_t", "ssize_t", "ptrdiff_t", "intptr_t", "uintptr_t", + "string", "vector", "map", "set", "list", "deque", "queue", + "stack", "array", "unordered_map", "unordered_set", + "shared_ptr", "unique_ptr", "weak_ptr", "optional", "variant", + "FILE", "nullptr_t", "std", "iostream", "fstream" + ], + "preprocessor": [ + "define", "undef", "include", "ifdef", "ifndef", "if", "elif", "else", "endif", + "error", "pragma", "line" + ], + "colors": { + "comment": "#6a9955", + "string": "#ce9178", + "preprocessor": "#c586c0", + "number": "#b5cea8", + "keyword": "#569cd6", + "type": "#4ec9b0", + "function": "#dcdcaa", + "constant": "#4fc1ff", + "identifier": "#9cdcfe", + "operator": "#d4d4d4", + "plain": "#d4d4d4" + } + } +} diff --git a/src/renderer/utils/syntaxHighlighter.js b/src/renderer/utils/syntaxHighlighter.js index 14b9749..cc54806 100644 --- a/src/renderer/utils/syntaxHighlighter.js +++ b/src/renderer/utils/syntaxHighlighter.js @@ -1,9 +1,9 @@ -;(function() { -/** - * Scalable Syntax Highlighter for C/C++ code - * Provides real-time syntax highlighting with optimized performance for large files - */ - +;(function() { +/** + * Scalable Syntax Highlighter for C/C++ code + * Provides real-time syntax highlighting with optimized performance for large files + */ + // Load syntax configuration lazily so preload does not block renderer startup. let syntaxConfig = {}; const syntaxConfigReady = (typeof window !== 'undefined' && window.api && typeof window.api.getSyntaxConfig === 'function') @@ -15,295 +15,295 @@ const syntaxConfigReady = (typeof window !== 'undefined' && window.api && typeof }) .catch(() => syntaxConfig) : Promise.resolve(syntaxConfig); - -// Cache for language configurations -let languageCache = {}; - -/** - * Get language configuration - * @param {string} fileType - The file type - * @returns {Object} - Language configuration - */ -function getLanguageConfig(fileType) { - // Check cache first - if (languageCache[fileType]) { - return languageCache[fileType]; - } - - // Find matching language config - for (const [langKey, config] of Object.entries(syntaxConfig)) { - if (config.fileTypes.includes(fileType)) { - // Convert arrays to Sets for faster lookup - const processedConfig = { - ...config, - keywords: new Set(config.keywords), - types: new Set(config.types), - preprocessor: new Set(config.preprocessor) - }; - languageCache[fileType] = processedConfig; - return processedConfig; - } - } - - return null; -} - -/** - * Escape HTML special characters - * @param {string} text - Text to escape - * @returns {string} Escaped HTML string - */ -function escapeHtml(text) { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -/** - * Tokenize C/C++ code for highlighting - * @param {string} code - The code to tokenize - * @param {Object} langConfig - Language configuration - * @returns {Array} - Array of tokens with type and value - */ -function tokenizeCpp(code, langConfig) { - const tokens = []; - let i = 0; - - while (i < code.length) { - const char = code[i]; - - // Single-line comment - if (code.substr(i, 2) === '//') { - let comment = '//'; - i += 2; - while (i < code.length && code[i] !== '\n') { - comment += code[i]; - i++; - } - tokens.push({ type: 'comment', value: comment }); - continue; - } - - // Multi-line comment - if (code.substr(i, 2) === '/*') { - let comment = '/*'; - i += 2; - while (i < code.length && code.substr(i, 2) !== '*/') { - comment += code[i]; - i++; - } - if (code.substr(i, 2) === '*/') { - comment += '*/'; - i += 2; - } - tokens.push({ type: 'comment', value: comment }); - continue; - } - - // String literals - if (char === '"' || char === "'") { - const quote = char; - let str = quote; - i++; - while (i < code.length) { - if (code[i] === '\\' && i + 1 < code.length) { - str += code[i] + code[i + 1]; - i += 2; - } else if (code[i] === quote) { - str += quote; - i++; - break; - } else { - str += code[i]; - i++; - } - } - tokens.push({ type: 'string', value: str }); - continue; - } - - // Preprocessor directives - if (char === '#') { - let directive = '#'; - i++; - while (i < code.length && /[a-zA-Z_]/.test(code[i])) { - directive += code[i]; - i++; - } - tokens.push({ type: 'preprocessor', value: directive }); - - // For #include, capture the file path - if (directive.includes('include')) { - while (i < code.length && /\s/.test(code[i])) { - tokens.push({ type: 'whitespace', value: code[i] }); - i++; - } - if (i < code.length && (code[i] === '<' || code[i] === '"')) { - const endChar = code[i] === '<' ? '>' : '"'; - let path = code[i]; - i++; - while (i < code.length && code[i] !== endChar && code[i] !== '\n') { - path += code[i]; - i++; - } - if (i < code.length && code[i] === endChar) { - path += code[i]; - i++; - } - tokens.push({ type: 'string', value: path }); - } - } - continue; - } - - // Numbers - if (/\d/.test(char) || (char === '.' && i + 1 < code.length && /\d/.test(code[i + 1]))) { - let num = char; - i++; - while (i < code.length && /[\d.xXa-fA-F]/.test(code[i])) { - num += code[i]; - i++; - } - // Check for number suffixes - if (i < code.length && /[fFlLuU]/.test(code[i])) { - num += code[i]; - i++; - } - tokens.push({ type: 'number', value: num }); - continue; - } - - // Identifiers, keywords, types - if (/[a-zA-Z_]/.test(char)) { - let word = ''; - while (i < code.length && /[a-zA-Z0-9_]/.test(code[i])) { - word += code[i]; - i++; - } - - // Skip whitespace to check for function call - let j = i; - while (j < code.length && /\s/.test(code[j])) j++; - - const isFunction = code[j] === '('; - - if (langConfig.keywords.has(word)) { - tokens.push({ type: 'keyword', value: word }); - } else if (langConfig.types.has(word)) { - tokens.push({ type: 'type', value: word }); - } else if (isFunction) { - tokens.push({ type: 'function', value: word }); - } else if (/^[A-Z_][A-Z0-9_]*$/.test(word)) { - tokens.push({ type: 'constant', value: word }); - } else { - tokens.push({ type: 'identifier', value: word }); - } - continue; - } - - // Operators - if ('+-*/%=<>!&|^~?:'.includes(char)) { - tokens.push({ type: 'operator', value: char }); - i++; - continue; - } - - // Everything else - tokens.push({ type: 'plain', value: char }); - i++; - } - - return tokens; -} - -/** - * Convert tokens to HTML - * @param {Array} tokens - Array of tokens - * @param {Object} langConfig - Language configuration - * @returns {string} - HTML string - */ -function tokensToHtml(tokens, langConfig) { - const colorMap = langConfig.colors; - - return tokens.map(token => { - const color = colorMap[token.type] || colorMap.plain; - if (token.type === 'whitespace' || token.type === 'plain') { - return escapeHtml(token.value); - } - return `${escapeHtml(token.value)}`; - }).join(''); -} - -/** - * Highlight C/C++ syntax (optimized version) - * @param {string} code - The code to highlight - * @param {string} fileType - The file type - * @returns {string} - HTML with syntax highlighting - */ -function highlightCppSyntax(code, fileType) { - if (!code) return ''; - - const langConfig = getLanguageConfig(fileType); - if (!langConfig) return escapeHtml(code); - - const tokens = tokenizeCpp(code, langConfig); - return tokensToHtml(tokens, langConfig); -} - -/** - * Apply syntax highlighting to editor based on file type - * @param {string} code - The code to highlight - * @param {string} fileType - The detected file type - * @returns {string} - HTML with syntax highlighting - */ -function applySyntaxHighlight(code, fileType) { - if (!code) return ''; - - const langConfig = getLanguageConfig(fileType); - - // Apply highlighting if language config found - if (langConfig) { - return highlightCppSyntax(code, fileType); - } - - // For other file types, return escaped HTML - return escapeHtml(code); -} - -/** - * Check if file type should have syntax highlighting - * @param {string} fileType - The file type to check - * @returns {boolean} - True if highlighting should be applied - */ -function shouldHighlight(fileType) { - return getLanguageConfig(fileType) !== null; -} - -// Export for use in other modules -if (typeof module !== 'undefined' && module.exports) { - module.exports = { - highlightCppSyntax, - applySyntaxHighlight, - shouldHighlight, - escapeHtml, - tokenizeCpp, - tokensToHtml - }; -} - -// Make available globally + +// Cache for language configurations +let languageCache = {}; + +/** + * Get language configuration + * @param {string} fileType - The file type + * @returns {Object} - Language configuration + */ +function getLanguageConfig(fileType) { + // Check cache first + if (languageCache[fileType]) { + return languageCache[fileType]; + } + + // Find matching language config + for (const [langKey, config] of Object.entries(syntaxConfig)) { + if (config.fileTypes.includes(fileType)) { + // Convert arrays to Sets for faster lookup + const processedConfig = { + ...config, + keywords: new Set(config.keywords), + types: new Set(config.types), + preprocessor: new Set(config.preprocessor) + }; + languageCache[fileType] = processedConfig; + return processedConfig; + } + } + + return null; +} + +/** + * Escape HTML special characters + * @param {string} text - Text to escape + * @returns {string} Escaped HTML string + */ +function escapeHtml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Tokenize C/C++ code for highlighting + * @param {string} code - The code to tokenize + * @param {Object} langConfig - Language configuration + * @returns {Array} - Array of tokens with type and value + */ +function tokenizeCpp(code, langConfig) { + const tokens = []; + let i = 0; + + while (i < code.length) { + const char = code[i]; + + // Single-line comment + if (code.substr(i, 2) === '//') { + let comment = '//'; + i += 2; + while (i < code.length && code[i] !== '\n') { + comment += code[i]; + i++; + } + tokens.push({ type: 'comment', value: comment }); + continue; + } + + // Multi-line comment + if (code.substr(i, 2) === '/*') { + let comment = '/*'; + i += 2; + while (i < code.length && code.substr(i, 2) !== '*/') { + comment += code[i]; + i++; + } + if (code.substr(i, 2) === '*/') { + comment += '*/'; + i += 2; + } + tokens.push({ type: 'comment', value: comment }); + continue; + } + + // String literals + if (char === '"' || char === "'") { + const quote = char; + let str = quote; + i++; + while (i < code.length) { + if (code[i] === '\\' && i + 1 < code.length) { + str += code[i] + code[i + 1]; + i += 2; + } else if (code[i] === quote) { + str += quote; + i++; + break; + } else { + str += code[i]; + i++; + } + } + tokens.push({ type: 'string', value: str }); + continue; + } + + // Preprocessor directives + if (char === '#') { + let directive = '#'; + i++; + while (i < code.length && /[a-zA-Z_]/.test(code[i])) { + directive += code[i]; + i++; + } + tokens.push({ type: 'preprocessor', value: directive }); + + // For #include, capture the file path + if (directive.includes('include')) { + while (i < code.length && /\s/.test(code[i])) { + tokens.push({ type: 'whitespace', value: code[i] }); + i++; + } + if (i < code.length && (code[i] === '<' || code[i] === '"')) { + const endChar = code[i] === '<' ? '>' : '"'; + let path = code[i]; + i++; + while (i < code.length && code[i] !== endChar && code[i] !== '\n') { + path += code[i]; + i++; + } + if (i < code.length && code[i] === endChar) { + path += code[i]; + i++; + } + tokens.push({ type: 'string', value: path }); + } + } + continue; + } + + // Numbers + if (/\d/.test(char) || (char === '.' && i + 1 < code.length && /\d/.test(code[i + 1]))) { + let num = char; + i++; + while (i < code.length && /[\d.xXa-fA-F]/.test(code[i])) { + num += code[i]; + i++; + } + // Check for number suffixes + if (i < code.length && /[fFlLuU]/.test(code[i])) { + num += code[i]; + i++; + } + tokens.push({ type: 'number', value: num }); + continue; + } + + // Identifiers, keywords, types + if (/[a-zA-Z_]/.test(char)) { + let word = ''; + while (i < code.length && /[a-zA-Z0-9_]/.test(code[i])) { + word += code[i]; + i++; + } + + // Skip whitespace to check for function call + let j = i; + while (j < code.length && /\s/.test(code[j])) j++; + + const isFunction = code[j] === '('; + + if (langConfig.keywords.has(word)) { + tokens.push({ type: 'keyword', value: word }); + } else if (langConfig.types.has(word)) { + tokens.push({ type: 'type', value: word }); + } else if (isFunction) { + tokens.push({ type: 'function', value: word }); + } else if (/^[A-Z_][A-Z0-9_]*$/.test(word)) { + tokens.push({ type: 'constant', value: word }); + } else { + tokens.push({ type: 'identifier', value: word }); + } + continue; + } + + // Operators + if ('+-*/%=<>!&|^~?:'.includes(char)) { + tokens.push({ type: 'operator', value: char }); + i++; + continue; + } + + // Everything else + tokens.push({ type: 'plain', value: char }); + i++; + } + + return tokens; +} + +/** + * Convert tokens to HTML + * @param {Array} tokens - Array of tokens + * @param {Object} langConfig - Language configuration + * @returns {string} - HTML string + */ +function tokensToHtml(tokens, langConfig) { + const colorMap = langConfig.colors; + + return tokens.map(token => { + const color = colorMap[token.type] || colorMap.plain; + if (token.type === 'whitespace' || token.type === 'plain') { + return escapeHtml(token.value); + } + return `${escapeHtml(token.value)}`; + }).join(''); +} + +/** + * Highlight C/C++ syntax (optimized version) + * @param {string} code - The code to highlight + * @param {string} fileType - The file type + * @returns {string} - HTML with syntax highlighting + */ +function highlightCppSyntax(code, fileType) { + if (!code) return ''; + + const langConfig = getLanguageConfig(fileType); + if (!langConfig) return escapeHtml(code); + + const tokens = tokenizeCpp(code, langConfig); + return tokensToHtml(tokens, langConfig); +} + +/** + * Apply syntax highlighting to editor based on file type + * @param {string} code - The code to highlight + * @param {string} fileType - The detected file type + * @returns {string} - HTML with syntax highlighting + */ +function applySyntaxHighlight(code, fileType) { + if (!code) return ''; + + const langConfig = getLanguageConfig(fileType); + + // Apply highlighting if language config found + if (langConfig) { + return highlightCppSyntax(code, fileType); + } + + // For other file types, return escaped HTML + return escapeHtml(code); +} + +/** + * Check if file type should have syntax highlighting + * @param {string} fileType - The file type to check + * @returns {boolean} - True if highlighting should be applied + */ +function shouldHighlight(fileType) { + return getLanguageConfig(fileType) !== null; +} + +// Export for use in other modules +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + highlightCppSyntax, + applySyntaxHighlight, + shouldHighlight, + escapeHtml, + tokenizeCpp, + tokensToHtml + }; +} + +// Make available globally if (typeof window !== 'undefined') { window.syntaxHighlighter = { ready: syntaxConfigReady, highlightCppSyntax, applySyntaxHighlight, shouldHighlight, - escapeHtml, - tokenizeCpp, - tokensToHtml - }; -} -})(); + escapeHtml, + tokenizeCpp, + tokensToHtml + }; +} +})(); diff --git a/src/styles/activity-bar.css b/src/styles/activity-bar.css index 204a2b0..b63d147 100644 --- a/src/styles/activity-bar.css +++ b/src/styles/activity-bar.css @@ -1,47 +1,47 @@ -/* Activity bar styles */ -.activity-bar { - width: 56px; - display: flex; - flex-direction: column; - align-items: center; - padding: 14px 0; - background: linear-gradient(180deg, rgba(11, 14, 20, 0.82) 0%, rgba(10, 13, 18, 0.68) 100%); - border-right: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.03); -} - -.activity-bar .activity-item { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - margin: 5px 0; - border-radius: 12px; - cursor: pointer; - transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease; - color: rgba(214, 222, 237, 0.46); - position: relative; -} - -.activity-bar .activity-item:hover { - background: rgba(255, 255, 255, 0.06); - color: rgba(255, 255, 255, 0.88); - transform: translateY(-1px); -} - -.activity-bar .activity-item.active { - background: rgba(255, 255, 255, 0.1); - color: #f5f8ff; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); -} - -.activity-bar .activity-item.active::before { - content: ""; - position: absolute; - left: -10px; - width: 2px; - height: 16px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.84); -} +/* Activity bar styles */ +.activity-bar { + width: 56px; + display: flex; + flex-direction: column; + align-items: center; + padding: 14px 0; + background: linear-gradient(180deg, rgba(11, 14, 20, 0.82) 0%, rgba(10, 13, 18, 0.68) 100%); + border-right: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.03); +} + +.activity-bar .activity-item { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + margin: 5px 0; + border-radius: 12px; + cursor: pointer; + transition: background-color 0.18s ease, color 0.18s ease, transform 0.18s ease; + color: rgba(214, 222, 237, 0.46); + position: relative; +} + +.activity-bar .activity-item:hover { + background: rgba(255, 255, 255, 0.06); + color: rgba(255, 255, 255, 0.88); + transform: translateY(-1px); +} + +.activity-bar .activity-item.active { + background: rgba(255, 255, 255, 0.1); + color: #f5f8ff; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.activity-bar .activity-item.active::before { + content: ""; + position: absolute; + left: -10px; + width: 2px; + height: 16px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.84); +} diff --git a/src/styles/base.css b/src/styles/base.css index 407f8a3..d054245 100644 --- a/src/styles/base.css +++ b/src/styles/base.css @@ -1,337 +1,337 @@ -/* Base styles and layout */ -:root { - --window-padding: 12px; - --panel-gap: 12px; - --panel-radius: 12px; - --panel-blur: 10px; - --surface-bg: rgba(20, 24, 31, 0.68); - --surface-bg-strong: rgba(13, 17, 24, 0.86); - --surface-bg-soft: rgba(255, 255, 255, 0.05); - --surface-border: rgba(255, 255, 255, 0.1); - --surface-border-strong: rgba(255, 255, 255, 0.14); - --surface-shadow: 0 12px 30px rgba(0, 0, 0, 0.28); - --text-primary: #eef4ff; - --text-secondary: #bac5d6; - --text-muted: #7f8ba0; - --editor-bg: #0d1219; - --editor-bg-elevated: #111826; - --accent: #d6deed; - --accent-soft: rgba(214, 222, 237, 0.16); - --danger: #ff6b6b; - --warning: #ffcf66; -} - -* { - box-sizing: border-box; -} - -html, -body { - height: 100%; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Noto Color Emoji", "Segoe UI Emoji", "Apple Color Emoji", "Twemoji Mozilla"; - margin: 0; - padding: 0; - color: var(--text-primary); - min-height: 100vh; - overflow: hidden; - font-size: 13px; - font-feature-settings: "liga" 1, "calt" 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background: - radial-gradient(circle at top left, rgba(120, 166, 255, 0.15), transparent 28%), - radial-gradient(circle at 82% 8%, rgba(154, 231, 214, 0.12), transparent 24%), - linear-gradient(135deg, #080b12 0%, #0d1018 38%, #121925 100%); - position: relative; -} - -body::before, -body::after { - content: ""; - position: fixed; - inset: auto; - border-radius: 999px; - filter: blur(44px); - pointer-events: none; - opacity: 0.75; - z-index: 0; - will-change: transform, opacity; -} - -body::before { - width: 280px; - height: 280px; - top: -60px; - right: 8%; - background: rgba(126, 173, 255, 0.18); - animation: glow-orbit-a 42s ease-in-out infinite; -} - -body::after { - width: 240px; - height: 240px; - bottom: -80px; - left: 6%; - background: rgba(120, 244, 211, 0.12); - animation: glow-orbit-b 52s ease-in-out infinite; -} - -/* Top-right glow walks clockwise around the window perimeter (transform-only, - viewport-relative) with a gentle opacity breathe. */ -@keyframes glow-orbit-a { - 0% { transform: translate(0, 0); opacity: 0.75; } - 25% { transform: translate(0, 82vh); opacity: 0.55; } - 50% { transform: translate(-88vw, 82vh); opacity: 0.75; } - 75% { transform: translate(-88vw, 0); opacity: 0.55; } - 100% { transform: translate(0, 0); opacity: 0.75; } -} - -/* Bottom-left glow also travels clockwise (up the left edge first), starting - from the opposite corner so the two lights chase each other. */ -@keyframes glow-orbit-b { - 0% { transform: translate(0, 0); opacity: 0.7; } - 25% { transform: translate(0, -82vh); opacity: 0.5; } - 50% { transform: translate(88vw, -82vh); opacity: 0.7; } - 75% { transform: translate(88vw, 0); opacity: 0.5; } - 100% { transform: translate(0, 0); opacity: 0.7; } -} - -@media (prefers-reduced-motion: reduce) { - body::before, - body::after { - animation: none; - } -} - -.window-shell { - position: relative; - z-index: 1; - height: 100vh; - padding: var(--window-padding); - display: grid; - grid-template-rows: auto minmax(0, 1fr); - gap: var(--panel-gap); -} - -.app-surface { - background: var(--surface-bg); - border: 1px solid var(--surface-border); - border-radius: var(--panel-radius); - box-shadow: var(--surface-shadow); - backdrop-filter: blur(var(--panel-blur)) saturate(140%); - -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(140%); - overflow: hidden; -} - -body.lite-effects { - --panel-blur: 0px; - --surface-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); - --surface-bg: rgba(18, 22, 30, 0.94); -} - -body.lite-effects::before, -body.lite-effects::after { - display: none; -} - -body.lite-effects .app-surface, -body.lite-effects .title-bar-pill, -body.lite-effects .dropdown-menu, -body.lite-effects .search-widget, -body.lite-effects .context-menu { - backdrop-filter: none !important; - -webkit-backdrop-filter: none !important; -} - -.ide-container { - display: flex; - gap: var(--panel-gap); - min-height: 0; - min-width: 0; -} - -.main-content-area { - display: flex; - flex: 0 0 auto; - min-height: 0; - min-width: 0; -} - -.main { - flex: 1 1 auto; - min-width: 0; - min-height: 0; - display: flex; - flex-direction: column; - background: linear-gradient(180deg, rgba(19, 23, 31, 0.78) 0%, rgba(16, 19, 27, 0.84) 100%); -} - -button, -input, -textarea, -select { - color: inherit; -} - -button, -input, -textarea, -select { - font: inherit; -} - -::selection { - background: rgba(153, 190, 255, 0.28); -} - -::-webkit-scrollbar { - width: 7px; - height: 7px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(198, 209, 224, 0.18); - border-radius: 999px; - border: 1px solid rgba(255, 255, 255, 0.04); -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(198, 209, 224, 0.3); -} - -.performance-hud { - position: fixed; - right: 18px; - bottom: 18px; - width: 232px; - padding: 12px; - border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(10, 13, 19, 0.92); - color: var(--text-primary); - box-shadow: 0 18px 36px rgba(0, 0, 0, 0.32); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - z-index: 5000; - display: none; -} - -.performance-hud.visible { - display: block; -} - -.performance-hud-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 10px; -} - -.performance-hud-title { - font-size: 11px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--text-secondary); -} - -.performance-hud-actions { - display: flex; - gap: 6px; -} - -.performance-hud-btn { - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.04); - color: var(--text-secondary); - border-radius: 10px; - padding: 5px 8px; - cursor: pointer; - font-size: 11px; -} - -.performance-hud-btn:hover { - background: rgba(255, 255, 255, 0.08); - color: var(--text-primary); -} - -.performance-hud-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 8px; -} - -.performance-hud-card { - min-width: 0; - padding: 8px 9px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.05); -} - -.performance-hud-label { - display: block; - font-size: 10px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.08em; - margin-bottom: 3px; -} - -.performance-hud-value { - display: block; - font-size: 12px; - font-weight: 600; - color: var(--text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.performance-hud-note { - margin-top: 10px; - font-size: 10px; - color: var(--text-muted); - line-height: 1.4; -} - -@media (max-width: 1100px) { - .window-shell { - padding: 10px; - } - - .main-content-area .sidebar { - width: 240px; - } - - .tools-panel { - width: 320px; - } -} - -@media (max-width: 900px) { - .window-shell { - padding: 8px; - gap: 8px; - } - - .ide-container { - gap: 8px; - } - - .title-bar-app-tag { - display: none; - } - - .main-content-area .sidebar { - width: 220px; - } -} +/* Base styles and layout */ +:root { + --window-padding: 12px; + --panel-gap: 12px; + --panel-radius: 12px; + --panel-blur: 10px; + --surface-bg: rgba(20, 24, 31, 0.68); + --surface-bg-strong: rgba(13, 17, 24, 0.86); + --surface-bg-soft: rgba(255, 255, 255, 0.05); + --surface-border: rgba(255, 255, 255, 0.1); + --surface-border-strong: rgba(255, 255, 255, 0.14); + --surface-shadow: 0 12px 30px rgba(0, 0, 0, 0.28); + --text-primary: #eef4ff; + --text-secondary: #bac5d6; + --text-muted: #7f8ba0; + --editor-bg: #0d1219; + --editor-bg-elevated: #111826; + --accent: #d6deed; + --accent-soft: rgba(214, 222, 237, 0.16); + --danger: #ff6b6b; + --warning: #ffcf66; +} + +* { + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Noto Color Emoji", "Segoe UI Emoji", "Apple Color Emoji", "Twemoji Mozilla"; + margin: 0; + padding: 0; + color: var(--text-primary); + min-height: 100vh; + overflow: hidden; + font-size: 13px; + font-feature-settings: "liga" 1, "calt" 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background: + radial-gradient(circle at top left, rgba(120, 166, 255, 0.15), transparent 28%), + radial-gradient(circle at 82% 8%, rgba(154, 231, 214, 0.12), transparent 24%), + linear-gradient(135deg, #080b12 0%, #0d1018 38%, #121925 100%); + position: relative; +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: auto; + border-radius: 999px; + filter: blur(44px); + pointer-events: none; + opacity: 0.75; + z-index: 0; + will-change: transform, opacity; +} + +body::before { + width: 280px; + height: 280px; + top: -60px; + right: 8%; + background: rgba(126, 173, 255, 0.18); + animation: glow-orbit-a 42s ease-in-out infinite; +} + +body::after { + width: 240px; + height: 240px; + bottom: -80px; + left: 6%; + background: rgba(120, 244, 211, 0.12); + animation: glow-orbit-b 52s ease-in-out infinite; +} + +/* Top-right glow walks clockwise around the window perimeter (transform-only, + viewport-relative) with a gentle opacity breathe. */ +@keyframes glow-orbit-a { + 0% { transform: translate(0, 0); opacity: 0.75; } + 25% { transform: translate(0, 82vh); opacity: 0.55; } + 50% { transform: translate(-88vw, 82vh); opacity: 0.75; } + 75% { transform: translate(-88vw, 0); opacity: 0.55; } + 100% { transform: translate(0, 0); opacity: 0.75; } +} + +/* Bottom-left glow also travels clockwise (up the left edge first), starting + from the opposite corner so the two lights chase each other. */ +@keyframes glow-orbit-b { + 0% { transform: translate(0, 0); opacity: 0.7; } + 25% { transform: translate(0, -82vh); opacity: 0.5; } + 50% { transform: translate(88vw, -82vh); opacity: 0.7; } + 75% { transform: translate(88vw, 0); opacity: 0.5; } + 100% { transform: translate(0, 0); opacity: 0.7; } +} + +@media (prefers-reduced-motion: reduce) { + body::before, + body::after { + animation: none; + } +} + +.window-shell { + position: relative; + z-index: 1; + height: 100vh; + padding: var(--window-padding); + display: grid; + grid-template-rows: auto minmax(0, 1fr); + gap: var(--panel-gap); +} + +.app-surface { + background: var(--surface-bg); + border: 1px solid var(--surface-border); + border-radius: var(--panel-radius); + box-shadow: var(--surface-shadow); + backdrop-filter: blur(var(--panel-blur)) saturate(140%); + -webkit-backdrop-filter: blur(var(--panel-blur)) saturate(140%); + overflow: hidden; +} + +body.lite-effects { + --panel-blur: 0px; + --surface-shadow: 0 8px 18px rgba(0, 0, 0, 0.18); + --surface-bg: rgba(18, 22, 30, 0.94); +} + +body.lite-effects::before, +body.lite-effects::after { + display: none; +} + +body.lite-effects .app-surface, +body.lite-effects .title-bar-pill, +body.lite-effects .dropdown-menu, +body.lite-effects .search-widget, +body.lite-effects .context-menu { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} + +.ide-container { + display: flex; + gap: var(--panel-gap); + min-height: 0; + min-width: 0; +} + +.main-content-area { + display: flex; + flex: 0 0 auto; + min-height: 0; + min-width: 0; +} + +.main { + flex: 1 1 auto; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + background: linear-gradient(180deg, rgba(19, 23, 31, 0.78) 0%, rgba(16, 19, 27, 0.84) 100%); +} + +button, +input, +textarea, +select { + color: inherit; +} + +button, +input, +textarea, +select { + font: inherit; +} + +::selection { + background: rgba(153, 190, 255, 0.28); +} + +::-webkit-scrollbar { + width: 7px; + height: 7px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(198, 209, 224, 0.18); + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.04); +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(198, 209, 224, 0.3); +} + +.performance-hud { + position: fixed; + right: 18px; + bottom: 18px; + width: 232px; + padding: 12px; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(10, 13, 19, 0.92); + color: var(--text-primary); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.32); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + z-index: 5000; + display: none; +} + +.performance-hud.visible { + display: block; +} + +.performance-hud-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 10px; +} + +.performance-hud-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.performance-hud-actions { + display: flex; + gap: 6px; +} + +.performance-hud-btn { + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.04); + color: var(--text-secondary); + border-radius: 10px; + padding: 5px 8px; + cursor: pointer; + font-size: 11px; +} + +.performance-hud-btn:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--text-primary); +} + +.performance-hud-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.performance-hud-card { + min-width: 0; + padding: 8px 9px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.performance-hud-label { + display: block; + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; + margin-bottom: 3px; +} + +.performance-hud-value { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.performance-hud-note { + margin-top: 10px; + font-size: 10px; + color: var(--text-muted); + line-height: 1.4; +} + +@media (max-width: 1100px) { + .window-shell { + padding: 10px; + } + + .main-content-area .sidebar { + width: 240px; + } + + .tools-panel { + width: 320px; + } +} + +@media (max-width: 900px) { + .window-shell { + padding: 8px; + gap: 8px; + } + + .ide-container { + gap: 8px; + } + + .title-bar-app-tag { + display: none; + } + + .main-content-area .sidebar { + width: 220px; + } +} diff --git a/src/styles/diagnostics.css b/src/styles/diagnostics.css index c6b347b..a5175a0 100644 --- a/src/styles/diagnostics.css +++ b/src/styles/diagnostics.css @@ -1,612 +1,612 @@ -/* Diagnostics panel styles */ -.tools-section { - padding: 18px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - background: rgba(255, 255, 255, 0.02); - overflow: hidden; - min-width: 0; -} - -.section-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 16px; -} - -.section-icon { - width: 12px; - height: 12px; - border-radius: 4px; - background: linear-gradient(180deg, rgba(238, 244, 255, 0.88) 0%, rgba(186, 197, 214, 0.65) 100%); - box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.03); - flex-shrink: 0; -} - -.section-title { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); -} - -.config-group { - margin-bottom: 16px; -} - -.config-label { - display: flex; - flex-direction: column; - gap: 4px; - font-size: 12px; - font-weight: 500; - color: var(--text-secondary); - margin-bottom: 8px; -} - -.config-hint { - font-size: 10px; - font-weight: 400; - color: var(--text-muted); - font-style: italic; -} - -.config-input { - width: 100%; - background: rgba(9, 13, 19, 0.88); - border: 1px solid rgba(255, 255, 255, 0.08); - color: var(--text-primary); - padding: 11px 12px; - border-radius: 14px; - font-size: 12px; - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; - transition: border-color 0.2s ease, box-shadow 0.2s ease; - outline: none; -} - -.config-input:hover { - border-color: rgba(255, 255, 255, 0.14); -} - -.config-input:focus { - border-color: rgba(214, 222, 237, 0.28); - box-shadow: 0 0 0 3px rgba(214, 222, 237, 0.1); -} - -.config-input::placeholder { - color: rgba(127, 139, 160, 0.8); -} - -.ctrace-run-btn { - width: 100%; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.11) 0%, rgba(255, 255, 255, 0.07) 100%); - border: 1px solid rgba(255, 255, 255, 0.12); - color: #f5f8ff; - padding: 13px 16px; - border-radius: 16px; - cursor: pointer; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.01em; - transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); -} - -.ctrace-run-btn:hover { - transform: translateY(-1px); - box-shadow: 0 14px 28px rgba(0, 0, 0, 0.24); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.09) 100%); -} - -.ctrace-run-btn:active { - transform: translateY(0); -} - -.ctrace-run-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none; -} - -.ctrace-btn-row { - display: flex; - gap: 8px; - align-items: stretch; -} - -.ctrace-btn-row .ctrace-run-btn { - flex: 1; - width: auto; -} - -.ctrace-clear-btn { - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.10); - color: var(--text-muted); - padding: 13px 14px; - border-radius: 16px; - cursor: pointer; - font-size: 12px; - font-weight: 600; - letter-spacing: 0.01em; - transition: transform 0.18s ease, background-color 0.18s ease, color 0.18s ease; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - white-space: nowrap; -} - -.ctrace-clear-btn:hover { - background: rgba(255, 107, 107, 0.10); - border-color: rgba(255, 107, 107, 0.22); - color: #ffb0b0; - transform: translateY(-1px); -} - -.ctrace-clear-btn:active { - transform: translateY(0); -} - -#ctrace-results-area { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; - padding: 16px; -} - -.ctrace-placeholder, -.ctrace-loading, -.ctrace-error { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 56px 20px; - text-align: center; - border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 18px; - background: rgba(255, 255, 255, 0.02); -} - -.placeholder-mark { - width: 34px; - height: 34px; - border-radius: 10px; - margin-bottom: 16px; - background: linear-gradient(180deg, rgba(238, 244, 255, 0.84) 0%, rgba(186, 197, 214, 0.55) 100%); - opacity: 0.82; -} - -.placeholder-text, -.loading-text, -.error-text { - font-size: 16px; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 8px; -} - -.placeholder-subtext, -.loading-subtext, -.error-subtext, -.empty-text { - font-size: 13px; - color: var(--text-muted); -} - -.loading-spinner { - width: 40px; - height: 40px; - border: 3px solid rgba(255, 255, 255, 0.1); - border-top: 3px solid rgba(214, 222, 237, 0.84); - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 18px; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -.error-text { - color: #ffb0b0; -} - -.error-details, -.error-help, -.raw-output-content { - width: 100%; - background: rgba(10, 13, 18, 0.78); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 14px; - padding: 14px; - margin-top: 16px; - max-width: 100%; - overflow-x: hidden; - white-space: pre-wrap; - overflow-wrap: anywhere; - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; - font-size: 11px; - color: var(--text-primary); - text-align: left; -} - -.error-help { - line-height: 1.65; - color: var(--text-secondary); - font-family: inherit; -} - -.error-help code, -.detail-row code { - background: rgba(255, 255, 255, 0.05); - padding: 2px 6px; - border-radius: 6px; - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; - color: #eaf2ff; -} - -.ctrace-raw-output { - display: flex; - flex-direction: column; - gap: 12px; -} - -.raw-output-header { - font-size: 12px; - font-weight: 700; - color: var(--text-muted); - padding-bottom: 8px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.ctrace-metadata-compact, -.diagnostics-container { - width: 100%; - min-width: 0; - background: rgba(11, 15, 21, 0.48); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 18px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); -} - -.ctrace-metadata-compact { - padding: 16px; - margin-bottom: 12px; -} - -.metadata-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 12px; -} - -.metadata-accent { - width: 12px; - height: 28px; - border-radius: 999px; - background: linear-gradient(180deg, rgba(214, 222, 237, 0.94) 0%, rgba(214, 222, 237, 0.28) 100%); - flex-shrink: 0; -} - -.metadata-file-name { - font-size: 14px; - font-weight: 600; - color: #f0f5ff; - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.metadata-mode-badge { - background: rgba(214, 222, 237, 0.14); - color: #f1f6ff; - font-size: 10px; - font-weight: 700; - padding: 5px 9px; - border-radius: 999px; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.metadata-stats { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 10px; -} - -.stat-item { - display: flex; - flex-direction: column; - gap: 3px; - min-width: 0; - padding: 10px 12px; - border-radius: 14px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.04); -} - -.stat-label { - font-size: 10px; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.stat-value { - font-size: 12px; - color: var(--text-primary); - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.diagnostics-toolbar { - position: sticky; - top: 0; - z-index: 5; - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - padding: 14px 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.06); - background: #11161d; -} - -.diagnostics-count { - display: flex; - align-items: center; - gap: 10px; - flex: 1; - min-width: 0; - overflow: hidden; -} - -.count-badge { - padding: 5px 8px; - border-radius: 999px; - background: rgba(214, 222, 237, 0.12); - border: 1px solid rgba(214, 222, 237, 0.14); - color: #eef4ff; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - flex-shrink: 0; -} - -.count-badge-clear { - background: rgba(132, 190, 155, 0.14); - border-color: rgba(132, 190, 155, 0.18); -} - -.count-text { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - min-width: 0; -} - -.severity-filter { - flex-shrink: 0; -} - -.severity-filter select { - background: rgba(10, 13, 18, 0.78); - border: 1px solid rgba(255, 255, 255, 0.08); - color: var(--text-primary); - padding: 9px 12px; - border-radius: 12px; - font-size: 12px; - cursor: pointer; - outline: none; - transition: border-color 0.15s ease, box-shadow 0.15s ease; - font-weight: 500; -} - -.severity-filter select:hover, -.severity-filter select:focus { - border-color: rgba(214, 222, 237, 0.24); - box-shadow: 0 0 0 3px rgba(214, 222, 237, 0.08); -} - -.diagnostics-flat-list { - display: flex; - flex-direction: column; - gap: 12px; - padding: 14px; - overflow-x: hidden; -} - -.diagnostic-item { - width: 100%; - min-width: 0; - background: rgba(255, 255, 255, 0.035); - border: 1px solid rgba(255, 255, 255, 0.08); - border-left: 2px solid rgba(255, 255, 255, 0.18); - border-radius: 16px; - padding: 14px; - cursor: pointer; - transition: transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; -} - -.diagnostic-item:hover { - background: rgba(255, 255, 255, 0.055); - transform: translateY(-1px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.14); -} - -.diagnostic-item[data-severity="ERROR"] { - border-left-color: var(--danger); -} - -.diagnostic-item[data-severity="WARNING"] { - border-left-color: var(--warning); -} - -.diagnostic-item[data-severity="INFO"] { - border-left-color: #9ec2ff; -} - -.diagnostic-item-header { - display: flex; - gap: 12px; - align-items: flex-start; - margin-bottom: 12px; -} - -.diagnostic-severity-icon { - width: 32px; - height: 32px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - font-size: 12px; - font-weight: 700; - color: #0f1218; - flex-shrink: 0; -} - -.diagnostic-severity-icon-error, -.diagnostic-severity-icon-warning, -.diagnostic-severity-icon-info { - color: #0f1218; -} - -.diagnostic-item-info { - flex: 1; - min-width: 0; -} - -.diagnostic-title { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 6px; -} - -.diagnostic-rule { - font-size: 13px; - font-weight: 600; - color: #f0f5ff; - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; - flex: 1 1 100%; - min-width: 0; - line-height: 1.4; - overflow-wrap: anywhere; - word-break: break-word; -} - -.diagnostic-separator { - color: rgba(255, 255, 255, 0.22); - font-size: 12px; -} - -.diagnostic-function, -.diagnostic-location { - font-size: 11px; - color: var(--text-muted); - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; - min-width: 0; - overflow-wrap: anywhere; - word-break: break-word; -} - -.diagnostic-item-details { - padding-left: 44px; - display: flex; - flex-direction: column; - gap: 8px; - min-width: 0; -} - -.detail-row { - font-size: 11px; - color: var(--text-secondary); - line-height: 1.6; - min-width: 0; -} - -.detail-label { - color: var(--text-muted); - font-weight: 700; - text-transform: uppercase; - font-size: 10px; - letter-spacing: 0.08em; -} - -.detail-note { - color: var(--text-secondary); - padding: 11px 12px; - border-radius: 12px; - background: rgba(255, 255, 255, 0.03); - overflow-wrap: anywhere; - word-break: break-word; -} - -.diagnostics-empty { - padding: 34px 20px; - text-align: center; -} - -.diagnostic-decoration-error { - background: rgba(255, 107, 107, 0.12); - border-bottom: 2px wavy var(--danger); -} - -.diagnostic-decoration-warning { - background: rgba(255, 207, 102, 0.12); - border-bottom: 2px wavy var(--warning); -} - -.diagnostic-decoration-info { - background: rgba(158, 194, 255, 0.12); - border-bottom: 2px wavy #9ec2ff; -} - -.diagnostic-glyph-error::before, -.diagnostic-glyph-warning::before, -.diagnostic-glyph-info::before { - font-size: 10px; - font-weight: 700; - color: #f5f8ff; -} - -.diagnostic-glyph-error::before { - content: "E"; -} - -.diagnostic-glyph-warning::before { - content: "W"; -} - -.diagnostic-glyph-info::before { - content: "I"; -} - -#ctrace-output { - font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; -} - -#ctrace-output.has-diagnostics { - white-space: normal; - line-height: 1.6; -} +/* Diagnostics panel styles */ +.tools-section { + padding: 18px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.02); + overflow: hidden; + min-width: 0; +} + +.section-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; +} + +.section-icon { + width: 12px; + height: 12px; + border-radius: 4px; + background: linear-gradient(180deg, rgba(238, 244, 255, 0.88) 0%, rgba(186, 197, 214, 0.65) 100%); + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.03); + flex-shrink: 0; +} + +.section-title { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.config-group { + margin-bottom: 16px; +} + +.config-label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: 8px; +} + +.config-hint { + font-size: 10px; + font-weight: 400; + color: var(--text-muted); + font-style: italic; +} + +.config-input { + width: 100%; + background: rgba(9, 13, 19, 0.88); + border: 1px solid rgba(255, 255, 255, 0.08); + color: var(--text-primary); + padding: 11px 12px; + border-radius: 14px; + font-size: 12px; + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + outline: none; +} + +.config-input:hover { + border-color: rgba(255, 255, 255, 0.14); +} + +.config-input:focus { + border-color: rgba(214, 222, 237, 0.28); + box-shadow: 0 0 0 3px rgba(214, 222, 237, 0.1); +} + +.config-input::placeholder { + color: rgba(127, 139, 160, 0.8); +} + +.ctrace-run-btn { + width: 100%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.11) 0%, rgba(255, 255, 255, 0.07) 100%); + border: 1px solid rgba(255, 255, 255, 0.12); + color: #f5f8ff; + padding: 13px 16px; + border-radius: 16px; + cursor: pointer; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.01em; + transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); +} + +.ctrace-run-btn:hover { + transform: translateY(-1px); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.24); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.09) 100%); +} + +.ctrace-run-btn:active { + transform: translateY(0); +} + +.ctrace-run-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +.ctrace-btn-row { + display: flex; + gap: 8px; + align-items: stretch; +} + +.ctrace-btn-row .ctrace-run-btn { + flex: 1; + width: auto; +} + +.ctrace-clear-btn { + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.10); + color: var(--text-muted); + padding: 13px 14px; + border-radius: 16px; + cursor: pointer; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.01em; + transition: transform 0.18s ease, background-color 0.18s ease, color 0.18s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + white-space: nowrap; +} + +.ctrace-clear-btn:hover { + background: rgba(255, 107, 107, 0.10); + border-color: rgba(255, 107, 107, 0.22); + color: #ffb0b0; + transform: translateY(-1px); +} + +.ctrace-clear-btn:active { + transform: translateY(0); +} + +#ctrace-results-area { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 16px; +} + +.ctrace-placeholder, +.ctrace-loading, +.ctrace-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 56px 20px; + text-align: center; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 18px; + background: rgba(255, 255, 255, 0.02); +} + +.placeholder-mark { + width: 34px; + height: 34px; + border-radius: 10px; + margin-bottom: 16px; + background: linear-gradient(180deg, rgba(238, 244, 255, 0.84) 0%, rgba(186, 197, 214, 0.55) 100%); + opacity: 0.82; +} + +.placeholder-text, +.loading-text, +.error-text { + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 8px; +} + +.placeholder-subtext, +.loading-subtext, +.error-subtext, +.empty-text { + font-size: 13px; + color: var(--text-muted); +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid rgba(255, 255, 255, 0.1); + border-top: 3px solid rgba(214, 222, 237, 0.84); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 18px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.error-text { + color: #ffb0b0; +} + +.error-details, +.error-help, +.raw-output-content { + width: 100%; + background: rgba(10, 13, 18, 0.78); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 14px; + padding: 14px; + margin-top: 16px; + max-width: 100%; + overflow-x: hidden; + white-space: pre-wrap; + overflow-wrap: anywhere; + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + font-size: 11px; + color: var(--text-primary); + text-align: left; +} + +.error-help { + line-height: 1.65; + color: var(--text-secondary); + font-family: inherit; +} + +.error-help code, +.detail-row code { + background: rgba(255, 255, 255, 0.05); + padding: 2px 6px; + border-radius: 6px; + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + color: #eaf2ff; +} + +.ctrace-raw-output { + display: flex; + flex-direction: column; + gap: 12px; +} + +.raw-output-header { + font-size: 12px; + font-weight: 700; + color: var(--text-muted); + padding-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.ctrace-metadata-compact, +.diagnostics-container { + width: 100%; + min-width: 0; + background: rgba(11, 15, 21, 0.48); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 18px; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +.ctrace-metadata-compact { + padding: 16px; + margin-bottom: 12px; +} + +.metadata-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} + +.metadata-accent { + width: 12px; + height: 28px; + border-radius: 999px; + background: linear-gradient(180deg, rgba(214, 222, 237, 0.94) 0%, rgba(214, 222, 237, 0.28) 100%); + flex-shrink: 0; +} + +.metadata-file-name { + font-size: 14px; + font-weight: 600; + color: #f0f5ff; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.metadata-mode-badge { + background: rgba(214, 222, 237, 0.14); + color: #f1f6ff; + font-size: 10px; + font-weight: 700; + padding: 5px 9px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.metadata-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.stat-item { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + padding: 10px 12px; + border-radius: 14px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.04); +} + +.stat-label { + font-size: 10px; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.stat-value { + font-size: 12px; + color: var(--text-primary); + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.diagnostics-toolbar { + position: sticky; + top: 0; + z-index: 5; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + background: #11161d; +} + +.diagnostics-count { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; + overflow: hidden; +} + +.count-badge { + padding: 5px 8px; + border-radius: 999px; + background: rgba(214, 222, 237, 0.12); + border: 1px solid rgba(214, 222, 237, 0.14); + color: #eef4ff; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + flex-shrink: 0; +} + +.count-badge-clear { + background: rgba(132, 190, 155, 0.14); + border-color: rgba(132, 190, 155, 0.18); +} + +.count-text { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.severity-filter { + flex-shrink: 0; +} + +.severity-filter select { + background: rgba(10, 13, 18, 0.78); + border: 1px solid rgba(255, 255, 255, 0.08); + color: var(--text-primary); + padding: 9px 12px; + border-radius: 12px; + font-size: 12px; + cursor: pointer; + outline: none; + transition: border-color 0.15s ease, box-shadow 0.15s ease; + font-weight: 500; +} + +.severity-filter select:hover, +.severity-filter select:focus { + border-color: rgba(214, 222, 237, 0.24); + box-shadow: 0 0 0 3px rgba(214, 222, 237, 0.08); +} + +.diagnostics-flat-list { + display: flex; + flex-direction: column; + gap: 12px; + padding: 14px; + overflow-x: hidden; +} + +.diagnostic-item { + width: 100%; + min-width: 0; + background: rgba(255, 255, 255, 0.035); + border: 1px solid rgba(255, 255, 255, 0.08); + border-left: 2px solid rgba(255, 255, 255, 0.18); + border-radius: 16px; + padding: 14px; + cursor: pointer; + transition: transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; +} + +.diagnostic-item:hover { + background: rgba(255, 255, 255, 0.055); + transform: translateY(-1px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.14); +} + +.diagnostic-item[data-severity="ERROR"] { + border-left-color: var(--danger); +} + +.diagnostic-item[data-severity="WARNING"] { + border-left-color: var(--warning); +} + +.diagnostic-item[data-severity="INFO"] { + border-left-color: #9ec2ff; +} + +.diagnostic-item-header { + display: flex; + gap: 12px; + align-items: flex-start; + margin-bottom: 12px; +} + +.diagnostic-severity-icon { + width: 32px; + height: 32px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + color: #0f1218; + flex-shrink: 0; +} + +.diagnostic-severity-icon-error, +.diagnostic-severity-icon-warning, +.diagnostic-severity-icon-info { + color: #0f1218; +} + +.diagnostic-item-info { + flex: 1; + min-width: 0; +} + +.diagnostic-title { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 6px; +} + +.diagnostic-rule { + font-size: 13px; + font-weight: 600; + color: #f0f5ff; + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + flex: 1 1 100%; + min-width: 0; + line-height: 1.4; + overflow-wrap: anywhere; + word-break: break-word; +} + +.diagnostic-separator { + color: rgba(255, 255, 255, 0.22); + font-size: 12px; +} + +.diagnostic-function, +.diagnostic-location { + font-size: 11px; + color: var(--text-muted); + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + min-width: 0; + overflow-wrap: anywhere; + word-break: break-word; +} + +.diagnostic-item-details { + padding-left: 44px; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; +} + +.detail-row { + font-size: 11px; + color: var(--text-secondary); + line-height: 1.6; + min-width: 0; +} + +.detail-label { + color: var(--text-muted); + font-weight: 700; + text-transform: uppercase; + font-size: 10px; + letter-spacing: 0.08em; +} + +.detail-note { + color: var(--text-secondary); + padding: 11px 12px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + overflow-wrap: anywhere; + word-break: break-word; +} + +.diagnostics-empty { + padding: 34px 20px; + text-align: center; +} + +.diagnostic-decoration-error { + background: rgba(255, 107, 107, 0.12); + border-bottom: 2px wavy var(--danger); +} + +.diagnostic-decoration-warning { + background: rgba(255, 207, 102, 0.12); + border-bottom: 2px wavy var(--warning); +} + +.diagnostic-decoration-info { + background: rgba(158, 194, 255, 0.12); + border-bottom: 2px wavy #9ec2ff; +} + +.diagnostic-glyph-error::before, +.diagnostic-glyph-warning::before, +.diagnostic-glyph-info::before { + font-size: 10px; + font-weight: 700; + color: #f5f8ff; +} + +.diagnostic-glyph-error::before { + content: "E"; +} + +.diagnostic-glyph-warning::before { + content: "W"; +} + +.diagnostic-glyph-info::before { + content: "I"; +} + +#ctrace-output { + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; +} + +#ctrace-output.has-diagnostics { + white-space: normal; + line-height: 1.6; +} diff --git a/src/styles/editor.css b/src/styles/editor.css index 5d8c115..68d05d0 100644 --- a/src/styles/editor.css +++ b/src/styles/editor.css @@ -1,389 +1,389 @@ -/* ===== Split editor (two groups side by side) ===== */ -.editor-split-row { - flex: 1; - display: flex; - flex-direction: row; - min-height: 0; - min-width: 0; -} - -.editor-group { - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; - position: relative; -} - -.editor-group-main { - flex: 1 1 0; -} - -.editor-group-extra { - flex: 0 0 auto; - width: 45%; - min-width: 220px; -} - -/* Vertical drag handle between the two editor groups */ -.editor-split-resizer { - flex: 0 0 6px; - width: 6px; - cursor: col-resize; - background: rgba(255, 255, 255, 0.06); - z-index: 6; - transition: background-color 0.15s ease; -} -.editor-split-resizer:hover, -.editor-split-resizer.dragging { - background: rgba(88, 166, 255, 0.35); -} - -/* When split is active, flatten the two cards into one continuous panel so it - reads as a single editor divided by the resizer (not two separate windows). */ -.editor-split-row.split-active .editor-group-main > .tabs-container, -.editor-split-row.split-active .editor-group-main > .editor-container { - margin-right: 0; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: none; -} -.editor-split-row.split-active .editor-group-extra > .tabs-container, -.editor-split-row.split-active .editor-group-extra > .editor-container { - margin-left: 0; - margin-right: 0; - border-radius: 0; - border-left: none; -} -/* Round only the far-right edge of the whole split panel. */ -.editor-split-row.split-active .editor-group-extra:last-of-type > .tabs-container, -.editor-split-row.split-active .editor-group-extra:last-of-type > .editor-container { - margin-right: 12px; - border-top-right-radius: 12px; - border-bottom-right-radius: 12px; -} - -/* Secondary editor area (mirrors #editor-area minus the banners) */ -.editor-area-2 { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; - overflow: hidden; - background: var(--editor-bg); - position: relative; - width: 100%; - height: 100%; -} - -/* Drop-region indicator shown while dragging a tab over a group. - .show-center = add to this group; .show-right = open a new group to the right. */ -.editor-split-indicator { - position: absolute; - top: 10px; - left: 12px; - right: 12px; - bottom: 10px; - pointer-events: none; - display: none; - z-index: 50; - border-radius: 12px; -} -.editor-split-indicator.show-center { - display: block; - background: rgba(88, 166, 255, 0.14); - border: 2px solid rgba(88, 166, 255, 0.5); -} -.editor-split-indicator.show-right { - display: block; - background: linear-gradient(to right, transparent 60%, rgba(88, 166, 255, 0.2) 60%); - border-right: 3px solid rgba(88, 166, 255, 0.65); -} - -/* A tab being dragged */ -.tab.tab-dragging { - opacity: 0.5; -} - -/* Editor styles */ -.editor-container { - flex: 1; - display: flex; - flex-direction: column; - margin: 10px 12px; - border-radius: 12px; - background: - linear-gradient(180deg, rgba(15, 20, 29, 0.88) 0%, rgba(10, 14, 21, 0.96) 100%); - border: 1px solid rgba(255, 255, 255, 0.06); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); - position: relative; - overflow: hidden; - z-index: 1; -} - -.editor-main { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; - overflow: hidden; - width: 100%; - height: 100%; - position: relative; -} - -#editor-area { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; - min-width: 0; - overflow: hidden; - background: var(--editor-bg); - position: relative; - width: 100%; - height: 100%; -} - -.editor { - flex: 1; - width: 100%; - height: 100%; - min-height: 0; - min-width: 0; - overflow: hidden; -} - -.monaco-editor, -.monaco-editor .margin, -.monaco-editor .gutter, -.monaco-editor .overflow-guard, -.monaco-editor .sticky-widget, -.monaco-editor .sticky-widget .sticky-line-root { - background-color: var(--editor-bg) !important; -} - -/* Monaco uses a hidden