From acd73f682f62dfb3612120d6b243d39ef7ae4a5c Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Tue, 14 Apr 2026 09:18:11 +0200 Subject: [PATCH] feat: implement phase 3 agent pipeline and ticket review UI --- package-lock.json | 1477 +++++++++++++++++- package.json | 4 +- src-tauri/Cargo.lock | 12 + src-tauri/Cargo.toml | 5 +- src-tauri/src/commands/mod.rs | 2 + src-tauri/src/commands/orchestrator.rs | 83 + src-tauri/src/commands/worktree.rs | 104 ++ src-tauri/src/lib.rs | 16 +- src-tauri/src/models/mod.rs | 1 + src-tauri/src/models/ticket.rs | 138 +- src-tauri/src/models/worktree.rs | 199 +++ src-tauri/src/services/mod.rs | 2 + src-tauri/src/services/orchestrator.rs | 421 +++++ src-tauri/src/services/worktree_manager.rs | 274 ++++ src/App.tsx | 4 + src/components/projects/ProjectDashboard.tsx | 19 +- src/components/tickets/TicketDetail.tsx | 323 ++++ src/components/tickets/TicketList.tsx | 114 ++ src/lib/api.ts | 30 + src/lib/types.ts | 16 + 20 files changed, 3227 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/commands/orchestrator.rs create mode 100644 src-tauri/src/commands/worktree.rs create mode 100644 src-tauri/src/models/worktree.rs create mode 100644 src-tauri/src/services/orchestrator.rs create mode 100644 src-tauri/src/services/worktree_manager.rs create mode 100644 src/components/tickets/TicketDetail.tsx create mode 100644 src/components/tickets/TicketList.tsx diff --git a/package-lock.json b/package-lock.json index 7d5ea9a..9872af5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@tauri-apps/plugin-dialog": "^2.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.14.0" + "react-markdown": "^10.1.0", + "react-router-dom": "^7.14.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", @@ -1710,25 +1712,64 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.28", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -1746,6 +1787,18 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1767,6 +1820,16 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.18", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", @@ -1836,6 +1899,66 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1860,14 +1983,12 @@ "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/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1881,6 +2002,28 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1891,6 +2034,19 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.335", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz", @@ -1964,6 +2120,34 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2014,6 +2198,118 @@ "dev": true, "license": "ISC" }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -2317,6 +2613,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2349,11 +2655,853 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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/nanoid": { @@ -2382,6 +3530,31 @@ "dev": true, "license": "MIT" }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -2432,6 +3605,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -2459,6 +3642,33 @@ "react": "^18.3.1" } }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -2507,6 +3717,72 @@ "react-dom": ">=18" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -2587,6 +3863,48 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/tailwindcss": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", @@ -2625,6 +3943,26 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2639,6 +3977,93 @@ "node": ">=14.17" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2670,6 +4095,34 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", @@ -2752,6 +4205,16 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index c6da43e..ea4ee89 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "@tauri-apps/plugin-dialog": "^2.7.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^7.14.0" + "react-markdown": "^10.1.0", + "react-router-dom": "^7.14.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@tailwindcss/vite": "^4.2.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e030ba3..4fe72c9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2411,6 +2411,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tempfile", "tokio", "uuid", ] @@ -3551,6 +3552,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[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.9" @@ -4246,6 +4257,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.61.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9466249..8ade337 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,11 +27,14 @@ uuid = { version = "1", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] } dirs = "5" reqwest = { version = "0.12", features = ["json"] } -tokio = { version = "1", features = ["time", "sync", "macros"] } +tokio = { version = "1", features = ["time", "sync", "macros", "process", "io-util"] } aes-gcm = "0.10" rand = "0.8" base64 = "0.22" +[dev-dependencies] +tempfile = "3" + [profile.dev] incremental = true # Compiles your binary in smaller steps. diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index e404626..6f50de7 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,4 +1,6 @@ pub mod credential; +pub mod orchestrator; pub mod poller; pub mod project; pub mod tracker; +pub mod worktree; diff --git a/src-tauri/src/commands/orchestrator.rs b/src-tauri/src/commands/orchestrator.rs new file mode 100644 index 0000000..8894472 --- /dev/null +++ b/src-tauri/src/commands/orchestrator.rs @@ -0,0 +1,83 @@ +use crate::error::AppError; +use crate::models::ticket::ProcessedTicket; +use crate::models::worktree::Worktree; +use crate::AppState; +use serde::Serialize; +use tauri::State; + +#[derive(Debug, Clone, Serialize)] +pub struct TicketResult { + pub ticket: ProcessedTicket, + pub worktree: Option, +} + +#[tauri::command] +pub fn get_ticket_result( + state: State<'_, AppState>, + ticket_id: String, +) -> Result { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + let ticket = ProcessedTicket::get_by_id(&conn, &ticket_id)?; + let worktree = Worktree::get_by_ticket_id(&conn, &ticket_id)?; + Ok(TicketResult { ticket, worktree }) +} + +#[tauri::command] +pub fn retry_ticket( + state: State<'_, AppState>, + ticket_id: String, +) -> Result<(), AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + let ticket = ProcessedTicket::get_by_id(&conn, &ticket_id)?; + + if ticket.status != "Error" && ticket.status != "Done" && ticket.status != "Cancelled" { + return Err(AppError::from(format!( + "Cannot retry ticket with status '{}'", + ticket.status + ))); + } + + ProcessedTicket::update_status(&conn, &ticket_id, "Pending")?; + conn.execute( + "UPDATE processed_tickets SET analyst_report = NULL, developer_report = NULL, \ + worktree_path = NULL, branch_name = NULL, processed_at = NULL WHERE id = ?1", + rusqlite::params![ticket_id], + )?; + + if let Some(wt) = Worktree::get_by_ticket_id(&conn, &ticket_id)? { + if wt.status == "Active" { + let project_id = { + let tracker = crate::models::tracker::WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?; + tracker.project_id + }; + let project = crate::models::project::Project::get_by_id(&conn, &project_id)?; + let _ = crate::services::worktree_manager::delete_worktree( + &project.path, + &wt.path, + &wt.branch_name, + ); + } + Worktree::delete(&conn, &wt.id)?; + } + + Ok(()) +} + +#[tauri::command] +pub fn cancel_ticket( + state: State<'_, AppState>, + ticket_id: String, +) -> Result<(), AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + let ticket = ProcessedTicket::get_by_id(&conn, &ticket_id)?; + + if ticket.status == "Done" || ticket.status == "Cancelled" { + return Err(AppError::from(format!( + "Cannot cancel ticket with status '{}'", + ticket.status + ))); + } + + ProcessedTicket::update_status(&conn, &ticket_id, "Cancelled")?; + Ok(()) +} diff --git a/src-tauri/src/commands/worktree.rs b/src-tauri/src/commands/worktree.rs new file mode 100644 index 0000000..5b14796 --- /dev/null +++ b/src-tauri/src/commands/worktree.rs @@ -0,0 +1,104 @@ +use crate::error::AppError; +use crate::models::project::Project; +use crate::models::ticket::ProcessedTicket; +use crate::models::tracker::WatchedTracker; +use crate::models::worktree::Worktree; +use crate::services::worktree_manager; +use crate::AppState; +use tauri::State; + +#[tauri::command] +pub fn list_worktrees( + state: State<'_, AppState>, + project_id: String, +) -> Result, AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + let worktrees = Worktree::list_by_project(&conn, &project_id)?; + Ok(worktrees) +} + +#[tauri::command] +pub fn get_worktree_diff( + state: State<'_, AppState>, + worktree_id: String, +) -> Result { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + + let wt = Worktree::get_by_id(&conn, &worktree_id)?; + let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?; + let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?; + let project = Project::get_by_id(&conn, &tracker.project_id)?; + + drop(conn); + + let diff = worktree_manager::get_diff(&project.path, &project.base_branch, &wt.branch_name) + .map_err(AppError::from)?; + + Ok(diff) +} + +#[tauri::command] +pub fn apply_fix_to_branch( + state: State<'_, AppState>, + worktree_id: String, + target_branch: String, +) -> Result<(), AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + + let wt = Worktree::get_by_id(&conn, &worktree_id)?; + let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?; + let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?; + let project = Project::get_by_id(&conn, &tracker.project_id)?; + + drop(conn); + + worktree_manager::apply_fix( + &project.path, + &project.base_branch, + &wt.branch_name, + &target_branch, + ) + .map_err(AppError::from)?; + + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + Worktree::set_merged(&conn, &worktree_id, &target_branch)?; + + Ok(()) +} + +#[tauri::command] +pub fn delete_worktree_cmd( + state: State<'_, AppState>, + worktree_id: String, +) -> Result<(), AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + + let wt = Worktree::get_by_id(&conn, &worktree_id)?; + let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?; + let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?; + let project = Project::get_by_id(&conn, &tracker.project_id)?; + + drop(conn); + + worktree_manager::delete_worktree(&project.path, &wt.path, &wt.branch_name) + .map_err(AppError::from)?; + + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + Worktree::delete(&conn, &worktree_id)?; + + Ok(()) +} + +#[tauri::command] +pub fn list_local_branches( + state: State<'_, AppState>, + project_id: String, +) -> Result, AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + let project = Project::get_by_id(&conn, &project_id)?; + + drop(conn); + + let branches = worktree_manager::list_local_branches(&project.path).map_err(AppError::from)?; + Ok(branches) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c735851..7858471 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -38,12 +38,18 @@ pub fn run() { // Start background poller services::poller::start( - db_arc, + db_arc.clone(), encryption_key, http_client, app.handle().clone(), ); + // Start agent orchestrator + services::orchestrator::start( + db_arc, + app.handle().clone(), + ); + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -64,6 +70,14 @@ pub fn run() { commands::tracker::list_processed_tickets, commands::poller::manual_poll, commands::poller::get_queue_status, + commands::orchestrator::get_ticket_result, + commands::orchestrator::retry_ticket, + commands::orchestrator::cancel_ticket, + commands::worktree::list_worktrees, + commands::worktree::get_worktree_diff, + commands::worktree::apply_fix_to_branch, + commands::worktree::delete_worktree_cmd, + commands::worktree::list_local_branches, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/models/mod.rs b/src-tauri/src/models/mod.rs index 5caa5db..4499f10 100644 --- a/src-tauri/src/models/mod.rs +++ b/src-tauri/src/models/mod.rs @@ -2,3 +2,4 @@ pub mod credential; pub mod project; pub mod ticket; pub mod tracker; +pub mod worktree; diff --git a/src-tauri/src/models/ticket.rs b/src-tauri/src/models/ticket.rs index 201ce8e..2230cd0 100644 --- a/src-tauri/src/models/ticket.rs +++ b/src-tauri/src/models/ticket.rs @@ -35,7 +35,6 @@ fn from_row(row: &rusqlite::Row) -> rusqlite::Result { }) } -#[allow(dead_code)] const SELECT_ALL_COLS: &str = "SELECT id, tracker_id, artifact_id, artifact_title, artifact_data, \ status, analyst_report, developer_report, worktree_path, branch_name, \ detected_at, processed_at FROM processed_tickets"; @@ -92,7 +91,6 @@ impl ProcessedTicket { Ok(count > 0) } - #[allow(dead_code)] pub fn list_by_tracker(conn: &Connection, tracker_id: &str) -> Result> { let sql = format!( "{} WHERE tracker_id = ?1 ORDER BY detected_at DESC", @@ -117,11 +115,65 @@ impl ProcessedTicket { rows.collect() } - #[allow(dead_code)] pub fn get_by_id(conn: &Connection, id: &str) -> Result { let sql = format!("{} WHERE id = ?1", SELECT_ALL_COLS); conn.query_row(&sql, params![id], from_row) } + + pub fn update_status(conn: &Connection, id: &str, status: &str) -> Result<()> { + conn.execute( + "UPDATE processed_tickets SET status = ?1 WHERE id = ?2", + params![status, id], + )?; + Ok(()) + } + + pub fn set_analyst_report(conn: &Connection, id: &str, report: &str) -> Result<()> { + conn.execute( + "UPDATE processed_tickets SET analyst_report = ?1 WHERE id = ?2", + params![report, id], + )?; + Ok(()) + } + + pub fn set_developer_report(conn: &Connection, id: &str, report: &str) -> Result<()> { + conn.execute( + "UPDATE processed_tickets SET developer_report = ?1, processed_at = datetime('now') WHERE id = ?2", + params![report, id], + )?; + Ok(()) + } + + pub fn set_worktree_info( + conn: &Connection, + id: &str, + worktree_path: &str, + branch_name: &str, + ) -> Result<()> { + conn.execute( + "UPDATE processed_tickets SET worktree_path = ?1, branch_name = ?2 WHERE id = ?3", + params![worktree_path, branch_name, id], + )?; + Ok(()) + } + + pub fn list_pending(conn: &Connection) -> Result> { + let sql = format!( + "{} WHERE status = 'Pending' ORDER BY detected_at ASC", + SELECT_ALL_COLS + ); + let mut stmt = conn.prepare(&sql)?; + let rows = stmt.query_map([], from_row)?; + rows.collect() + } + + pub fn set_error(conn: &Connection, id: &str, error_message: &str) -> Result<()> { + conn.execute( + "UPDATE processed_tickets SET status = 'Error', analyst_report = COALESCE(analyst_report, '') || ?1, processed_at = datetime('now') WHERE id = ?2", + params![error_message, id], + )?; + Ok(()) + } } #[cfg(test)] @@ -239,4 +291,84 @@ mod tests { assert_eq!(found.artifact_title, "Not Found Bug"); assert_eq!(found.status, "Pending"); } + + #[test] + fn test_update_status() { + let (conn, tracker_id) = setup(); + let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}") + .unwrap() + .unwrap(); + + ProcessedTicket::update_status(&conn, &ticket.id, "Analyzing").unwrap(); + let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); + assert_eq!(updated.status, "Analyzing"); + } + + #[test] + fn test_set_analyst_report() { + let (conn, tracker_id) = setup(); + let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}") + .unwrap() + .unwrap(); + + ProcessedTicket::set_analyst_report(&conn, &ticket.id, "## Report\nAll good.").unwrap(); + let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); + assert_eq!(updated.analyst_report.unwrap(), "## Report\nAll good."); + } + + #[test] + fn test_set_developer_report() { + let (conn, tracker_id) = setup(); + let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}") + .unwrap() + .unwrap(); + + ProcessedTicket::set_developer_report(&conn, &ticket.id, "Fixed in main.rs").unwrap(); + let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); + assert_eq!(updated.developer_report.unwrap(), "Fixed in main.rs"); + assert!(updated.processed_at.is_some()); + } + + #[test] + fn test_set_worktree_info() { + let (conn, tracker_id) = setup(); + let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}") + .unwrap() + .unwrap(); + + ProcessedTicket::set_worktree_info(&conn, &ticket.id, "/tmp/wt", "orchai/1").unwrap(); + let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); + assert_eq!(updated.worktree_path.unwrap(), "/tmp/wt"); + assert_eq!(updated.branch_name.unwrap(), "orchai/1"); + } + + #[test] + fn test_list_pending() { + let (conn, tracker_id) = setup(); + ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}").unwrap(); + ProcessedTicket::insert_if_new(&conn, &tracker_id, 2, "T2", "{}").unwrap(); + + let pending = ProcessedTicket::list_pending(&conn).unwrap(); + assert_eq!(pending.len(), 2); + assert_eq!(pending[0].artifact_id, 1); + assert_eq!(pending[1].artifact_id, 2); + + ProcessedTicket::update_status(&conn, &pending[0].id, "Analyzing").unwrap(); + let pending2 = ProcessedTicket::list_pending(&conn).unwrap(); + assert_eq!(pending2.len(), 1); + assert_eq!(pending2[0].artifact_id, 2); + } + + #[test] + fn test_set_error() { + let (conn, tracker_id) = setup(); + let ticket = ProcessedTicket::insert_if_new(&conn, &tracker_id, 1, "T1", "{}") + .unwrap() + .unwrap(); + + ProcessedTicket::set_error(&conn, &ticket.id, "CLI timeout after 600s").unwrap(); + let updated = ProcessedTicket::get_by_id(&conn, &ticket.id).unwrap(); + assert_eq!(updated.status, "Error"); + assert_eq!(updated.analyst_report.unwrap(), "CLI timeout after 600s"); + } } diff --git a/src-tauri/src/models/worktree.rs b/src-tauri/src/models/worktree.rs new file mode 100644 index 0000000..ae09afc --- /dev/null +++ b/src-tauri/src/models/worktree.rs @@ -0,0 +1,199 @@ +use rusqlite::{params, Connection, Result}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Worktree { + pub id: String, + pub ticket_id: String, + pub path: String, + pub branch_name: String, + pub status: String, + pub created_at: String, + pub merged_at: Option, + pub merged_into: Option, +} + +fn from_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(Worktree { + id: row.get(0)?, + ticket_id: row.get(1)?, + path: row.get(2)?, + branch_name: row.get(3)?, + status: row.get(4)?, + created_at: row.get(5)?, + merged_at: row.get(6)?, + merged_into: row.get(7)?, + }) +} + +const SELECT_ALL_COLS: &str = "SELECT id, ticket_id, path, branch_name, status, \ + created_at, merged_at, merged_into FROM worktrees"; + +impl Worktree { + pub fn insert(conn: &Connection, ticket_id: &str, path: &str, branch_name: &str) -> Result { + let id = Uuid::new_v4().to_string(); + let now = chrono::Utc::now().to_rfc3339(); + + conn.execute( + "INSERT INTO worktrees (id, ticket_id, path, branch_name, status, created_at) \ + VALUES (?1, ?2, ?3, ?4, 'Active', ?5)", + params![id, ticket_id, path, branch_name, now], + )?; + + Ok(Worktree { + id, + ticket_id: ticket_id.to_string(), + path: path.to_string(), + branch_name: branch_name.to_string(), + status: "Active".to_string(), + created_at: now, + merged_at: None, + merged_into: None, + }) + } + + pub fn get_by_id(conn: &Connection, id: &str) -> Result { + let sql = format!("{} WHERE id = ?1", SELECT_ALL_COLS); + conn.query_row(&sql, params![id], from_row) + } + + pub fn get_by_ticket_id(conn: &Connection, ticket_id: &str) -> Result> { + let sql = format!("{} WHERE ticket_id = ?1", SELECT_ALL_COLS); + let mut stmt = conn.prepare(&sql)?; + let mut rows = stmt.query_map(params![ticket_id], from_row)?; + match rows.next() { + Some(Ok(w)) => Ok(Some(w)), + Some(Err(e)) => Err(e), + None => Ok(None), + } + } + + pub fn list_by_project(conn: &Connection, project_id: &str) -> Result> { + let sql = "SELECT w.id, w.ticket_id, w.path, w.branch_name, w.status, \ + w.created_at, w.merged_at, w.merged_into \ + FROM worktrees w \ + JOIN processed_tickets pt ON w.ticket_id = pt.id \ + JOIN watched_trackers wt ON pt.tracker_id = wt.id \ + WHERE wt.project_id = ?1 \ + ORDER BY w.created_at DESC"; + let mut stmt = conn.prepare(sql)?; + let rows = stmt.query_map(params![project_id], from_row)?; + rows.collect() + } + + pub fn set_merged(conn: &Connection, id: &str, target_branch: &str) -> Result<()> { + conn.execute( + "UPDATE worktrees SET status = 'Merged', merged_at = datetime('now'), merged_into = ?1 WHERE id = ?2", + params![target_branch, id], + )?; + Ok(()) + } + + pub fn delete(conn: &Connection, id: &str) -> Result<()> { + conn.execute("DELETE FROM worktrees WHERE id = ?1", params![id])?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db; + use crate::models::project::Project; + use crate::models::ticket::ProcessedTicket; + use crate::models::tracker::{AgentConfig, WatchedTracker}; + + fn setup() -> (Connection, String) { + let conn = db::init_in_memory().expect("db init"); + let project = Project::insert(&conn, "Test", "/path", None, "main").unwrap(); + let agent_config = AgentConfig { + analyst_command: "echo".into(), + analyst_args: vec![], + developer_command: "echo".into(), + developer_args: vec![], + }; + let tracker = WatchedTracker::insert(&conn, &project.id, 100, "Bugs", 10, agent_config, vec![]) + .unwrap(); + let ticket = ProcessedTicket::insert_if_new(&conn, &tracker.id, 42, "Bug 42", "{}") + .unwrap() + .unwrap(); + (conn, ticket.id) + } + + #[test] + fn test_insert_and_get_by_id() { + let (conn, ticket_id) = setup(); + + let wt = Worktree::insert(&conn, &ticket_id, "/tmp/orchai-42", "orchai/42").unwrap(); + assert_eq!(wt.status, "Active"); + assert_eq!(wt.branch_name, "orchai/42"); + + let found = Worktree::get_by_id(&conn, &wt.id).unwrap(); + assert_eq!(found.id, wt.id); + assert_eq!(found.ticket_id, ticket_id); + assert_eq!(found.path, "/tmp/orchai-42"); + } + + #[test] + fn test_get_by_ticket_id() { + let (conn, ticket_id) = setup(); + + let none = Worktree::get_by_ticket_id(&conn, &ticket_id).unwrap(); + assert!(none.is_none()); + + Worktree::insert(&conn, &ticket_id, "/tmp/wt", "orchai/42").unwrap(); + + let some = Worktree::get_by_ticket_id(&conn, &ticket_id).unwrap(); + assert!(some.is_some()); + assert_eq!(some.unwrap().ticket_id, ticket_id); + } + + #[test] + fn test_list_by_project() { + let conn = db::init_in_memory().expect("db init"); + let project = Project::insert(&conn, "P1", "/path", None, "main").unwrap(); + let agent_config = AgentConfig { + analyst_command: "echo".into(), + analyst_args: vec![], + developer_command: "echo".into(), + developer_args: vec![], + }; + let tracker = WatchedTracker::insert(&conn, &project.id, 100, "Bugs", 10, agent_config, vec![]) + .unwrap(); + let t1 = ProcessedTicket::insert_if_new(&conn, &tracker.id, 1, "T1", "{}") + .unwrap() + .unwrap(); + let t2 = ProcessedTicket::insert_if_new(&conn, &tracker.id, 2, "T2", "{}") + .unwrap() + .unwrap(); + + Worktree::insert(&conn, &t1.id, "/wt1", "orchai/1").unwrap(); + Worktree::insert(&conn, &t2.id, "/wt2", "orchai/2").unwrap(); + + let worktrees = Worktree::list_by_project(&conn, &project.id).unwrap(); + assert_eq!(worktrees.len(), 2); + } + + #[test] + fn test_set_merged() { + let (conn, ticket_id) = setup(); + let wt = Worktree::insert(&conn, &ticket_id, "/tmp/wt", "orchai/42").unwrap(); + + Worktree::set_merged(&conn, &wt.id, "feature/login").unwrap(); + let updated = Worktree::get_by_id(&conn, &wt.id).unwrap(); + assert_eq!(updated.status, "Merged"); + assert_eq!(updated.merged_into.unwrap(), "feature/login"); + assert!(updated.merged_at.is_some()); + } + + #[test] + fn test_delete() { + let (conn, ticket_id) = setup(); + let wt = Worktree::insert(&conn, &ticket_id, "/tmp/wt", "orchai/42").unwrap(); + + Worktree::delete(&conn, &wt.id).unwrap(); + let result = Worktree::get_by_id(&conn, &wt.id); + assert!(result.is_err()); + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index c335479..af6a5bf 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -1,4 +1,6 @@ pub mod crypto; pub mod filter_engine; +pub mod orchestrator; pub mod poller; pub mod tuleap_client; +pub mod worktree_manager; diff --git a/src-tauri/src/services/orchestrator.rs b/src-tauri/src/services/orchestrator.rs new file mode 100644 index 0000000..2436e19 --- /dev/null +++ b/src-tauri/src/services/orchestrator.rs @@ -0,0 +1,421 @@ +use crate::models::project::Project; +use crate::models::ticket::ProcessedTicket; +use crate::models::tracker::WatchedTracker; +use crate::models::worktree::Worktree; +use crate::services::worktree_manager; +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; +use tokio::time::{interval, timeout, Duration}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Verdict { + FixNeeded, + NoFix, +} + +pub fn build_analyst_prompt(ticket: &ProcessedTicket, project: &Project) -> String { + format!( + r#"Tu es un analyste technique. Voici un ticket Tuleap a analyser. + +## Ticket +- ID: {artifact_id} +- Titre: {title} +- Donnees: {data} + +## Contexte +- Projet: {project_name} +- Repo: {project_path} +- Branche de base: {base_branch} + +## Ta mission +1. Analyse le ticket et identifie les fichiers/fonctions concernes +2. Explique techniquement le probleme +3. Evalue si une correction de code est necessaire +4. Produis un rapport structure en markdown + +Termine ton rapport par un de ces verdicts sur une ligne separee: +[VERDICT: FIX_NEEDED] si une correction de code est necessaire +[VERDICT: NO_FIX] si aucune correction n'est necessaire"#, + artifact_id = ticket.artifact_id, + title = ticket.artifact_title, + data = ticket.artifact_data, + project_name = project.name, + project_path = project.path, + base_branch = project.base_branch, + ) +} + +pub fn build_developer_prompt( + ticket: &ProcessedTicket, + project: &Project, + analyst_report: &str, + worktree_path: &str, +) -> String { + format!( + r#"Tu es un developpeur. Tu dois corriger un bug ou implementer une fonctionnalite d'apres l'analyse suivante. + +## Rapport d'analyse +{analyst_report} + +## Ticket +- ID: {artifact_id} +- Titre: {title} + +## Contexte +- Projet: {project_name} +- Repo (worktree): {worktree_path} +- Branche de base: {base_branch} + +## Ta mission +1. Implemente la correction dans le code +2. Fais des commits atomiques avec des messages clairs +3. Produis un rapport en markdown decrivant les changements effectues"#, + analyst_report = analyst_report, + artifact_id = ticket.artifact_id, + title = ticket.artifact_title, + project_name = project.name, + worktree_path = worktree_path, + base_branch = project.base_branch, + ) +} + +pub fn parse_verdict(report: &str) -> Verdict { + for line in report.lines().rev() { + let trimmed = line.trim(); + if trimmed.contains("[VERDICT: NO_FIX]") { + return Verdict::NoFix; + } + if trimmed.contains("[VERDICT: FIX_NEEDED]") { + return Verdict::FixNeeded; + } + } + Verdict::FixNeeded +} + +pub async fn run_cli_command( + command: &str, + args: &[String], + prompt: &str, + working_dir: &str, + timeout_secs: u64, + app_handle: &AppHandle, + ticket_id: &str, +) -> Result { + let mut child = Command::new(command) + .args(args) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .current_dir(working_dir) + .spawn() + .map_err(|e| format!("Failed to spawn '{}': {}", command, e))?; + + if let Some(mut stdin) = child.stdin.take() { + use tokio::io::AsyncWriteExt; + stdin + .write_all(prompt.as_bytes()) + .await + .map_err(|e| format!("Failed to write to stdin: {}", e))?; + } + + let stdout = child.stdout.take().ok_or("Failed to capture stdout")?; + let mut reader = BufReader::new(stdout).lines(); + let mut output = String::new(); + + let read_future = async { + while let Ok(Some(line)) = reader.next_line().await { + let _ = app_handle.emit( + "ticket-processing-progress", + serde_json::json!({ + "ticket_id": ticket_id, + "output_chunk": line, + }), + ); + output.push_str(&line); + output.push('\n'); + } + output + }; + + let result = timeout(Duration::from_secs(timeout_secs), read_future) + .await + .map_err(|_| format!("CLI command timed out after {}s", timeout_secs))?; + + let status = child + .wait() + .await + .map_err(|e| format!("Failed to wait for process: {}", e))?; + + if !status.success() { + let code = status.code().unwrap_or(-1); + return Err(format!("CLI command exited with code {}", code)); + } + + Ok(result) +} + +async fn process_ticket(db: &Arc>, app_handle: &AppHandle) -> Result { + let (ticket, tracker, project) = { + let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?; + + let pending = ProcessedTicket::list_pending(&conn).map_err(|e| format!("list_pending failed: {}", e))?; + + let ticket = match pending.into_iter().next() { + Some(t) => t, + None => return Ok(false), + }; + + let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id) + .map_err(|e| format!("get tracker failed: {}", e))?; + + let project = Project::get_by_id(&conn, &tracker.project_id) + .map_err(|e| format!("get project failed: {}", e))?; + + ProcessedTicket::update_status(&conn, &ticket.id, "Analyzing") + .map_err(|e| format!("update_status failed: {}", e))?; + + (ticket, tracker, project) + }; + + let _ = app_handle.emit( + "ticket-processing-started", + serde_json::json!({ + "ticket_id": ticket.id, + "step": "analyst", + }), + ); + + let analyst_prompt = build_analyst_prompt(&ticket, &project); + let analyst_result = run_cli_command( + &tracker.agent_config.analyst_command, + &tracker.agent_config.analyst_args, + &analyst_prompt, + &project.path, + 600, + app_handle, + &ticket.id, + ) + .await; + + let analyst_report = match analyst_result { + Ok(report) => report, + Err(e) => { + let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?; + let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e); + let _ = app_handle.emit( + "ticket-processing-error", + serde_json::json!({ "ticket_id": ticket.id, "error": e }), + ); + return Ok(true); + } + }; + + { + let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?; + ProcessedTicket::set_analyst_report(&conn, &ticket.id, &analyst_report) + .map_err(|e| format!("set_analyst_report: {}", e))?; + } + + let verdict = parse_verdict(&analyst_report); + if verdict == Verdict::NoFix { + let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?; + ProcessedTicket::update_status(&conn, &ticket.id, "Done") + .map_err(|e| format!("update_status: {}", e))?; + let _ = app_handle.emit( + "ticket-processing-done", + serde_json::json!({ "ticket_id": ticket.id }), + ); + return Ok(true); + } + + { + let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?; + let current = ProcessedTicket::get_by_id(&conn, &ticket.id).map_err(|e| format!("get_by_id: {}", e))?; + if current.status == "Cancelled" { + return Ok(true); + } + } + + let (wt_path, branch_name) = + worktree_manager::create_worktree(&project.path, &project.base_branch, ticket.artifact_id).map_err(|e| { + let conn = db.lock().ok(); + if let Some(conn) = conn { + let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e); + } + e + })?; + + { + let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?; + ProcessedTicket::set_worktree_info(&conn, &ticket.id, &wt_path, &branch_name) + .map_err(|e| format!("set_worktree_info: {}", e))?; + Worktree::insert(&conn, &ticket.id, &wt_path, &branch_name) + .map_err(|e| format!("insert worktree: {}", e))?; + ProcessedTicket::update_status(&conn, &ticket.id, "Developing") + .map_err(|e| format!("update_status: {}", e))?; + } + + let _ = app_handle.emit( + "ticket-processing-started", + serde_json::json!({ + "ticket_id": ticket.id, + "step": "developer", + }), + ); + + let developer_prompt = build_developer_prompt(&ticket, &project, &analyst_report, &wt_path); + let developer_result = run_cli_command( + &tracker.agent_config.developer_command, + &tracker.agent_config.developer_args, + &developer_prompt, + &wt_path, + 600, + app_handle, + &ticket.id, + ) + .await; + + let developer_report = match developer_result { + Ok(report) => report, + Err(e) => { + let conn = db.lock().map_err(|e2| format!("DB lock: {}", e2))?; + let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e); + let _ = app_handle.emit( + "ticket-processing-error", + serde_json::json!({ "ticket_id": ticket.id, "error": e }), + ); + return Ok(true); + } + }; + + { + let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?; + ProcessedTicket::set_developer_report(&conn, &ticket.id, &developer_report) + .map_err(|e| format!("set_developer_report: {}", e))?; + ProcessedTicket::update_status(&conn, &ticket.id, "Done") + .map_err(|e| format!("update_status: {}", e))?; + } + + let _ = app_handle.emit( + "ticket-processing-done", + serde_json::json!({ "ticket_id": ticket.id }), + ); + + Ok(true) +} + +pub fn start(db: Arc>, app_handle: AppHandle) { + tokio::spawn(async move { + let mut tick = interval(Duration::from_secs(10)); + loop { + tick.tick().await; + match process_ticket(&db, &app_handle).await { + Ok(true) => { + continue; + } + Ok(false) => {} + Err(e) => { + eprintln!("orchestrator: {}", e); + } + } + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_build_analyst_prompt_contains_ticket_info() { + let ticket = ProcessedTicket { + id: "t1".into(), + tracker_id: "tr1".into(), + artifact_id: 42, + artifact_title: "Login crash on empty password".into(), + artifact_data: r#"{"id":42,"title":"Login crash"}"#.into(), + status: "Pending".into(), + analyst_report: None, + developer_report: None, + worktree_path: None, + branch_name: None, + detected_at: "2026-01-01T00:00:00Z".into(), + processed_at: None, + }; + let project = Project { + id: "p1".into(), + name: "MyApp".into(), + path: "/home/user/myapp".into(), + cloned_from: None, + base_branch: "stable".into(), + created_at: "2026-01-01T00:00:00Z".into(), + }; + + let prompt = build_analyst_prompt(&ticket, &project); + assert!(prompt.contains("42")); + assert!(prompt.contains("Login crash on empty password")); + assert!(prompt.contains("MyApp")); + assert!(prompt.contains("/home/user/myapp")); + assert!(prompt.contains("stable")); + assert!(prompt.contains("[VERDICT: FIX_NEEDED]")); + assert!(prompt.contains("[VERDICT: NO_FIX]")); + } + + #[test] + fn test_build_developer_prompt_contains_report() { + let ticket = ProcessedTicket { + id: "t1".into(), + tracker_id: "tr1".into(), + artifact_id: 42, + artifact_title: "Login crash".into(), + artifact_data: "{}".into(), + status: "Developing".into(), + analyst_report: None, + developer_report: None, + worktree_path: None, + branch_name: None, + detected_at: "2026-01-01T00:00:00Z".into(), + processed_at: None, + }; + let project = Project { + id: "p1".into(), + name: "MyApp".into(), + path: "/home/user/myapp".into(), + cloned_from: None, + base_branch: "main".into(), + created_at: "2026-01-01T00:00:00Z".into(), + }; + + let prompt = build_developer_prompt(&ticket, &project, "## Bug found in auth.rs", "/tmp/wt"); + assert!(prompt.contains("## Bug found in auth.rs")); + assert!(prompt.contains("42")); + assert!(prompt.contains("/tmp/wt")); + } + + #[test] + fn test_parse_verdict_fix_needed() { + let report = "## Analysis\nBug found.\n[VERDICT: FIX_NEEDED]\n"; + assert_eq!(parse_verdict(report), Verdict::FixNeeded); + } + + #[test] + fn test_parse_verdict_no_fix() { + let report = "## Analysis\nThis is a feature request, not a bug.\n[VERDICT: NO_FIX]\n"; + assert_eq!(parse_verdict(report), Verdict::NoFix); + } + + #[test] + fn test_parse_verdict_missing_defaults_to_fix() { + let report = "## Analysis\nSomething is wrong but I forgot the verdict."; + assert_eq!(parse_verdict(report), Verdict::FixNeeded); + } + + #[test] + fn test_parse_verdict_embedded_in_line() { + let report = "Verdict: [VERDICT: NO_FIX] - no code change needed."; + assert_eq!(parse_verdict(report), Verdict::NoFix); + } +} diff --git a/src-tauri/src/services/worktree_manager.rs b/src-tauri/src/services/worktree_manager.rs new file mode 100644 index 0000000..084a73b --- /dev/null +++ b/src-tauri/src/services/worktree_manager.rs @@ -0,0 +1,274 @@ +use std::path::Path; +use std::process::Command; + +fn run_git(project_path: &str, args: &[&str]) -> Result { + let output = Command::new("git") + .args(args) + .current_dir(project_path) + .output() + .map_err(|e| format!("Failed to run git: {}", e))?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!("git {} failed: {}", args.join(" "), stderr)) + } +} + +pub fn create_worktree( + project_path: &str, + base_branch: &str, + artifact_id: i32, +) -> Result<(String, String), String> { + let orchai_dir = Path::new(project_path).join(".orchai").join("worktrees"); + std::fs::create_dir_all(&orchai_dir) + .map_err(|e| format!("Failed to create .orchai/worktrees dir: {}", e))?; + + let worktree_name = format!("orchai-{}", artifact_id); + let worktree_path = orchai_dir.join(&worktree_name); + let branch_name = format!("orchai/{}", artifact_id); + + let wt_path_str = worktree_path.to_str().ok_or("Invalid worktree path")?; + + run_git( + project_path, + &["worktree", "add", wt_path_str, "-b", &branch_name, base_branch], + )?; + + Ok((wt_path_str.to_string(), branch_name)) +} + +pub fn get_diff(project_path: &str, base_branch: &str, branch_name: &str) -> Result { + let range = format!("{}...{}", base_branch, branch_name); + run_git(project_path, &["diff", &range]) +} + +pub fn list_commits( + project_path: &str, + base_branch: &str, + branch_name: &str, +) -> Result, String> { + let range = format!("{}..{}", base_branch, branch_name); + let output = run_git(project_path, &["log", &range, "--format=%H", "--reverse"])?; + Ok(output + .lines() + .filter(|l| !l.is_empty()) + .map(String::from) + .collect()) +} + +pub fn apply_fix( + project_path: &str, + base_branch: &str, + branch_name: &str, + target_branch: &str, +) -> Result<(), String> { + let commits = list_commits(project_path, base_branch, branch_name)?; + if commits.is_empty() { + return Err("No commits to cherry-pick".to_string()); + } + + let current = run_git(project_path, &["rev-parse", "--abbrev-ref", "HEAD"])?; + let current = current.trim(); + + run_git(project_path, &["checkout", target_branch])?; + + let mut cherry_args = vec!["cherry-pick"]; + let commit_refs: Vec<&str> = commits.iter().map(|s| s.as_str()).collect(); + cherry_args.extend(&commit_refs); + + let result = run_git(project_path, &cherry_args); + + if let Err(e) = &result { + let _ = run_git(project_path, &["cherry-pick", "--abort"]); + let _ = run_git(project_path, &["checkout", current]); + return Err(format!("Cherry-pick failed (conflict?): {}", e)); + } + + run_git(project_path, &["checkout", current])?; + + Ok(()) +} + +pub fn delete_worktree( + project_path: &str, + worktree_path: &str, + branch_name: &str, +) -> Result<(), String> { + run_git(project_path, &["worktree", "remove", worktree_path, "--force"])?; + let _ = run_git(project_path, &["branch", "-D", branch_name]); + Ok(()) +} + +pub fn list_local_branches(project_path: &str) -> Result, String> { + let output = run_git(project_path, &["branch", "--format=%(refname:short)"])?; + Ok(output + .lines() + .filter(|l| !l.is_empty()) + .map(String::from) + .collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::process::Command; + + fn setup_test_repo() -> tempfile::TempDir { + let dir = tempfile::tempdir().expect("create temp dir"); + let path = dir.path().to_str().unwrap(); + + Command::new("git").args(["init"]).current_dir(path).output().unwrap(); + Command::new("git") + .args(["config", "user.email", "test@test.com"]) + .current_dir(path) + .output() + .unwrap(); + Command::new("git") + .args(["config", "user.name", "Test"]) + .current_dir(path) + .output() + .unwrap(); + + std::fs::write(dir.path().join("README.md"), "# Test").unwrap(); + Command::new("git").args(["add", "."]).current_dir(path).output().unwrap(); + Command::new("git") + .args(["commit", "-m", "init"]) + .current_dir(path) + .output() + .unwrap(); + + dir + } + + #[test] + fn test_create_worktree() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + let (wt_path, branch) = create_worktree(path, "main", 42).unwrap(); + assert!(wt_path.contains("orchai-42")); + assert_eq!(branch, "orchai/42"); + assert!(Path::new(&wt_path).exists()); + } + + #[test] + fn test_get_diff_empty() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + let (_, branch) = create_worktree(path, "main", 1).unwrap(); + let diff = get_diff(path, "main", &branch).unwrap(); + assert!(diff.is_empty(), "No changes yet, diff should be empty"); + } + + #[test] + fn test_get_diff_with_changes() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + let (wt_path, branch) = create_worktree(path, "main", 2).unwrap(); + + std::fs::write(Path::new(&wt_path).join("fix.txt"), "fixed").unwrap(); + Command::new("git").args(["add", "."]).current_dir(&wt_path).output().unwrap(); + Command::new("git") + .args(["commit", "-m", "fix"]) + .current_dir(&wt_path) + .output() + .unwrap(); + + let diff = get_diff(path, "main", &branch).unwrap(); + assert!(diff.contains("fix.txt")); + assert!(diff.contains("+fixed")); + } + + #[test] + fn test_list_commits() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + let (wt_path, branch) = create_worktree(path, "main", 3).unwrap(); + + std::fs::write(Path::new(&wt_path).join("a.txt"), "a").unwrap(); + Command::new("git").args(["add", "."]).current_dir(&wt_path).output().unwrap(); + Command::new("git") + .args(["commit", "-m", "first"]) + .current_dir(&wt_path) + .output() + .unwrap(); + + std::fs::write(Path::new(&wt_path).join("b.txt"), "b").unwrap(); + Command::new("git").args(["add", "."]).current_dir(&wt_path).output().unwrap(); + Command::new("git") + .args(["commit", "-m", "second"]) + .current_dir(&wt_path) + .output() + .unwrap(); + + let commits = list_commits(path, "main", &branch).unwrap(); + assert_eq!(commits.len(), 2); + } + + #[test] + fn test_list_local_branches() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + create_worktree(path, "main", 10).unwrap(); + let branches = list_local_branches(path).unwrap(); + assert!(branches.contains(&"main".to_string())); + assert!(branches.contains(&"orchai/10".to_string())); + } + + #[test] + fn test_delete_worktree() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + let (wt_path, branch) = create_worktree(path, "main", 99).unwrap(); + assert!(Path::new(&wt_path).exists()); + + delete_worktree(path, &wt_path, &branch).unwrap(); + assert!(!Path::new(&wt_path).exists()); + + let branches = list_local_branches(path).unwrap(); + assert!(!branches.contains(&"orchai/99".to_string())); + } + + #[test] + fn test_apply_fix() { + let dir = setup_test_repo(); + let path = dir.path().to_str().unwrap(); + + Command::new("git") + .args(["branch", "feature/test"]) + .current_dir(path) + .output() + .unwrap(); + + let (wt_path, branch) = create_worktree(path, "main", 7).unwrap(); + std::fs::write(Path::new(&wt_path).join("fix.txt"), "the fix").unwrap(); + Command::new("git").args(["add", "."]).current_dir(&wt_path).output().unwrap(); + Command::new("git") + .args(["commit", "-m", "apply fix"]) + .current_dir(&wt_path) + .output() + .unwrap(); + + apply_fix(path, "main", &branch, "feature/test").unwrap(); + + Command::new("git") + .args(["checkout", "feature/test"]) + .current_dir(path) + .output() + .unwrap(); + assert!(Path::new(path).join("fix.txt").exists()); + + Command::new("git") + .args(["checkout", "main"]) + .current_dir(path) + .output() + .unwrap(); + } +} diff --git a/src/App.tsx b/src/App.tsx index d1f0cfb..a6c3698 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,8 @@ import AppLayout from "./components/layout/AppLayout"; import ProjectForm from "./components/projects/ProjectForm"; import ProjectDashboard from "./components/projects/ProjectDashboard"; import SettingsPage from "./components/settings/SettingsPage"; +import TicketDetail from "./components/tickets/TicketDetail"; +import TicketList from "./components/tickets/TicketList"; import TrackerConfig from "./components/trackers/TrackerConfig"; function EmptyState() { @@ -21,8 +23,10 @@ function App() { } /> } /> } /> + } /> } /> } /> + } /> } /> } /> diff --git a/src/components/projects/ProjectDashboard.tsx b/src/components/projects/ProjectDashboard.tsx index 8e0b089..cfff89f 100644 --- a/src/components/projects/ProjectDashboard.tsx +++ b/src/components/projects/ProjectDashboard.tsx @@ -102,14 +102,25 @@ export default function ProjectDashboard() {
-

