feat: implement full context-scoped AI chat UI in AIChatPanel
- Added AIChatPanel component with context header, user and AI message handling. - Integrated streaming responses via IPC and error handling for chat mutations. - Enhanced user experience with input handling and auto-scrolling features. - Updated AppShell to derive AI chat context from the current route. - Introduced ScrollArea component for better scrolling behavior in various dialogs. - Added support for Tailwind typography and improved global styles. - Updated project and task dialogs to utilize ScrollArea for better UX.
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
## Your Task
|
## Your Task US-020
|
||||||
|
|
||||||
1. Read the full app PRD at `prd-main.md` (in the same directory as this file)
|
1. Read the full app PRD at `prd-main.md` (in the same directory as this file)
|
||||||
2. Read the PRD at `prd.json` (in the same directory as this file)
|
2. Read the PRD at `prd.json` (in the same directory as this file)
|
||||||
@@ -23,19 +23,22 @@ APPEND to progress.txt (never replace, always append):
|
|||||||
|
|
||||||
## USER REQUEST
|
## USER REQUEST
|
||||||
{
|
{
|
||||||
"id": "US-018",
|
"id": "US-020",
|
||||||
"title": "GitHub Copilot SDK setup and keytar token storage",
|
"title": "Context-scoped AI chat UI",
|
||||||
"description": "As a developer, I need the GitHub Copilot SDK initialized in the main process with secure OS keychain token storage so that AI features can authenticate.",
|
"description": "As a user, I want the AI chat (revealed by the Fluid Curtain) to display a context header, support message input, and stream AI responses.",
|
||||||
"acceptanceCriteria": [
|
"acceptanceCriteria": [
|
||||||
"keytar installed and imported in main process only (not renderer)",
|
"Chat panel shows a context header using shadcn/ui Badge (variant=outline): 'Chatting about: [Project Name]' when opened from a project detail view, or 'Global workspace' when opened from other sections",
|
||||||
"ai.setToken tRPC mutation accepts { token: string } and stores it via keytar.setPassword('adiuva', 'copilot-token', token)",
|
"Chat input box uses shadcn/ui Textarea: white background, border #d4d4d4, shadow-lg, min-height 109px, placeholder 'Ask me anything...'; Send uses shadcn/ui Button (black bg, Send Lucide icon + 'Send' label) anchored bottom-right",
|
||||||
"ai.hasToken tRPC query returns a boolean indicating whether a token is stored",
|
"User messages appear as right-aligned message bubbles using shadcn/ui Card; AI responses as left-aligned Cards",
|
||||||
"On app start, main process reads the token from keychain and initializes the GitHub Copilot SDK client",
|
"Streaming: AI response tokens appended to the current AI bubble as they arrive from ai.chat",
|
||||||
"Settings dialog uses shadcn/ui Dialog (DialogTrigger as a SidebarMenuButton with Settings/gear icon in the sidebar footer); dialog content uses shadcn/ui Input for token paste + shadcn/ui Button to save via ai.setToken",
|
"A loading spinner or pulsing indicator (shadcn/ui Skeleton) shown while waiting for first token",
|
||||||
"If no token is stored, AI-dependent features display a prompt using shadcn/ui Card with a shadcn/ui Button linking to the Settings dialog instead of throwing an error",
|
"If ai.chat returns { error }, display the error message in a shadcn/ui Card with destructive border styling",
|
||||||
"Typecheck passes"
|
"Chat history is session-only — cleared when the curtain closes or the app restarts",
|
||||||
|
"Install shadcn/ui components via 'npx shadcn@latest add textarea' before implementing (card, badge, button, skeleton already installed)",
|
||||||
|
"Typecheck passes",
|
||||||
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 18,
|
"priority": 20,
|
||||||
"passes": false,
|
"passes": false,
|
||||||
"notes": ""
|
"notes": ""
|
||||||
}
|
}
|
||||||
415
package-lock.json
generated
415
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"@langchain/openai": "^1.2.9",
|
"@langchain/openai": "^1.2.9",
|
||||||
"@milkdown/crepe": "^7.18.0",
|
"@milkdown/crepe": "^7.18.0",
|
||||||
"@milkdown/kit": "^7.18.0",
|
"@milkdown/kit": "^7.18.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.2.0",
|
"@tailwindcss/vite": "^4.2.0",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-router": "^1.161.1",
|
"@tanstack/react-router": "^1.161.1",
|
||||||
@@ -38,6 +39,8 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
@@ -7147,6 +7150,18 @@
|
|||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||||
|
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/vite": {
|
"node_modules/@tailwindcss/vite": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.0.tgz",
|
||||||
@@ -7669,6 +7684,15 @@
|
|||||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
"license": "MIT"
|
"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/fs-extra": {
|
"node_modules/@types/fs-extra": {
|
||||||
"version": "9.0.13",
|
"version": "9.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
|
||||||
@@ -7779,7 +7803,6 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -8069,7 +8092,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
|
||||||
@@ -9681,6 +9703,36 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"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/chardet": {
|
"node_modules/chardet": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||||
@@ -9970,6 +10022,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/commander": {
|
"node_modules/commander": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||||
@@ -10238,7 +10300,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"cssesc": "bin/cssesc"
|
"cssesc": "bin/cssesc"
|
||||||
@@ -12707,6 +12768,16 @@
|
|||||||
"node": ">=4.0"
|
"node": ">=4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
@@ -14030,6 +14101,46 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/headers-polyfill": {
|
"node_modules/headers-polyfill": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
|
||||||
@@ -14054,6 +14165,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"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/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
@@ -14261,6 +14382,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/internal-slot": {
|
"node_modules/internal-slot": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||||
@@ -14306,6 +14433,30 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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-array-buffer": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
||||||
@@ -14471,6 +14622,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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-docker": {
|
"node_modules/is-docker": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
|
||||||
@@ -14559,6 +14720,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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-in-ssh": {
|
"node_modules/is-in-ssh": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz",
|
||||||
@@ -15974,6 +16145,66 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/mdast-util-phrasing": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||||
@@ -15988,6 +16219,27 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/mdast-util-to-markdown": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
||||||
@@ -17844,6 +18096,31 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
|
||||||
@@ -18144,6 +18421,19 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postject": {
|
"node_modules/postject": {
|
||||||
"version": "1.0.0-alpha.6",
|
"version": "1.0.0-alpha.6",
|
||||||
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
||||||
@@ -18316,6 +18606,16 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/prosemirror-changeset": {
|
"node_modules/prosemirror-changeset": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
||||||
@@ -18864,6 +19164,33 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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-redux": {
|
"node_modules/react-redux": {
|
||||||
"version": "9.2.0",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
@@ -19297,6 +19624,23 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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": {
|
"node_modules/remark-stringify": {
|
||||||
"version": "11.0.0",
|
"version": "11.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
||||||
@@ -20731,6 +21075,16 @@
|
|||||||
"source-map": "^0.6.0"
|
"source-map": "^0.6.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/spdx-correct": {
|
"node_modules/spdx-correct": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||||
@@ -20957,6 +21311,20 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/stringify-object": {
|
"node_modules/stringify-object": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-5.0.0.tgz",
|
||||||
@@ -21073,6 +21441,24 @@
|
|||||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/sumchecker": {
|
"node_modules/sumchecker": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||||
@@ -21505,6 +21891,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"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/trim-repeated": {
|
"node_modules/trim-repeated": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz",
|
||||||
@@ -22321,6 +22717,19 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"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-remove-position": {
|
"node_modules/unist-util-remove-position": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
"@langchain/openai": "^1.2.9",
|
"@langchain/openai": "^1.2.9",
|
||||||
"@milkdown/crepe": "^7.18.0",
|
"@milkdown/crepe": "^7.18.0",
|
||||||
"@milkdown/kit": "^7.18.0",
|
"@milkdown/kit": "^7.18.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.2.0",
|
"@tailwindcss/vite": "^4.2.0",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-router": "^1.161.1",
|
"@tanstack/react-router": "^1.161.1",
|
||||||
@@ -74,6 +75,8 @@
|
|||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-day-picker": "^9.13.2",
|
"react-day-picker": "^9.13.2",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"react-markdown": "^10.1.0",
|
||||||
|
"remark-gfm": "^4.0.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
|
|||||||
4
prd.json
4
prd.json
@@ -370,8 +370,8 @@
|
|||||||
"Verify in browser using dev-browser skill"
|
"Verify in browser using dev-browser skill"
|
||||||
],
|
],
|
||||||
"priority": 20,
|
"priority": 20,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": ""
|
"notes": "Completed: Full chat UI in AIChatPanel with context header Badge, user Card bubbles, AI plain text (Sparkles+Adiuva header), streaming via IPC, Skeleton loading, error Cards, session-only history cleared on curtain close"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "US-021",
|
"id": "US-021",
|
||||||
|
|||||||
26
progress.txt
26
progress.txt
@@ -416,3 +416,29 @@
|
|||||||
- The graph is compiled once via `buildGraph()` singleton — no per-request overhead for graph construction
|
- The graph is compiled once via `buildGraph()` singleton — no per-request overhead for graph construction
|
||||||
- Architecture: agent logic (LangGraph) is now fully decoupled from the LLM provider. Adding a new provider only requires a new factory function in `llm.ts`
|
- Architecture: agent logic (LangGraph) is now fully decoupled from the LLM provider. Adding a new provider only requires a new factory function in `llm.ts`
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-02-23 - US-020
|
||||||
|
- What was implemented:
|
||||||
|
- Full context-scoped AI chat UI in `AIChatPanel` component, replacing the "coming soon" placeholder
|
||||||
|
- Two-mode layout: empty state (centered input) and chat state (messages + pinned bottom input)
|
||||||
|
- Context header: `Badge` (variant=outline) showing "Chatting about: [Project Name]" or "Global workspace"
|
||||||
|
- Context derived in AppShell from `currentPath` + `searchObj['projectId']`; project name fetched via `trpc.projects.get` query
|
||||||
|
- User messages: right-aligned `Card` components
|
||||||
|
- AI messages: left-aligned plain text (no Card) with `Sparkles` icon + bold "Adiuva" header line
|
||||||
|
- Streaming: subscribes to `window.electronAI.onStreamChunk` IPC channel before firing `trpc.ai.chat.mutate()`; tokens accumulate in `streamingContent` state via `useRef` pattern
|
||||||
|
- Loading indicator: `Skeleton` lines (w-48 + w-32) shown below Adiuva header while waiting for first token
|
||||||
|
- Error handling: mutation errors and `{ error }` responses display in `Card` with `border-destructive` styling
|
||||||
|
- Session-only history: `useEffect` on `curtainOpen` prop clears all messages, input, and streaming state when curtain closes
|
||||||
|
- Scroll behavior: after user sends, scrolls user message to top of visible area; does NOT auto-scroll during AI streaming
|
||||||
|
- Input: `Textarea` matching Figma (white bg, border #d4d4d4, shadow-lg, min-h 109px, "Ask me anything..."), Send `Button` (default variant, Send icon + label) absolute bottom-right
|
||||||
|
- Enter sends, Shift+Enter for newline
|
||||||
|
- Extracted `ChatInput` sub-component for reuse between empty and chat states
|
||||||
|
- Typecheck passes (zero errors), no new lint errors introduced
|
||||||
|
- Files changed: `src/renderer/components/ai/AIChatPanel.tsx`, `src/renderer/components/layout/AppShell.tsx`, `prd.json`, `progress.txt`
|
||||||
|
- **Learnings for future iterations:**
|
||||||
|
- `window.electronAI.onStreamChunk` returns an unsubscribe function — subscribe before firing the mutation, unsubscribe in error/completion handlers
|
||||||
|
- Use `useRef` for accumulating streaming content (`streamingContentRef.current += token`) to avoid stale closure issues in the stream callback, then sync to state with `setStreamingContent(streamingContentRef.current)`
|
||||||
|
- `trpc.ai.chat.mutate()` returns `{ response, error? }` — the `error` field is `string | undefined`, so must narrow before passing to typed state (assign to a `const` first)
|
||||||
|
- `trpc.projects.get` query with `enabled: !!projectId` + `id: projectId ?? ''` avoids both the non-null assertion lint warning and unnecessary queries
|
||||||
|
- For scroll-to-user-message UX: track the last user message with a ref and use `scrollIntoView({ behavior: 'smooth', block: 'start' })` — do NOT auto-scroll on AI streaming to let the user read from the top
|
||||||
|
---
|
||||||
|
|||||||
@@ -3,31 +3,43 @@
|
|||||||
*
|
*
|
||||||
* Wraps the CopilotClient's session API so it can be used as a drop-in
|
* Wraps the CopilotClient's session API so it can be used as a drop-in
|
||||||
* BaseChatModel within LangGraph, making the orchestrator provider-agnostic.
|
* BaseChatModel within LangGraph, making the orchestrator provider-agnostic.
|
||||||
|
*
|
||||||
|
* Accepts a client-getter function to avoid module duplication issues when
|
||||||
|
* this file is code-split into a separate chunk by Vite.
|
||||||
*/
|
*/
|
||||||
import { SimpleChatModel, type BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
|
import { SimpleChatModel, type BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
|
||||||
import type { BaseMessage } from '@langchain/core/messages';
|
import type { BaseMessage } from '@langchain/core/messages';
|
||||||
import { AIMessageChunk } from '@langchain/core/messages';
|
import { AIMessageChunk } from '@langchain/core/messages';
|
||||||
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
import { ChatGenerationChunk } from '@langchain/core/outputs';
|
||||||
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
|
||||||
import { getCopilotClient } from './copilot';
|
|
||||||
|
type CopilotClientType = import('@github/copilot-sdk').CopilotClient;
|
||||||
|
|
||||||
const COPILOT_TIMEOUT = 60_000;
|
const COPILOT_TIMEOUT = 60_000;
|
||||||
|
|
||||||
export class ChatCopilot extends SimpleChatModel<BaseChatModelCallOptions> {
|
export class ChatCopilot extends SimpleChatModel<BaseChatModelCallOptions> {
|
||||||
constructor() {
|
private getClient: () => CopilotClientType | null;
|
||||||
|
|
||||||
|
constructor(getClient: () => CopilotClientType | null) {
|
||||||
super({});
|
super({});
|
||||||
|
this.getClient = getClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
_llmType(): string {
|
_llmType(): string {
|
||||||
return 'copilot';
|
return 'copilot';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private requireClient(): CopilotClientType {
|
||||||
|
const client = this.getClient();
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('CopilotClient not initialized. Please check that Copilot CLI is authenticated (copilot auth login).');
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
async _call(messages: BaseMessage[], _options: this['ParsedCallOptions'], _runManager?: CallbackManagerForLLMRun): Promise<string> {
|
async _call(messages: BaseMessage[], _options: this['ParsedCallOptions'], _runManager?: CallbackManagerForLLMRun): Promise<string> {
|
||||||
const client = getCopilotClient();
|
const client = this.requireClient();
|
||||||
if (!client) {
|
|
||||||
throw new Error('CopilotClient not initialized. Please add your GitHub token in Settings.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract system message and user prompt from LangChain messages
|
// Extract system message and user prompt from LangChain messages
|
||||||
const systemContent = messages
|
const systemContent = messages
|
||||||
@@ -61,10 +73,7 @@ export class ChatCopilot extends SimpleChatModel<BaseChatModelCallOptions> {
|
|||||||
_options: this['ParsedCallOptions'],
|
_options: this['ParsedCallOptions'],
|
||||||
_runManager?: CallbackManagerForLLMRun,
|
_runManager?: CallbackManagerForLLMRun,
|
||||||
): AsyncGenerator<ChatGenerationChunk> {
|
): AsyncGenerator<ChatGenerationChunk> {
|
||||||
const client = getCopilotClient();
|
const client = this.requireClient();
|
||||||
if (!client) {
|
|
||||||
throw new Error('CopilotClient not initialized. Please add your GitHub token in Settings.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemContent = messages
|
const systemContent = messages
|
||||||
.filter((m) => m._getType() === 'system')
|
.filter((m) => m._getType() === 'system')
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { registerProvider, type AIProvider } from './provider';
|
|||||||
type CopilotClientType = import('@github/copilot-sdk').CopilotClient;
|
type CopilotClientType = import('@github/copilot-sdk').CopilotClient;
|
||||||
|
|
||||||
let client: CopilotClientType | null = null;
|
let client: CopilotClientType | null = null;
|
||||||
let token: string | null = null;
|
let isReady = false;
|
||||||
|
|
||||||
const copilotProvider: AIProvider = {
|
const copilotProvider: AIProvider = {
|
||||||
name: 'copilot',
|
name: 'copilot',
|
||||||
displayName: 'GitHub Copilot',
|
displayName: 'GitHub Copilot',
|
||||||
|
usesExternalAuth: true,
|
||||||
|
|
||||||
async initialize(t: string): Promise<boolean> {
|
async initialize(): Promise<boolean> {
|
||||||
token = t;
|
|
||||||
try {
|
try {
|
||||||
// Stop existing client if re-initializing
|
// Stop existing client if re-initializing
|
||||||
if (client) {
|
if (client) {
|
||||||
@@ -22,32 +22,30 @@ const copilotProvider: AIProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { CopilotClient } = await import('@github/copilot-sdk');
|
const { CopilotClient } = await import('@github/copilot-sdk');
|
||||||
|
// No githubToken — uses stored OAuth credentials from Copilot CLI
|
||||||
|
// (authenticate first with `copilot auth login`)
|
||||||
client = new CopilotClient({
|
client = new CopilotClient({
|
||||||
githubToken: t,
|
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
autoRestart: true,
|
autoRestart: true,
|
||||||
logLevel: 'warning',
|
logLevel: 'warning',
|
||||||
});
|
});
|
||||||
await client.start();
|
await client.start();
|
||||||
console.log('[AI] CopilotClient started successfully');
|
isReady = true;
|
||||||
|
console.log('[AI] CopilotClient started (using CLI OAuth credentials)');
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AI] Failed to start CopilotClient:', err);
|
console.error('[AI] Failed to start CopilotClient:', err);
|
||||||
client = null;
|
client = null;
|
||||||
|
isReady = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
isReady(): boolean {
|
isReady(): boolean {
|
||||||
return client !== null && token !== null;
|
return isReady && client !== null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get the raw Copilot token (used by future chat/completion calls). */
|
|
||||||
export function getCopilotToken(): string | null {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get the CopilotClient instance (null if not initialized). */
|
/** Get the CopilotClient instance (null if not initialized). */
|
||||||
export function getCopilotClient(): CopilotClientType | null {
|
export function getCopilotClient(): CopilotClientType | null {
|
||||||
return client;
|
return client;
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
* the only place that knows how to create provider-specific LLM instances.
|
* the only place that knows how to create provider-specific LLM instances.
|
||||||
*/
|
*/
|
||||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import { getActiveProviderName } from './provider';
|
import { getActiveProviderName, getActiveProvider } from './provider';
|
||||||
import { getToken } from './token';
|
import { getToken } from './token';
|
||||||
|
import { getCopilotClient } from './copilot';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Provider-specific factory functions (lazy-loaded)
|
// Provider-specific factory functions (lazy-loaded)
|
||||||
@@ -36,8 +37,10 @@ async function createAnthropicModel(token: string): Promise<BaseChatModel> {
|
|||||||
async function createCopilotModel(_token: string): Promise<BaseChatModel> {
|
async function createCopilotModel(_token: string): Promise<BaseChatModel> {
|
||||||
// GitHub Copilot uses the Copilot SDK subprocess for auth and API access.
|
// GitHub Copilot uses the Copilot SDK subprocess for auth and API access.
|
||||||
// We wrap it in a LangChain-compatible adapter.
|
// We wrap it in a LangChain-compatible adapter.
|
||||||
|
// Pass getCopilotClient from this chunk (same as copilot.ts) to avoid
|
||||||
|
// module duplication when chat-copilot.ts is code-split by Vite.
|
||||||
const { ChatCopilot } = await import('./chat-copilot');
|
const { ChatCopilot } = await import('./chat-copilot');
|
||||||
return new ChatCopilot();
|
return new ChatCopilot(getCopilotClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -62,14 +65,15 @@ export async function getLLM(): Promise<BaseChatModel | null> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await getToken(providerName);
|
const provider = getActiveProvider();
|
||||||
if (!token) {
|
const token = provider?.usesExternalAuth ? '' : await getToken(providerName);
|
||||||
|
if (!provider?.usesExternalAuth && !token) {
|
||||||
console.log(`[AI] No token available for provider "${providerName}"`);
|
console.log(`[AI] No token available for provider "${providerName}"`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await factory(token);
|
return await factory(token ?? '');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`[AI] Failed to create LLM for "${providerName}":`, err);
|
console.error(`[AI] Failed to create LLM for "${providerName}":`, err);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
|
import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
|
||||||
import { SystemMessage, HumanMessage, type BaseMessage } from '@langchain/core/messages';
|
import { SystemMessage, HumanMessage, type BaseMessage } from '@langchain/core/messages';
|
||||||
import { z } from 'zod';
|
|
||||||
import { eq, asc } from 'drizzle-orm';
|
import { eq, asc } from 'drizzle-orm';
|
||||||
import { getDb } from '../db';
|
import { getDb } from '../db';
|
||||||
import { projects, tasks, checkpoints, notes, clients } from '../db/schema';
|
import { projects, tasks, checkpoints, notes, clients } from '../db/schema';
|
||||||
@@ -178,12 +177,6 @@ If the user asks about specific note contents that aren't included here, let the
|
|||||||
// LangGraph State
|
// LangGraph State
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const RouteSchema = z.object({
|
|
||||||
route: z.enum(['project', 'knowledge', 'general']).describe(
|
|
||||||
'Which specialist agent should handle this request',
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const OrchestratorState = Annotation.Root({
|
const OrchestratorState = Annotation.Root({
|
||||||
/** The user's original message */
|
/** The user's original message */
|
||||||
userMessage: Annotation<string>(),
|
userMessage: Annotation<string>(),
|
||||||
@@ -207,25 +200,29 @@ type State = typeof OrchestratorState.State;
|
|||||||
// Graph nodes
|
// Graph nodes
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/** Node 1: Classify intent using structured output */
|
/** Node 1: Classify intent using plain-text extraction (works with all providers) */
|
||||||
async function classifyIntent(state: State): Promise<Partial<State>> {
|
async function classifyIntent(state: State): Promise<Partial<State>> {
|
||||||
const llm = await getLLM();
|
const llm = await getLLM();
|
||||||
if (!llm) throw new Error('AI provider not configured. Please add your token in Settings.');
|
if (!llm) throw new Error('AI provider not configured. Please add your token in Settings.');
|
||||||
|
|
||||||
const routerLLM = llm.withStructuredOutput(RouteSchema);
|
const response = await llm.invoke([
|
||||||
|
|
||||||
const result = await routerLLM.invoke([
|
|
||||||
new SystemMessage(
|
new SystemMessage(
|
||||||
`You are a routing classifier for Adiuva, a project management workspace.
|
`You are a routing classifier for Adiuva, a project management workspace.
|
||||||
Classify the user's message into one of these categories:
|
Classify the user's message into exactly one category. Reply with ONLY the category name, nothing else.
|
||||||
- "project": Question about a specific project (tasks, notes, checkpoints, progress, summaries)
|
|
||||||
- "knowledge": Cross-project or historical question (e.g., "what did we decide about X?", "find notes about Y")
|
Categories:
|
||||||
- "general": Everything else (general help, scheduling, task overviews, workspace summaries)`,
|
- project: Question about a specific project (tasks, notes, checkpoints, progress, summaries)
|
||||||
|
- knowledge: Cross-project or historical question (e.g., "what did we decide about X?", "find notes about Y")
|
||||||
|
- general: Everything else (general help, scheduling, task overviews, workspace summaries)`,
|
||||||
),
|
),
|
||||||
new HumanMessage(state.userMessage),
|
new HumanMessage(state.userMessage),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { route: result.route };
|
const text = (typeof response.content === 'string' ? response.content : '').trim().toLowerCase();
|
||||||
|
const validRoutes = ['project', 'knowledge', 'general'] as const;
|
||||||
|
const route = validRoutes.find((r) => text.includes(r)) ?? 'general';
|
||||||
|
|
||||||
|
return { route };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Node 2a: Project agent — answer project-scoped questions */
|
/** Node 2a: Project agent — answer project-scoped questions */
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export interface AIProvider {
|
|||||||
initialize(token: string): Promise<boolean>;
|
initialize(token: string): Promise<boolean>;
|
||||||
/** Whether the provider is initialized and ready to handle requests. */
|
/** Whether the provider is initialized and ready to handle requests. */
|
||||||
isReady(): boolean;
|
isReady(): boolean;
|
||||||
|
/** If true, this provider uses external auth (e.g. CLI OAuth) and doesn't need a stored token. */
|
||||||
|
usesExternalAuth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providers = new Map<string, AIProvider>();
|
const providers = new Map<string, AIProvider>();
|
||||||
@@ -49,9 +51,12 @@ export async function saveTokenAndInit(token: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check whether the active provider has a stored token. */
|
/** Check whether the active provider has credentials (stored token or external auth). */
|
||||||
export async function hasActiveToken(): Promise<boolean> {
|
export async function hasActiveToken(): Promise<boolean> {
|
||||||
const name = getActiveProviderName();
|
const name = getActiveProviderName();
|
||||||
|
const provider = providers.get(name);
|
||||||
|
// Providers with external auth (e.g. Copilot CLI OAuth) don't need a stored token
|
||||||
|
if (provider?.usesExternalAuth) return true;
|
||||||
const token = await getToken(name);
|
const token = await getToken(name);
|
||||||
return token !== null && token.length > 0;
|
return token !== null && token.length > 0;
|
||||||
}
|
}
|
||||||
@@ -69,6 +74,14 @@ export async function initAI(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Providers with external auth (e.g. Copilot CLI OAuth) initialize without a stored token
|
||||||
|
if (provider.usesExternalAuth) {
|
||||||
|
const ready = await provider.initialize('');
|
||||||
|
activeProvider = provider;
|
||||||
|
console.log(`[AI] Provider "${provider.displayName}" initialized (external auth): ready=${ready}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const token = await getToken(name);
|
const token = await getToken(name);
|
||||||
if (token) {
|
if (token) {
|
||||||
const ready = await provider.initialize(token);
|
const ready = await provider.initialize(token);
|
||||||
|
|||||||
@@ -1,15 +1,154 @@
|
|||||||
import { Sparkles, KeyRound } from 'lucide-react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { Sparkles, KeyRound, ArrowUp } from 'lucide-react';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
import { trpc } from '@/lib/trpc';
|
import { trpc } from '@/lib/trpc';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
|
interface ChatMessage {
|
||||||
|
id: string;
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface AIChatPanelProps {
|
interface AIChatPanelProps {
|
||||||
onOpenSettings?: () => void;
|
onOpenSettings?: () => void;
|
||||||
|
contextType: 'global' | 'project';
|
||||||
|
projectId?: string;
|
||||||
|
projectName?: string;
|
||||||
|
curtainOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AIChatPanel({ onOpenSettings }: AIChatPanelProps) {
|
export function AIChatPanel({
|
||||||
|
onOpenSettings,
|
||||||
|
contextType,
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
curtainOpen,
|
||||||
|
}: AIChatPanelProps) {
|
||||||
const hasTokenQuery = trpc.ai.hasToken.useQuery();
|
const hasTokenQuery = trpc.ai.hasToken.useQuery();
|
||||||
|
|
||||||
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
||||||
|
const [input, setInput] = useState('');
|
||||||
|
const [isStreaming, setIsStreaming] = useState(false);
|
||||||
|
const [streamingContent, setStreamingContent] = useState('');
|
||||||
|
|
||||||
|
const messagesContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const streamingContentRef = useRef('');
|
||||||
|
const chatMutation = trpc.ai.chat.useMutation();
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
const el = messagesContainerRef.current;
|
||||||
|
if (el) el.scrollTo({ top: el.scrollHeight });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Reset input when curtain closes; scroll to bottom when it reopens
|
||||||
|
useEffect(() => {
|
||||||
|
if (!curtainOpen) {
|
||||||
|
setInput('');
|
||||||
|
} else {
|
||||||
|
setTimeout(scrollToBottom, 50);
|
||||||
|
}
|
||||||
|
}, [curtainOpen, scrollToBottom]);
|
||||||
|
|
||||||
|
// Auto-scroll when messages change or streaming content updates
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
}, [messages, streamingContent, scrollToBottom]);
|
||||||
|
|
||||||
|
const handleSend = useCallback(() => {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed || isStreaming) return;
|
||||||
|
|
||||||
|
const userMsg: ChatMessage = {
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
role: 'user',
|
||||||
|
content: trimmed,
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMsg]);
|
||||||
|
setInput('');
|
||||||
|
setIsStreaming(true);
|
||||||
|
setStreamingContent('');
|
||||||
|
streamingContentRef.current = '';
|
||||||
|
|
||||||
|
const unsubscribe = window.electronAI.onStreamChunk(({ token, done }) => {
|
||||||
|
if (done) {
|
||||||
|
const finalContent = streamingContentRef.current;
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id: crypto.randomUUID(), role: 'assistant', content: finalContent },
|
||||||
|
]);
|
||||||
|
setStreamingContent('');
|
||||||
|
streamingContentRef.current = '';
|
||||||
|
setIsStreaming(false);
|
||||||
|
unsubscribe();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
streamingContentRef.current += token;
|
||||||
|
setStreamingContent(streamingContentRef.current);
|
||||||
|
});
|
||||||
|
|
||||||
|
chatMutation.mutate(
|
||||||
|
{
|
||||||
|
message: trimmed,
|
||||||
|
context: {
|
||||||
|
type: contextType,
|
||||||
|
...(contextType === 'project' && projectId ? { projectId } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data.error) {
|
||||||
|
unsubscribe();
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id: crypto.randomUUID(), role: 'assistant', content: data.error!, error: true },
|
||||||
|
]);
|
||||||
|
setStreamingContent('');
|
||||||
|
streamingContentRef.current = '';
|
||||||
|
setIsStreaming(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
unsubscribe();
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id: crypto.randomUUID(), role: 'assistant', content: err.message || 'An unexpected error occurred.', error: true },
|
||||||
|
]);
|
||||||
|
setStreamingContent('');
|
||||||
|
streamingContentRef.current = '';
|
||||||
|
setIsStreaming(false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [input, isStreaming, contextType, projectId, chatMutation]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Smart wheel handler: only stop propagation when there's content to scroll through
|
||||||
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||||
|
const el = messagesContainerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 2;
|
||||||
|
const atTop = el.scrollTop < 2;
|
||||||
|
// Let event propagate to AppShell when at boundaries
|
||||||
|
if ((e.deltaY > 0 && atBottom) || (e.deltaY < 0 && atTop)) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// No token configured — show settings prompt
|
||||||
if (hasTokenQuery.data === false) {
|
if (hasTokenQuery.data === false) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-0 flex flex-col items-center justify-center bg-background">
|
<div className="absolute inset-0 z-0 flex flex-col items-center justify-center bg-background">
|
||||||
@@ -19,7 +158,8 @@ export function AIChatPanel({ onOpenSettings }: AIChatPanelProps) {
|
|||||||
<div className="text-center space-y-1">
|
<div className="text-center space-y-1">
|
||||||
<p className="text-sm font-medium">AI provider not configured</p>
|
<p className="text-sm font-medium">AI provider not configured</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Connect your GitHub Copilot token to enable AI-powered features like chat, summaries, and suggestions.
|
Connect your GitHub Copilot token to enable AI-powered features
|
||||||
|
like chat, summaries, and suggestions.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" onClick={onOpenSettings}>
|
<Button variant="outline" size="sm" onClick={onOpenSettings}>
|
||||||
@@ -31,12 +171,173 @@ export function AIChatPanel({ onOpenSettings }: AIChatPanelProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasMessages = messages.length > 0 || isStreaming;
|
||||||
|
|
||||||
|
const contextLabel =
|
||||||
|
contextType === 'project' && projectName
|
||||||
|
? `Chatting about: ${projectName}`
|
||||||
|
: 'Global workspace';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 z-0 flex flex-col items-center justify-center bg-background">
|
<div className="absolute inset-0 z-0 flex flex-col bg-background">
|
||||||
<Sparkles size={32} className="text-muted-foreground/40 mb-3" />
|
{/* Context header */}
|
||||||
<p className="text-sm text-muted-foreground/60 tracking-wide">
|
<div className="flex items-center gap-2 px-6 pt-4 pb-2">
|
||||||
AI Chat — coming soon
|
<Badge variant="outline">{contextLabel}</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable messages area */}
|
||||||
|
<ScrollArea
|
||||||
|
className="flex-1 min-h-0"
|
||||||
|
viewportRef={messagesContainerRef}
|
||||||
|
viewportClassName="[&>div]:!flex [&>div]:!flex-col [&>div]:!min-h-full [&>div]:!justify-end"
|
||||||
|
onWheel={handleWheel}
|
||||||
|
>
|
||||||
|
{/* Messages */}
|
||||||
|
{hasMessages && (
|
||||||
|
<div className="mx-auto w-full max-w-[1088px] px-6 pt-4 pb-44">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{messages.map((msg) => {
|
||||||
|
if (msg.role === 'user') {
|
||||||
|
return (
|
||||||
|
<div key={msg.id} className="flex justify-end">
|
||||||
|
<div className="ml-auto max-w-[75%] rounded-2xl bg-muted px-4 py-2">
|
||||||
|
<ChatMarkdown content={msg.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.error) {
|
||||||
|
return (
|
||||||
|
<div key={msg.id} className="mr-auto max-w-[75%]">
|
||||||
|
<p className="text-sm text-destructive whitespace-pre-wrap">
|
||||||
|
{msg.content}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={msg.id} className="mr-auto max-w-[75%]">
|
||||||
|
<div className="flex items-center gap-1.5 mb-1">
|
||||||
|
<Sparkles size={16} className="text-foreground" />
|
||||||
|
<span className="text-sm font-semibold">Adiuva</span>
|
||||||
|
</div>
|
||||||
|
<div className="pl-[22px]">
|
||||||
|
<ChatMarkdown content={msg.content} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Streaming AI response */}
|
||||||
|
{isStreaming && (
|
||||||
|
<div className="mr-auto max-w-[75%]">
|
||||||
|
<div className="flex items-center gap-1.5 mb-1">
|
||||||
|
<Sparkles size={16} className="text-foreground" />
|
||||||
|
<span className="text-sm font-semibold">Adiuva</span>
|
||||||
|
</div>
|
||||||
|
{streamingContent ? (
|
||||||
|
<div className="pl-[22px]">
|
||||||
|
<ChatMarkdown content={streamingContent} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 pl-[22px]">
|
||||||
|
<Skeleton className="h-4 w-48" />
|
||||||
|
<Skeleton className="h-4 w-32" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Fixed input — pinned to the bottom */}
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 z-10 px-6 pb-4 pt-12 pointer-events-none">
|
||||||
|
<div className="absolute inset-x-0 top-0 h-full bg-gradient-to-b from-transparent via-background/80 to-background" />
|
||||||
|
<div className="relative pointer-events-auto mx-auto max-w-[1088px]">
|
||||||
|
<ChatInput
|
||||||
|
input={input}
|
||||||
|
isStreaming={isStreaming}
|
||||||
|
onInputChange={setInput}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onSend={handleSend}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- ChatInput: Floating glass card ---------- */
|
||||||
|
|
||||||
|
interface ChatInputProps {
|
||||||
|
input: string;
|
||||||
|
isStreaming: boolean;
|
||||||
|
onInputChange: (value: string) => void;
|
||||||
|
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||||
|
onSend: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatInput({
|
||||||
|
input,
|
||||||
|
isStreaming,
|
||||||
|
onInputChange,
|
||||||
|
onKeyDown,
|
||||||
|
onSend,
|
||||||
|
}: ChatInputProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative rounded-2xl bg-muted/60 backdrop-blur-xl border border-border shadow-[0_2px_20px_rgba(0,0,0,0.08)] dark:shadow-[0_2px_20px_rgba(0,0,0,0.3)] overflow-hidden">
|
||||||
|
<textarea
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => onInputChange(e.target.value)}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
placeholder="Ask me anything..."
|
||||||
|
rows={3}
|
||||||
|
className="w-full resize-none bg-transparent px-4 pt-4 pb-12 text-sm placeholder:text-muted-foreground outline-none"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-3 right-3">
|
||||||
|
<button
|
||||||
|
onClick={onSend}
|
||||||
|
disabled={!input.trim() || isStreaming}
|
||||||
|
className="flex h-8 w-8 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm transition-opacity hover:opacity-90 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<ArrowUp size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- ChatMarkdown: lightweight markdown renderer ---------- */
|
||||||
|
|
||||||
|
function ChatMarkdown({ content }: { content: string }) {
|
||||||
|
return (
|
||||||
|
<div className="prose prose-sm dark:prose-invert max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
|
components={{
|
||||||
|
pre: ({ children }) => (
|
||||||
|
<pre className="bg-muted rounded-lg p-3 overflow-x-auto text-xs">
|
||||||
|
{children}
|
||||||
|
</pre>
|
||||||
|
),
|
||||||
|
code: ({ children, className }) => {
|
||||||
|
if (!className) {
|
||||||
|
return (
|
||||||
|
<code className="bg-muted rounded px-1.5 py-0.5 text-xs font-mono">
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <code className={className}>{children}</code>;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,12 +119,21 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
|
|
||||||
// Curtain is disabled on home page and on /projects without a selected project
|
// Curtain is disabled on home page and on /projects without a selected project
|
||||||
const searchObj = routerState.location.search as Record<string, unknown>;
|
const searchObj = routerState.location.search as Record<string, unknown>;
|
||||||
|
const projectId = typeof searchObj['projectId'] === 'string' ? searchObj['projectId'] : undefined;
|
||||||
const curtainEnabled =
|
const curtainEnabled =
|
||||||
currentPath !== '/' &&
|
currentPath !== '/' &&
|
||||||
!(currentPath === '/projects' && !searchObj['projectId']);
|
!(currentPath === '/projects' && !projectId);
|
||||||
const curtainEnabledRef = useRef(curtainEnabled);
|
const curtainEnabledRef = useRef(curtainEnabled);
|
||||||
curtainEnabledRef.current = curtainEnabled;
|
curtainEnabledRef.current = curtainEnabled;
|
||||||
|
|
||||||
|
// Derive AI chat context from current route
|
||||||
|
const isProjectView = currentPath === '/projects' && !!projectId;
|
||||||
|
const contextType = isProjectView ? 'project' as const : 'global' as const;
|
||||||
|
const projectQuery = trpc.projects.get.useQuery(
|
||||||
|
{ id: projectId ?? '' },
|
||||||
|
{ enabled: !!projectId },
|
||||||
|
);
|
||||||
|
|
||||||
// --- Curtain animation state ---
|
// --- Curtain animation state ---
|
||||||
const [curtainOpen, setCurtainOpen] = useState(false);
|
const [curtainOpen, setCurtainOpen] = useState(false);
|
||||||
const curtainOpenRef = useRef(false);
|
const curtainOpenRef = useRef(false);
|
||||||
@@ -149,6 +158,17 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
else openCurtain();
|
else openCurtain();
|
||||||
}, [openCurtain, closeCurtain]);
|
}, [openCurtain, closeCurtain]);
|
||||||
|
|
||||||
|
// Keep curtain position in sync with window height on resize
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
if (curtainOpenRef.current) {
|
||||||
|
y.set(window.innerHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, [y]);
|
||||||
|
|
||||||
// Keyboard shortcut: Cmd/Ctrl+K
|
// Keyboard shortcut: Cmd/Ctrl+K
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -191,10 +211,17 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
<AppSidebar
|
<AppSidebar
|
||||||
currentPath={currentPath}
|
currentPath={currentPath}
|
||||||
setTokenDialogOpen={setTokenDialogOpen}
|
setTokenDialogOpen={setTokenDialogOpen}
|
||||||
|
onNavClick={closeCurtain}
|
||||||
/>
|
/>
|
||||||
<SidebarInset className="overflow-hidden">
|
<SidebarInset className="overflow-hidden">
|
||||||
{/* AI Chat layer: always mounted behind the content panel */}
|
{/* AI Chat layer: always mounted behind the content panel */}
|
||||||
<AIChatPanel onOpenSettings={() => setTokenDialogOpen(true)} />
|
<AIChatPanel
|
||||||
|
onOpenSettings={() => setTokenDialogOpen(true)}
|
||||||
|
contextType={contextType}
|
||||||
|
projectId={projectId}
|
||||||
|
projectName={projectQuery.data?.name}
|
||||||
|
curtainOpen={curtainOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Content panel: slides down to reveal chat */}
|
{/* Content panel: slides down to reveal chat */}
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -273,9 +300,10 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
interface AppSidebarProps {
|
interface AppSidebarProps {
|
||||||
currentPath: string;
|
currentPath: string;
|
||||||
setTokenDialogOpen: (open: boolean) => void;
|
setTokenDialogOpen: (open: boolean) => void;
|
||||||
|
onNavClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppSidebar({ currentPath, setTokenDialogOpen }: AppSidebarProps) {
|
function AppSidebar({ currentPath, setTokenDialogOpen, onNavClick }: AppSidebarProps) {
|
||||||
const { toggleSidebar } = useSidebar();
|
const { toggleSidebar } = useSidebar();
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
@@ -328,7 +356,7 @@ function AppSidebar({ currentPath, setTokenDialogOpen }: AppSidebarProps) {
|
|||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
tooltip={label}
|
tooltip={label}
|
||||||
>
|
>
|
||||||
<Link to={to}>
|
<Link to={to} onClick={onNavClick}>
|
||||||
<Icon />
|
<Icon />
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible';
|
} from '@/components/ui/collapsible';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -416,7 +417,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Project tree */}
|
{/* Project tree */}
|
||||||
<div className="flex-1 overflow-y-auto py-1 px-1">
|
<ScrollArea className="flex-1 py-1 px-1">
|
||||||
{totalProjects === 0 ? (
|
{totalProjects === 0 ? (
|
||||||
<Empty>
|
<Empty>
|
||||||
<EmptyHeader>
|
<EmptyHeader>
|
||||||
@@ -824,7 +825,7 @@ export function ProjectSidebar({ selectedProjectId, onSelectProject }: ProjectSi
|
|||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Rename project dialog */}
|
{/* Rename project dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
|||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { TaskItem } from './TaskRow';
|
import type { TaskItem } from './TaskRow';
|
||||||
|
|
||||||
@@ -275,7 +276,8 @@ export function EditTaskDialog({ task, open, onOpenChange }: EditTaskDialogProps
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-64 p-2" align="start">
|
<PopoverContent className="w-64 p-2" align="start">
|
||||||
{knownAssignees.length > 0 && (
|
{knownAssignees.length > 0 && (
|
||||||
<div className="max-h-36 overflow-y-auto flex flex-col gap-0.5 mb-2">
|
<ScrollArea className="max-h-36 mb-2">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
{knownAssignees.map((name) => (
|
{knownAssignees.map((name) => (
|
||||||
<Button
|
<Button
|
||||||
key={name}
|
key={name}
|
||||||
@@ -294,6 +296,7 @@ export function EditTaskDialog({ task, open, onOpenChange }: EditTaskDialogProps
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
{knownAssignees.length === 0 && (
|
{knownAssignees.length === 0 && (
|
||||||
<p className="text-xs text-muted-foreground px-2 py-1 mb-2">No existing assignees</p>
|
<p className="text-xs text-muted-foreground px-2 py-1 mb-2">No existing assignees</p>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
|||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const NO_CLIENT = '__no_client__';
|
const NO_CLIENT = '__no_client__';
|
||||||
@@ -512,7 +513,8 @@ export function NewTaskDialog({ open, onOpenChange, defaultProjectId, defaultSta
|
|||||||
<PopoverContent className="w-64 p-2" align="start">
|
<PopoverContent className="w-64 p-2" align="start">
|
||||||
{/* Known assignees list */}
|
{/* Known assignees list */}
|
||||||
{knownAssignees.length > 0 && (
|
{knownAssignees.length > 0 && (
|
||||||
<div className="max-h-36 overflow-y-auto flex flex-col gap-0.5 mb-2">
|
<ScrollArea className="max-h-36 mb-2">
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
{knownAssignees.map((name) => (
|
{knownAssignees.map((name) => (
|
||||||
<Button
|
<Button
|
||||||
key={name}
|
key={name}
|
||||||
@@ -531,6 +533,7 @@ export function NewTaskDialog({ open, onOpenChange, defaultProjectId, defaultSta
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
{knownAssignees.length === 0 && (
|
{knownAssignees.length === 0 && (
|
||||||
<p className="text-xs text-muted-foreground px-2 py-1 mb-2">No existing assignees</p>
|
<p className="text-xs text-muted-foreground px-2 py-1 mb-2">No existing assignees</p>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { trpc } from '@/lib/trpc';
|
import { trpc } from '@/lib/trpc';
|
||||||
import { PriorityBadge } from './PriorityBadge';
|
import { PriorityBadge } from './PriorityBadge';
|
||||||
import { parseAssignees, type TaskItem } from './TaskRow';
|
import { parseAssignees, type TaskItem } from './TaskRow';
|
||||||
@@ -194,7 +195,8 @@ export function TaskDetailDialog({ task, open, onOpenChange, onEdit, onDelete }:
|
|||||||
|
|
||||||
<TabsContent value="comment" className="px-6 py-4 min-h-[120px] flex flex-col gap-4">
|
<TabsContent value="comment" className="px-6 py-4 min-h-[120px] flex flex-col gap-4">
|
||||||
{/* Comment list */}
|
{/* Comment list */}
|
||||||
<div className="flex flex-col gap-4 max-h-[260px] overflow-y-auto">
|
<ScrollArea className="max-h-[260px]">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
{(!comments || comments.length === 0) ? (
|
{(!comments || comments.length === 0) ? (
|
||||||
<p className="text-sm text-muted-foreground italic">No comments yet.</p>
|
<p className="text-sm text-muted-foreground italic">No comments yet.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -223,6 +225,7 @@ export function TaskDetailDialog({ task, open, onOpenChange, onEdit, onDelete }:
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
{/* Add comment input */}
|
{/* Add comment input */}
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface AddCheckpointDialogProps {
|
interface AddCheckpointDialogProps {
|
||||||
@@ -85,7 +86,8 @@ export function AddCheckpointDialog({ open, onOpenChange, defaultProjectId }: Ad
|
|||||||
|
|
||||||
{/* Just-added list */}
|
{/* Just-added list */}
|
||||||
{added.length > 0 && (
|
{added.length > 0 && (
|
||||||
<div className="flex flex-col gap-1.5 max-h-32 overflow-y-auto">
|
<ScrollArea className="max-h-32">
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
{added.map((entry, i) => (
|
{added.map((entry, i) => (
|
||||||
<div key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
|
<div key={i} className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
<Check className="h-3.5 w-3.5 text-chart-2 shrink-0" />
|
<Check className="h-3.5 w-3.5 text-chart-2 shrink-0" />
|
||||||
@@ -94,6 +96,7 @@ export function AddCheckpointDialog({ open, onOpenChange, defaultProjectId }: Ad
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||||
|
|||||||
65
src/renderer/components/ui/scroll-area.tsx
Normal file
65
src/renderer/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function ScrollArea({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
viewportRef,
|
||||||
|
viewportClassName,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root> & {
|
||||||
|
viewportRef?: React.Ref<HTMLDivElement>;
|
||||||
|
viewportClassName?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.Root
|
||||||
|
data-slot="scroll-area"
|
||||||
|
className={cn("relative", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Viewport
|
||||||
|
ref={viewportRef}
|
||||||
|
data-slot="scroll-area-viewport"
|
||||||
|
className={cn(
|
||||||
|
"focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1",
|
||||||
|
viewportClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollBar />
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollBar({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||||
|
return (
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||||
|
data-slot="scroll-area-scrollbar"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"flex touch-none p-px transition-colors select-none",
|
||||||
|
orientation === "vertical" &&
|
||||||
|
"h-full w-2.5 border-l border-l-transparent",
|
||||||
|
orientation === "horizontal" &&
|
||||||
|
"h-2.5 flex-col border-t border-t-transparent",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||||
|
data-slot="scroll-area-thumb"
|
||||||
|
className="bg-border relative flex-1 rounded-full"
|
||||||
|
/>
|
||||||
|
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ScrollArea, ScrollBar }
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "tw-animate-css";
|
@import "tw-animate-css";
|
||||||
@import "shadcn/tailwind.css";
|
@import "shadcn/tailwind.css";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@@ -128,7 +129,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Geist', 'Inter', system-ui, sans-serif;
|
font-family: 'Geist', 'Inter', system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji';
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden; /* Electron: no OS scrollbars */
|
overflow: hidden; /* Electron: no OS scrollbars */
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FolderKanban } from 'lucide-react';
|
|||||||
import { ProjectSidebar } from '@/components/projects/ProjectSidebar';
|
import { ProjectSidebar } from '@/components/projects/ProjectSidebar';
|
||||||
import { ProjectDetail } from '@/components/projects/ProjectDetail';
|
import { ProjectDetail } from '@/components/projects/ProjectDetail';
|
||||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty';
|
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
const searchSchema = z.object({
|
const searchSchema = z.object({
|
||||||
projectId: z.string().optional(),
|
projectId: z.string().optional(),
|
||||||
@@ -28,7 +29,7 @@ function ProjectsPage() {
|
|||||||
selectedProjectId={projectId}
|
selectedProjectId={projectId}
|
||||||
onSelectProject={handleSelectProject}
|
onSelectProject={handleSelectProject}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<ScrollArea className="flex-1">
|
||||||
{projectId ? (
|
{projectId ? (
|
||||||
<ProjectDetail projectId={projectId} />
|
<ProjectDetail projectId={projectId} />
|
||||||
) : (
|
) : (
|
||||||
@@ -44,7 +45,7 @@ function ProjectsPage() {
|
|||||||
</EmptyHeader>
|
</EmptyHeader>
|
||||||
</Empty>
|
</Empty>
|
||||||
)}
|
)}
|
||||||
</div>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user