Recent Tickets

+
+

Recent Tickets

+ {tickets.length > 0 && ( + + View all ({tickets.length}) + + )} +
{recentTickets.length === 0 ? (
No tickets processed yet.
) : ( -
+
{recentTickets.map((ticket) => ( -
@@ -123,7 +134,7 @@ export default function ProjectDashboard() { > {ticket.status} -
+ ))}
)} diff --git a/src/components/tickets/TicketDetail.tsx b/src/components/tickets/TicketDetail.tsx new file mode 100644 index 0000000..1fd5ac0 --- /dev/null +++ b/src/components/tickets/TicketDetail.tsx @@ -0,0 +1,323 @@ +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { + applyFixToBranch, + cancelTicket, + deleteWorktreeCmd, + getTicketResult, + getWorktreeDiff, + retryTicket, +} from "../../lib/api"; +import type { ProcessedTicket, Worktree } from "../../lib/types"; + +function statusBadgeClass(status: string): string { + switch (status) { + case "Pending": + return "bg-yellow-100 text-yellow-700"; + case "Analyzing": + return "bg-blue-100 text-blue-700"; + case "Developing": + return "bg-purple-100 text-purple-700"; + case "Done": + return "bg-green-100 text-green-700"; + case "Error": + return "bg-red-100 text-red-700"; + case "Cancelled": + return "bg-gray-100 text-gray-500"; + default: + return "bg-gray-100 text-gray-700"; + } +} + +function DiffViewer({ diff }: { diff: string }) { + if (!diff) { + return
No changes detected.
; + } + + const lines = diff.split("\n"); + return ( +
+      {lines.map((line, i) => {
+        let cls = "";
+        if (line.startsWith("+++") || line.startsWith("---")) cls = "text-gray-400";
+        else if (line.startsWith("+")) cls = "bg-green-900/20 text-green-400";
+        else if (line.startsWith("-")) cls = "bg-red-900/20 text-red-400";
+        else if (line.startsWith("@@")) cls = "text-blue-400";
+        else if (line.startsWith("diff ")) cls = "font-bold text-yellow-400";
+        return (
+          
+ {line} +
+ ); + })} +
+ ); +} + +export default function TicketDetail() { + const { ticketId } = useParams(); + const navigate = useNavigate(); + const [ticket, setTicket] = useState(null); + const [worktree, setWorktree] = useState(null); + const [diff, setDiff] = useState(null); + const [targetBranch, setTargetBranch] = useState(""); + const [tab, setTab] = useState<"info" | "analyst" | "developer" | "diff">("info"); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function loadData() { + if (!ticketId) return; + try { + const result = await getTicketResult(ticketId); + setTicket(result.ticket); + setWorktree(result.worktree); + + if (result.ticket.developer_report) setTab("developer"); + else if (result.ticket.analyst_report) setTab("analyst"); + + if (result.worktree && result.worktree.status === "Active") { + try { + const d = await getWorktreeDiff(result.worktree.id); + setDiff(d); + } catch { + setDiff(null); + } + } else { + setDiff(null); + } + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + } + + useEffect(() => { + loadData(); + }, [ticketId]); + + async function handleRetry() { + if (!ticketId) return; + setLoading(true); + try { + await retryTicket(ticketId); + await loadData(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + setLoading(false); + } + + async function handleCancel() { + if (!ticketId) return; + setLoading(true); + try { + await cancelTicket(ticketId); + await loadData(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + setLoading(false); + } + + async function handleApplyFix() { + if (!worktree || !targetBranch) return; + setLoading(true); + setError(""); + try { + await applyFixToBranch(worktree.id, targetBranch); + await loadData(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + setLoading(false); + } + + async function handleDeleteWorktree() { + if (!worktree) return; + if (!window.confirm("Delete this worktree and its branch?")) return; + setLoading(true); + try { + await deleteWorktreeCmd(worktree.id); + setWorktree(null); + setDiff(null); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } + setLoading(false); + } + + if (!ticket) { + return
Loading...
; + } + + const tabs = [ + { key: "info" as const, label: "Info" }, + { + key: "analyst" as const, + label: "Analyst Report", + disabled: !ticket.analyst_report, + }, + { + key: "developer" as const, + label: "Developer Report", + disabled: !ticket.developer_report, + }, + { key: "diff" as const, label: "Diff", disabled: !diff && !worktree }, + ]; + + return ( +
+
+
+ +

+ #{ticket.artifact_id} + {ticket.artifact_title} + + {ticket.status} + +

+
+
+ {(ticket.status === "Error" || ticket.status === "Done" || ticket.status === "Cancelled") && ( + + )} + {(ticket.status === "Pending" || + ticket.status === "Analyzing" || + ticket.status === "Developing") && ( + + )} +
+
+ + {error && ( +
{error}
+ )} + +
+ {tabs.map((t) => ( + + ))} +
+ + {tab === "info" && ( +
+
+
+ Status: + {ticket.status} +
+
+ Detected: + {new Date(ticket.detected_at).toLocaleString()} +
+ {ticket.processed_at && ( +
+ Processed: + {new Date(ticket.processed_at).toLocaleString()} +
+ )} + {worktree && ( +
+ Worktree: + {worktree.branch_name} + + {worktree.status} + +
+ )} +
+ + {worktree && worktree.status === "Active" && ( +
+

Worktree Actions

+
+ setTargetBranch(e.target.value)} + className="flex-1 rounded border border-gray-300 px-3 py-1.5 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500" + /> + +
+ +
+ )} + + {worktree && worktree.status === "Merged" && ( +
+ Fix applied to branch: {worktree.merged_into} +
+ )} +
+ )} + + {tab === "analyst" && ticket.analyst_report && ( +
+ {ticket.analyst_report} +
+ )} + + {tab === "developer" && ticket.developer_report && ( +
+ {ticket.developer_report} +
+ )} + + {tab === "diff" && } +
+ ); +} diff --git a/src/components/tickets/TicketList.tsx b/src/components/tickets/TicketList.tsx new file mode 100644 index 0000000..27e0c3f --- /dev/null +++ b/src/components/tickets/TicketList.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react"; +import { Link, useParams } from "react-router-dom"; +import { getProject, listProcessedTickets } from "../../lib/api"; +import type { ProcessedTicket, Project } from "../../lib/types"; + +function statusBadgeClass(status: string): string { + switch (status) { + case "Pending": + return "bg-yellow-100 text-yellow-700"; + case "Analyzing": + return "bg-blue-100 text-blue-700"; + case "Developing": + return "bg-purple-100 text-purple-700"; + case "Done": + return "bg-green-100 text-green-700"; + case "Error": + return "bg-red-100 text-red-700"; + case "Cancelled": + return "bg-gray-100 text-gray-500"; + default: + return "bg-gray-100 text-gray-700"; + } +} + +export default function TicketList() { + const { projectId } = useParams(); + const [project, setProject] = useState(null); + const [tickets, setTickets] = useState([]); + const [filter, setFilter] = useState("all"); + + useEffect(() => { + if (!projectId) return; + Promise.all([getProject(projectId), listProcessedTickets(projectId)]).then( + ([proj, tkts]) => { + setProject(proj); + setTickets(tkts); + } + ); + }, [projectId]); + + const filtered = filter === "all" ? tickets : tickets.filter((t) => t.status === filter); + + return ( +
+
+
+ + {project?.name} + +

Processed Tickets

+
+
+ +
+ {["all", "Pending", "Analyzing", "Developing", "Done", "Error"].map((s) => ( + + ))} +
+ + {filtered.length === 0 ? ( +
No tickets found.
+ ) : ( +
+ {filtered.map((ticket) => ( + +
+
+
+ #{ticket.artifact_id} + {ticket.artifact_title} +
+
+ {new Date(ticket.detected_at).toLocaleString()} + {ticket.processed_at && ( + + Processed: {new Date(ticket.processed_at).toLocaleString()} + + )} +
+
+ + {ticket.status} + +
+ + ))} +
+ )} +
+ ); +} diff --git a/src/lib/api.ts b/src/lib/api.ts index ca6e5e0..414a1c6 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -7,6 +7,8 @@ import type { WatchedTracker, TrackerField, ProcessedTicket, + Worktree, + TicketResult, } from "./types"; export async function createProject( @@ -82,3 +84,31 @@ export async function manualPoll(trackerId: string): Promise export async function getQueueStatus(projectId: string): Promise { return invoke("get_queue_status", { projectId }); } + +// Orchestrator +export async function getTicketResult(ticketId: string): Promise { + return invoke("get_ticket_result", { ticketId }); +} +export async function retryTicket(ticketId: string): Promise { + return invoke("retry_ticket", { ticketId }); +} +export async function cancelTicket(ticketId: string): Promise { + return invoke("cancel_ticket", { ticketId }); +} + +// Worktrees +export async function listWorktrees(projectId: string): Promise { + return invoke("list_worktrees", { projectId }); +} +export async function getWorktreeDiff(worktreeId: string): Promise { + return invoke("get_worktree_diff", { worktreeId }); +} +export async function applyFixToBranch(worktreeId: string, targetBranch: string): Promise { + return invoke("apply_fix_to_branch", { worktreeId, targetBranch }); +} +export async function deleteWorktreeCmd(worktreeId: string): Promise { + return invoke("delete_worktree_cmd", { worktreeId }); +} +export async function listLocalBranches(projectId: string): Promise { + return invoke("list_local_branches", { projectId }); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 85ebc7d..238f630 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -69,3 +69,19 @@ export interface ProcessedTicket { detected_at: string; processed_at: string | null; } + +export interface Worktree { + id: string; + ticket_id: string; + path: string; + branch_name: string; + status: string; + created_at: string; + merged_at: string | null; + merged_into: string | null; +} + +export interface TicketResult { + ticket: ProcessedTicket; + worktree: Worktree | null; +}