feat: Integrate GitHub Copilot SDK and LangChain for provider-independent orchestration

- Added @github/copilot-sdk and related dependencies for GitHub Copilot integration.
- Implemented ChatCopilot adapter for LangChain compatibility.
- Created LLM factory to return provider-specific models (OpenAI, Anthropic, Copilot).
- Developed Orchestrator agent using LangGraph for intent routing and context assembly.
- Enhanced IPC communication for streaming AI responses to the renderer.
- Updated progress documentation with implementation details and learnings.
This commit is contained in:
Roberto Musso
2026-02-23 17:58:00 +01:00
parent c1aa6829c9
commit 00a43e0fbc
13 changed files with 1348 additions and 14 deletions

574
package-lock.json generated
View File

@@ -10,7 +10,12 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fontsource/geist": "^5.2.8", "@fontsource/geist": "^5.2.8",
"@github/copilot-sdk": "^0.1.25",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@langchain/anthropic": "^1.3.19",
"@langchain/core": "^1.1.27",
"@langchain/langgraph": "^1.1.5",
"@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/vite": "^4.2.0", "@tailwindcss/vite": "^4.2.0",
@@ -89,6 +94,26 @@
"nup": "bin/nup.mjs" "nup": "bin/nup.mjs"
} }
}, },
"node_modules/@anthropic-ai/sdk": {
"version": "0.74.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.74.0.tgz",
"integrity": "sha512-srbJV7JKsc5cQ6eVuFzjZO7UR3xEPJqPamHFIe29bs38Ij2IripoAhC0S5NslNbaFUYqBKypmmpzMTpqfHEUDw==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.0", "version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -619,6 +644,12 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@cfworker/json-schema": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@cfworker/json-schema/-/json-schema-4.1.1.tgz",
"integrity": "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==",
"license": "MIT"
},
"node_modules/@codemirror/autocomplete": { "node_modules/@codemirror/autocomplete": {
"version": "6.20.0", "version": "6.20.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
@@ -3062,6 +3093,133 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@github/copilot": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.411.tgz",
"integrity": "sha512-I3/7gw40Iu1O+kTyNPKJHNqDRyOebjsUW6wJsvSVrOpT0TNa3/lfm8xdS2XUuJWkp+PgEG/PRwF7u3DVNdP7bQ==",
"license": "SEE LICENSE IN LICENSE.md",
"bin": {
"copilot": "npm-loader.js"
},
"optionalDependencies": {
"@github/copilot-darwin-arm64": "0.0.411",
"@github/copilot-darwin-x64": "0.0.411",
"@github/copilot-linux-arm64": "0.0.411",
"@github/copilot-linux-x64": "0.0.411",
"@github/copilot-win32-arm64": "0.0.411",
"@github/copilot-win32-x64": "0.0.411"
}
},
"node_modules/@github/copilot-darwin-arm64": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.411.tgz",
"integrity": "sha512-dtr+iHxTS4f8HlV2JT9Fp0FFoxuiPWCnU3XGmrHK+rY6bX5okPC2daU5idvs77WKUGcH8yHTZtfbKYUiMxKosw==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"copilot-darwin-arm64": "copilot"
}
},
"node_modules/@github/copilot-darwin-x64": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.411.tgz",
"integrity": "sha512-zhdbQCbPi1L4iHClackSLx8POfklA+NX9RQLuS48HlKi/0KI/JlaDA/bdbIeMR79wjif5t9gnc/m+RTVmHlRtA==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"darwin"
],
"bin": {
"copilot-darwin-x64": "copilot"
}
},
"node_modules/@github/copilot-linux-arm64": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.411.tgz",
"integrity": "sha512-oZYZ7oX/7O+jzdTUcHkfD1A8YnNRW6mlUgdPjUg+5rXC43bwIdyatAnc0ObY21m9h8ghxGqholoLhm5WnGv1LQ==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linux-arm64": "copilot"
}
},
"node_modules/@github/copilot-linux-x64": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.411.tgz",
"integrity": "sha512-nnXrKANmmGnkwa3ROlKdAhVNOx8daeMSE8Xh0o3ybKckFv4s38blhKdcxs0RJQRxgAk4p7XXGlDDKNRhurqF1g==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"linux"
],
"bin": {
"copilot-linux-x64": "copilot"
}
},
"node_modules/@github/copilot-sdk": {
"version": "0.1.25",
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.25.tgz",
"integrity": "sha512-hIgYLPXzWw9bNgrsD5BLKmgVH20ow5Or5UyVXfVe3YgeiaTgFxC4jWSAVHLGB6ufHZUrvbjppcq2dWK63FmDRA==",
"license": "MIT",
"dependencies": {
"@github/copilot": "^0.0.411",
"vscode-jsonrpc": "^8.2.1",
"zod": "^4.3.6"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@github/copilot-win32-arm64": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.411.tgz",
"integrity": "sha512-h+Bovb2YVCQSeELZOO7zxv8uht45XHcvAkFbRsc1gf9dl109sSUJIcB4KAhs8Aznk28qksxz7kvdSgUWyQBlIA==",
"cpu": [
"arm64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"copilot-win32-arm64": "copilot.exe"
}
},
"node_modules/@github/copilot-win32-x64": {
"version": "0.0.411",
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.411.tgz",
"integrity": "sha512-xmOgi1lGvUBHQJWmq5AK1EP95+Y8xR4TFoK9OCSOaGbQ+LFcX2jF7iavnMolfWwddabew/AMQjsEHlXvbgMG8Q==",
"cpu": [
"x64"
],
"license": "SEE LICENSE IN LICENSE.md",
"optional": true,
"os": [
"win32"
],
"bin": {
"copilot-win32-x64": "copilot.exe"
}
},
"node_modules/@hello-pangea/dnd": { "node_modules/@hello-pangea/dnd": {
"version": "18.0.1", "version": "18.0.1",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
@@ -3610,6 +3768,182 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@langchain/anthropic": {
"version": "1.3.19",
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.19.tgz",
"integrity": "sha512-OY7/pi4MBuBA/RCJJGqL0Dx3J7Udqwz35Q9sTv5oWiwt6UgMQKGZ17k8gEXyknpuKcRd3A81M09mzoGL8/6zGQ==",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.74.0",
"zod": "^3.25.76 || ^4"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@langchain/core": "^1.1.27"
}
},
"node_modules/@langchain/core": {
"version": "1.1.27",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.27.tgz",
"integrity": "sha512-YVtEz3nqCh8WxtdVXUICmt2BR2An+mn4YRJUBwcHX47Yrh2VwxpO0l97B2N/sNi658m65HnGyz2/hAjF3fzc1w==",
"license": "MIT",
"dependencies": {
"@cfworker/json-schema": "^4.0.2",
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
"js-tiktoken": "^1.0.12",
"langsmith": ">=0.5.0 <1.0.0",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",
"uuid": "^10.0.0",
"zod": "^3.25.76 || ^4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@langchain/core/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@langchain/langgraph": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@langchain/langgraph/-/langgraph-1.1.5.tgz",
"integrity": "sha512-uJC/asydf/GoHpo9x42lf9hs8ufCkMuJ9sDle5ybP7sMD0XryOfE0E4J3deARk9ZadCCt6zeCoCNu/mTbx8+Sg==",
"license": "MIT",
"dependencies": {
"@langchain/langgraph-checkpoint": "^1.0.0",
"@langchain/langgraph-sdk": "~2.0.0",
"@standard-schema/spec": "1.1.0",
"uuid": "^10.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": "^1.1.16",
"zod": "^3.25.32 || ^4.2.0",
"zod-to-json-schema": "^3.x"
},
"peerDependenciesMeta": {
"zod-to-json-schema": {
"optional": true
}
}
},
"node_modules/@langchain/langgraph-checkpoint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-checkpoint/-/langgraph-checkpoint-1.0.0.tgz",
"integrity": "sha512-xrclBGvNCXDmi0Nz28t3vjpxSH6UYx6w5XAXSiiB1WEdc2xD2iY/a913I3x3a31XpInUW/GGfXXfePfaghV54A==",
"license": "MIT",
"dependencies": {
"uuid": "^10.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@langchain/core": "^1.0.1"
}
},
"node_modules/@langchain/langgraph-sdk": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@langchain/langgraph-sdk/-/langgraph-sdk-2.0.0.tgz",
"integrity": "sha512-Xdkl1hve84ZGQ7fgpiBIBvjODhtjbPPccY4snOtYgSdzRXZkESsi2Y7RDKgFe1nC9+DbX+QaYom0raD/XFBKAw==",
"deprecated": "This version is not intended for use. Please use 1.x versions of @langchain/langgraph-sdk instead",
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.15",
"p-queue": "^9.0.1",
"p-retry": "^7.1.1",
"uuid": "^13.0.0"
},
"peerDependencies": {
"@langchain/core": "^1.1.16",
"react": "^18 || ^19",
"react-dom": "^18 || ^19"
},
"peerDependenciesMeta": {
"@langchain/core": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/@langchain/langgraph-sdk/node_modules/p-queue": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz",
"integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1",
"p-timeout": "^7.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@langchain/langgraph-sdk/node_modules/p-timeout": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz",
"integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@langchain/langgraph-sdk/node_modules/uuid": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
"integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/@langchain/openai": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@langchain/openai/-/openai-1.2.9.tgz",
"integrity": "sha512-hExRiUoKOg1vfkwBAI5J2C4tqNx5LLZ0CUelG8Ej6K8bS2LfFN9bL4ZNQYqNIwAJNSqpDaV9tknxP2fssZjp+Q==",
"license": "MIT",
"dependencies": {
"js-tiktoken": "^1.0.12",
"openai": "^6.18.0",
"zod": "^3.25.76 || ^4"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@langchain/core": "^1.1.27"
}
},
"node_modules/@lezer/common": { "node_modules/@lezer/common": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
@@ -6551,6 +6885,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@szmarczak/http-timer": { "node_modules/@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@@ -7360,7 +7700,6 @@
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/json5": { "node_modules/@types/json5": {
@@ -7499,6 +7838,12 @@
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/validate-npm-package-name": { "node_modules/@types/validate-npm-package-name": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz",
@@ -9266,6 +9611,18 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001770", "version": "1.0.30001770",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
@@ -9686,6 +10043,15 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/console-table-printer": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.15.0.tgz",
"integrity": "sha512-SrhBq4hYVjLCkBVOWaTzceJalvn5K1Zq5aQA6wXC/cYjI3frKWNPEMK3sZsJfNNQApvCQmgBcc13ZKmFj8qExw==",
"license": "MIT",
"dependencies": {
"simple-wcswidth": "^1.1.2"
}
},
"node_modules/content-disposition": { "node_modules/content-disposition": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
@@ -10008,6 +10374,15 @@
} }
} }
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decode-named-character-reference": { "node_modules/decode-named-character-reference": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
@@ -12362,7 +12737,6 @@
"version": "5.0.4", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/events": { "node_modules/events": {
@@ -14260,6 +14634,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-network-error": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz",
"integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-node-process": { "node_modules/is-node-process": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
@@ -14615,6 +15001,15 @@
"url": "https://github.com/sponsors/panva" "url": "https://github.com/sponsors/panva"
} }
}, },
"node_modules/js-tiktoken": {
"version": "1.0.21",
"resolved": "https://registry.npmjs.org/js-tiktoken/-/js-tiktoken-1.0.21.tgz",
"integrity": "sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.5.1"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -14662,6 +15057,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -14782,6 +15190,52 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/langsmith": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.5.6.tgz",
"integrity": "sha512-T/RA2l2MsTYX0z1aW8rQ2hBQZEOuXV2v/6tkfG6R5EotJTKMpw1dERCbvP8ezOP8otyWfnNlQA88ZnMRsQ7CHA==",
"license": "MIT",
"dependencies": {
"@types/uuid": "^10.0.0",
"chalk": "^5.6.2",
"console-table-printer": "^2.12.1",
"p-queue": "^6.6.2",
"semver": "^7.6.3",
"uuid": "^10.0.0"
},
"peerDependencies": {
"@opentelemetry/api": "*",
"@opentelemetry/exporter-trace-otlp-proto": "*",
"@opentelemetry/sdk-trace-base": "*",
"openai": "*"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@opentelemetry/exporter-trace-otlp-proto": {
"optional": true
},
"@opentelemetry/sdk-trace-base": {
"optional": true
},
"openai": {
"optional": true
}
}
},
"node_modules/langsmith/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/levn": { "node_modules/levn": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -16646,6 +17100,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"license": "MIT",
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/mute-stream": { "node_modules/mute-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
@@ -17071,6 +17534,27 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/openai": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.22.0.tgz",
"integrity": "sha512-7Yvy17F33Bi9RutWbsaYt5hJEEJ/krRPOrwan+f9aCPuMat1WVsb2VNSII5W1EksKT6fF69TG/xj4XzodK3JZw==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -17205,7 +17689,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@@ -17269,6 +17752,55 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-queue/node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/p-retry": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.1.tgz",
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
"license": "MIT",
"dependencies": {
"is-network-error": "^1.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"license": "MIT",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": { "node_modules/p-try": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@@ -20075,6 +20607,12 @@
"simple-concat": "^1.0.0" "simple-concat": "^1.0.0"
} }
}, },
"node_modules/simple-wcswidth": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.1.2.tgz",
"integrity": "sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==",
"license": "MIT"
},
"node_modules/sisteransi": { "node_modules/sisteransi": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -21000,6 +21538,12 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
"node_modules/ts-morph": { "node_modules/ts-morph": {
"version": "26.0.0", "version": "26.0.0",
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz", "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-26.0.0.tgz",
@@ -22052,6 +22596,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/validate-npm-package-license": { "node_modules/validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -22170,6 +22727,15 @@
} }
} }
}, },
"node_modules/vscode-jsonrpc": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
"integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.28", "version": "3.5.28",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz",
@@ -22664,7 +23230,7 @@
"version": "3.25.1", "version": "3.25.1",
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
"integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"dev": true, "devOptional": true,
"license": "ISC", "license": "ISC",
"peerDependencies": { "peerDependencies": {
"zod": "^3.25 || ^4" "zod": "^3.25 || ^4"

View File

@@ -46,7 +46,12 @@
}, },
"dependencies": { "dependencies": {
"@fontsource/geist": "^5.2.8", "@fontsource/geist": "^5.2.8",
"@github/copilot-sdk": "^0.1.25",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@langchain/anthropic": "^1.3.19",
"@langchain/core": "^1.1.27",
"@langchain/langgraph": "^1.1.5",
"@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/vite": "^4.2.0", "@tailwindcss/vite": "^4.2.0",

View File

@@ -350,8 +350,8 @@
"Typecheck passes" "Typecheck passes"
], ],
"priority": 19, "priority": 19,
"passes": false, "passes": true,
"notes": "" "notes": "Completed: LangGraph-based provider-independent orchestrator. StateGraph with classifyIntent node (structured output via withStructuredOutput(zodSchema) for route classification) + 3 specialist agent nodes (projectAgent, knowledgeAgent, generalAgent) connected via addConditionalEdges. LLM factory (src/main/ai/llm.ts) returns BaseChatModel per provider: ChatOpenAI (openai), ChatAnthropic (anthropic), ChatCopilot adapter (copilot). ChatCopilot (src/main/ai/chat-copilot.ts) wraps @github/copilot-sdk in SimpleChatModel with _call() and _streamResponseChunks(). Streaming via LangGraph streamMode:'messages' + IPC side-channel (ai:stream). Context assembly from DB (buildProjectContext/buildGlobalContext), tRPC context with event.sender, auth/timeout error handling. Typecheck passes."
}, },
{ {
"id": "US-020", "id": "US-020",

View File

@@ -351,3 +351,68 @@
- Token is stored per-provider so multiple providers can have tokens stored simultaneously; switching providers just changes which key is read - Token is stored per-provider so multiple providers can have tokens stored simultaneously; switching providers just changes which key is read
- `electron-store` dot-notation access (`store.get('a.b')`) works but loses type safety; prefer `store.get('encryptedTokens')` then access the nested key on the result object - `electron-store` dot-notation access (`store.get('a.b')`) works but loses type safety; prefer `store.get('encryptedTokens')` then access the nested key on the result object
--- ---
## 2026-02-23 - US-019
- What was implemented:
- Installed `@github/copilot-sdk` (v0.1.25) — official GitHub Copilot SDK with CopilotClient for programmatic CLI control via JSON-RPC
- Updated `src/main/ai/copilot.ts` — CopilotClient singleton created via dynamic `import()` (SDK is ESM-only), `initialize()` starts the client with `githubToken`, `getCopilotClient()` exported, clean shutdown on `app.before-quit`
- Added `@github/copilot-sdk` and `@github/copilot` to `vite.main.config.mts` externals (native CLI binary + prebuilds must stay in node_modules)
- Created IPC streaming side-channel:
- `src/preload/trpc.ts` — exposed `window.electronAI.onStreamChunk(cb)` via contextBridge on `ai:stream` IPC channel
- `src/main/ipc.ts` — added `TRPCContext` type with optional `sender: Electron.WebContents`, passed `event.sender` into tRPC context
- `src/renderer/lib/ipcLink.ts` — added `Window.electronAI` type declaration
- Updated `src/main/router/index.ts` — `initTRPC.context<TRPCContext>().create()`, `ai.chat` mutation now calls `orchestrate()` with streaming + error handling
- Created `src/main/ai/orchestrator.ts` (new) — core Orchestrator agent:
- System prompt instructs model to use exactly one routing tool per message
- 3 routing tools: `route_to_project`, `route_to_knowledge`, `route_to_general` (each with JSON schema params + handler callback)
- `buildProjectContext(projectId)` — fetches project, tasks, checkpoints, notes from DB
- `buildGlobalContext()` — fetches active projects, task counts, upcoming tasks due this week
- Two-phase orchestration: (1) Orchestrator session classifies intent via tool call, (2) Specialist session generates streamed response
- Specialist agent prompts: @ProjectAgent (scoped project data), @KnowledgeAgent (stub — LanceDB pending US-023), @GeneralAgent (workspace summary)
- Streaming via `session.on('assistant.message_delta')` → `sender.send('ai:stream', { token, done })`
- Error classification: auth (401/403) → friendly message, timeout → retry prompt, generic → error message
- Typecheck passes (zero errors), no new lint errors
- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/copilot.ts`, `src/main/ai/orchestrator.ts` (new), `src/main/ipc.ts`, `src/preload/trpc.ts`, `src/main/router/index.ts`, `src/renderer/lib/ipcLink.ts`, `prd.json`, `progress.txt`
- **Learnings for future iterations:**
- `@github/copilot-sdk` is ESM-only (`"type": "module"`) — cannot `require()` from CJS Electron main process. Use `await import('@github/copilot-sdk')` inside async functions
- The SDK depends on `@github/copilot` (the CLI binary, ~400MB) which includes native prebuilds, ripgrep, tree-sitter WASM, etc. Both must be externalized in Vite config
- `CopilotClient` manages a CLI subprocess via JSON-RPC. `autoStart: true` + `autoRestart: true` handles lifecycle. Call `client.stop()` on `app.before-quit`
- SDK tools use a `handler: async (args) => ToolResult` callback pattern — the SDK calls your handler when the model invokes the tool. This is different from OpenAI's function-calling (where you check tool_calls in the response)
- `session.sendAndWait()` blocks until `session.idle`, returns `AssistantMessageEvent | undefined`. Default timeout is 60s
- `session.on('assistant.message_delta', cb)` fires for each streaming chunk with `{ messageId, deltaContent }`. Returns an unsubscribe function
- `SessionConfig.availableTools` controls which tools are exposed to the model. Set to only your custom tool names to disable all built-in Copilot tools
- `SystemMessageConfig` has `mode: 'replace'` to fully replace the SDK's default system prompt — necessary for agent specialization
- For IPC streaming: extend the preload to expose a separate channel (`ai:stream`) via `contextBridge.exposeInMainWorld()`, and use `event.sender.send()` from the main process. This avoids touching the tRPC request-response infrastructure
- `TRPCContext` with optional `sender` field preserves backward compatibility — non-AI procedures get `sender` but don't use it
---
## 2026-02-23 - US-019 Refactor: LangGraph Provider-Independent Orchestration
- What was implemented:
- Installed `@langchain/langgraph`, `@langchain/core`, `@langchain/openai`, `@langchain/anthropic` for provider-independent agent orchestration
- Created `src/main/ai/llm.ts` (new) — LLM factory returning `BaseChatModel` for the active provider:
- `openai` → `ChatOpenAI` (gpt-4o-mini, streaming)
- `anthropic` → `ChatAnthropic` (claude-sonnet, streaming)
- `copilot` → `ChatCopilot` adapter wrapping the Copilot SDK
- Created `src/main/ai/chat-copilot.ts` (new) — LangChain-compatible `SimpleChatModel` adapter for GitHub Copilot SDK:
- `_call()` creates a Copilot session, sends messages, returns response text
- `_streamResponseChunks()` yields `ChatGenerationChunk` tokens via `assistant.message_delta` event listener + async generator pattern
- Refactored `src/main/ai/orchestrator.ts` — replaced Copilot SDK sessions with LangGraph `StateGraph`:
- `OrchestratorState` annotation: `userMessage`, `chatContext`, `route`, `messages`, `response`
- `classifyIntent` node: uses `llm.withStructuredOutput(RouteSchema)` for intent classification (project/knowledge/general)
- `projectAgent`, `knowledgeAgent`, `generalAgent` nodes: each gets context from DB + invokes LLM
- `addConditionalEdges` routes from classifier to specialist based on `state.route`
- Streaming via LangGraph `streamMode: 'messages'` — tokens from specialist nodes forwarded to renderer via IPC
- Graph is compiled once (singleton) and reused across calls
- Updated `vite.main.config.mts` — added all `@langchain/*` packages to externals
- Context assembly functions (`buildProjectContext`, `buildGlobalContext`) and system prompts preserved unchanged
- Typecheck passes (zero errors), no new lint errors
- Files changed: `package.json`, `package-lock.json`, `vite.main.config.mts`, `src/main/ai/llm.ts` (new), `src/main/ai/chat-copilot.ts` (new), `src/main/ai/orchestrator.ts`, `progress.txt`
- **Learnings for future iterations:**
- LangGraph packages ship dual ESM/CJS (`require` + `import` in exports map) — no dynamic import needed unlike `@github/copilot-sdk`
- `SimpleChatModel` from `@langchain/core` only requires implementing `_call()` and `_llmType()` — much simpler than full `BaseChatModel`
- `withStructuredOutput(zodSchema)` on a `BaseChatModel` forces the LLM to return JSON matching the schema — ideal for intent routing without custom tool handlers
- LangGraph `streamMode: 'messages'` yields `[chunk, metadata]` tuples; `metadata.langgraph_node` identifies which graph node produced each token — use this to filter out classifier tokens
- `Annotation.Root({})` with a `reducer` function enables append-only message arrays in state — matches LangGraph's immutable state update pattern
- 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`
---

134
src/main/ai/chat-copilot.ts Normal file
View File

@@ -0,0 +1,134 @@
/**
* ChatCopilot — LangChain-compatible ChatModel adapter for the GitHub Copilot SDK.
*
* Wraps the CopilotClient's session API so it can be used as a drop-in
* BaseChatModel within LangGraph, making the orchestrator provider-agnostic.
*/
import { SimpleChatModel, type BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
import type { BaseMessage } from '@langchain/core/messages';
import { AIMessageChunk } from '@langchain/core/messages';
import { ChatGenerationChunk } from '@langchain/core/outputs';
import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
import { getCopilotClient } from './copilot';
const COPILOT_TIMEOUT = 60_000;
export class ChatCopilot extends SimpleChatModel<BaseChatModelCallOptions> {
constructor() {
super({});
}
_llmType(): string {
return 'copilot';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async _call(messages: BaseMessage[], _options: this['ParsedCallOptions'], _runManager?: CallbackManagerForLLMRun): Promise<string> {
const client = getCopilotClient();
if (!client) {
throw new Error('CopilotClient not initialized. Please add your GitHub token in Settings.');
}
// Extract system message and user prompt from LangChain messages
const systemContent = messages
.filter((m) => m._getType() === 'system')
.map((m) => (typeof m.content === 'string' ? m.content : ''))
.join('\n');
const userContent = messages
.filter((m) => m._getType() === 'human')
.map((m) => (typeof m.content === 'string' ? m.content : ''))
.join('\n');
const session = await client.createSession({
systemMessage: systemContent
? { mode: 'replace', content: systemContent }
: undefined,
availableTools: [],
streaming: true,
});
try {
const result = await session.sendAndWait({ prompt: userContent }, COPILOT_TIMEOUT);
return result?.data.content ?? '';
} finally {
await session.destroy().catch(() => { /* ignore cleanup errors */ });
}
}
async *_streamResponseChunks(
messages: BaseMessage[],
_options: this['ParsedCallOptions'],
_runManager?: CallbackManagerForLLMRun,
): AsyncGenerator<ChatGenerationChunk> {
const client = getCopilotClient();
if (!client) {
throw new Error('CopilotClient not initialized. Please add your GitHub token in Settings.');
}
const systemContent = messages
.filter((m) => m._getType() === 'system')
.map((m) => (typeof m.content === 'string' ? m.content : ''))
.join('\n');
const userContent = messages
.filter((m) => m._getType() === 'human')
.map((m) => (typeof m.content === 'string' ? m.content : ''))
.join('\n');
const session = await client.createSession({
systemMessage: systemContent
? { mode: 'replace', content: systemContent }
: undefined,
availableTools: [],
streaming: true,
});
// Buffer chunks via event listener and yield them
const chunks: string[] = [];
let done = false;
let resolveNext: (() => void) | null = null;
const unsubDelta = session.on('assistant.message_delta', (event) => {
const delta = event.data.deltaContent;
if (delta) {
chunks.push(delta);
resolveNext?.();
}
});
const unsubEnd = session.on('session.idle', () => {
done = true;
resolveNext?.();
});
// Fire the request (don't await — we'll drain via events)
const sendPromise = session.sendAndWait({ prompt: userContent }, COPILOT_TIMEOUT);
try {
while (!done || chunks.length > 0) {
if (chunks.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const text = chunks.shift()!;
const chunk = new ChatGenerationChunk({
message: new AIMessageChunk({ content: text }),
text,
});
await _runManager?.handleLLMNewToken(text);
yield chunk;
} else if (!done) {
await new Promise<void>((resolve) => {
resolveNext = resolve;
});
}
}
// Ensure the send completes
await sendPromise;
} finally {
unsubDelta();
unsubEnd();
await session.destroy().catch(() => { /* ignore cleanup errors */ });
}
}
}

View File

@@ -1,5 +1,10 @@
import { app } from 'electron';
import { registerProvider, type AIProvider } from './provider'; import { registerProvider, type AIProvider } from './provider';
// Dynamic import type — @github/copilot-sdk is ESM-only
type CopilotClientType = import('@github/copilot-sdk').CopilotClient;
let client: CopilotClientType | null = null;
let token: string | null = null; let token: string | null = null;
const copilotProvider: AIProvider = { const copilotProvider: AIProvider = {
@@ -8,13 +13,33 @@ const copilotProvider: AIProvider = {
async initialize(t: string): Promise<boolean> { async initialize(t: string): Promise<boolean> {
token = t; token = t;
// Actual GitHub Copilot SDK client creation will be added in US-019. try {
// For now, having a token means the provider is ready. // Stop existing client if re-initializing
return true; if (client) {
// eslint-disable-next-line @typescript-eslint/no-empty-function
await client.stop().catch(() => {});
client = null;
}
const { CopilotClient } = await import('@github/copilot-sdk');
client = new CopilotClient({
githubToken: t,
autoStart: true,
autoRestart: true,
logLevel: 'warning',
});
await client.start();
console.log('[AI] CopilotClient started successfully');
return true;
} catch (err) {
console.error('[AI] Failed to start CopilotClient:', err);
client = null;
return false;
}
}, },
isReady(): boolean { isReady(): boolean {
return token !== null; return client !== null && token !== null;
}, },
}; };
@@ -23,4 +48,16 @@ export function getCopilotToken(): string | null {
return token; return token;
} }
/** Get the CopilotClient instance (null if not initialized). */
export function getCopilotClient(): CopilotClientType | null {
return client;
}
// Clean shutdown on app quit
app.on('before-quit', () => {
if (client) {
client.stop().catch((err: unknown) => console.error('[AI] Error stopping CopilotClient:', err));
}
});
registerProvider(copilotProvider); registerProvider(copilotProvider);

77
src/main/ai/llm.ts Normal file
View File

@@ -0,0 +1,77 @@
/**
* LLM connector factory — returns a LangChain BaseChatModel for the active provider.
*
* The agent orchestration (LangGraph) is provider-independent. This module is
* the only place that knows how to create provider-specific LLM instances.
*/
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { getActiveProviderName } from './provider';
import { getToken } from './token';
// ---------------------------------------------------------------------------
// Provider-specific factory functions (lazy-loaded)
// ---------------------------------------------------------------------------
async function createOpenAIModel(token: string): Promise<BaseChatModel> {
const { ChatOpenAI } = await import('@langchain/openai');
return new ChatOpenAI({
apiKey: token,
model: 'gpt-4o-mini',
temperature: 0.3,
streaming: true,
});
}
async function createAnthropicModel(token: string): Promise<BaseChatModel> {
const { ChatAnthropic } = await import('@langchain/anthropic');
return new ChatAnthropic({
apiKey: token,
model: 'claude-sonnet-4-20250514',
temperature: 0.3,
streaming: true,
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function createCopilotModel(_token: string): Promise<BaseChatModel> {
// GitHub Copilot uses the Copilot SDK subprocess for auth and API access.
// We wrap it in a LangChain-compatible adapter.
const { ChatCopilot } = await import('./chat-copilot');
return new ChatCopilot();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
const MODEL_FACTORIES: Record<string, (token: string) => Promise<BaseChatModel>> = {
openai: createOpenAIModel,
anthropic: createAnthropicModel,
copilot: createCopilotModel,
};
/**
* Get a LangChain BaseChatModel for the currently active AI provider.
* Returns null if no provider is configured or no token is available.
*/
export async function getLLM(): Promise<BaseChatModel | null> {
const providerName = getActiveProviderName();
const factory = MODEL_FACTORIES[providerName];
if (!factory) {
console.log(`[AI] No LLM factory for provider "${providerName}"`);
return null;
}
const token = await getToken(providerName);
if (!token) {
console.log(`[AI] No token available for provider "${providerName}"`);
return null;
}
try {
return await factory(token);
} catch (err) {
console.error(`[AI] Failed to create LLM for "${providerName}":`, err);
return null;
}
}

401
src/main/ai/orchestrator.ts Normal file
View File

@@ -0,0 +1,401 @@
/**
* @Orchestrator agent — LangGraph-based intent routing.
*
* The agent logic (routing, state) lives here and is fully LLM-agnostic.
* The LLM is a swappable connector obtained via `getLLM()`.
*/
import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
import { SystemMessage, HumanMessage, type BaseMessage } from '@langchain/core/messages';
import { z } from 'zod';
import { eq, asc } from 'drizzle-orm';
import { getDb } from '../db';
import { projects, tasks, checkpoints, notes, clients } from '../db/schema';
import { getLLM } from './llm';
const AI_STREAM_CHANNEL = 'ai:stream';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface OrchestrateInput {
message: string;
context: { type: 'global' | 'project'; projectId?: string };
sender?: Electron.WebContents;
}
export interface OrchestrateResult {
response: string;
error?: string;
}
// ---------------------------------------------------------------------------
// Context assembly (DB queries — provider-independent)
// ---------------------------------------------------------------------------
function buildProjectContext(projectId: string): string {
const db = getDb();
const project = db.select().from(projects).where(eq(projects.id, projectId)).all()[0];
if (!project) return 'Project not found.';
let clientName = '';
if (project.clientId) {
const client = db.select().from(clients).where(eq(clients.id, project.clientId)).all()[0];
if (client) clientName = client.name;
}
const projectTasks = db
.select({ title: tasks.title, status: tasks.status, priority: tasks.priority, dueDate: tasks.dueDate })
.from(tasks)
.where(eq(tasks.projectId, projectId))
.orderBy(asc(tasks.createdAt))
.all();
const projectCheckpoints = db
.select({ title: checkpoints.title, date: checkpoints.date, isApproved: checkpoints.isApproved })
.from(checkpoints)
.where(eq(checkpoints.projectId, projectId))
.orderBy(asc(checkpoints.date))
.all();
const projectNotes = db
.select({ title: notes.title, content: notes.content })
.from(notes)
.where(eq(notes.projectId, projectId))
.orderBy(asc(notes.createdAt))
.all();
const lines: string[] = [
`## Project: ${project.name}`,
clientName ? `Client: ${clientName}` : '',
`Status: ${project.status ?? 'active'}`,
project.aiSummary ? `AI Summary: ${project.aiSummary}` : '',
'',
`### Tasks (${projectTasks.length})`,
...projectTasks.map((t) => {
const due = t.dueDate ? new Date(t.dueDate).toLocaleDateString() : 'no due date';
return `- [${t.status}] ${t.title} (${t.priority}, ${due})`;
}),
'',
`### Checkpoints (${projectCheckpoints.length})`,
...projectCheckpoints.map((c) => {
const approved = c.isApproved ? 'approved' : 'pending';
return `- ${c.title}${new Date(c.date).toLocaleDateString()} (${approved})`;
}),
'',
`### Notes (${projectNotes.length})`,
...projectNotes.map((n) => {
const excerpt = n.content.length > 500 ? n.content.slice(0, 500) + '…' : n.content;
return `#### ${n.title}\n${excerpt}`;
}),
];
return lines.filter(Boolean).join('\n');
}
function buildGlobalContext(): string {
const db = getDb();
const allProjects = db
.select({ id: projects.id, name: projects.name, status: projects.status })
.from(projects)
.where(eq(projects.status, 'active'))
.orderBy(asc(projects.name))
.all();
const allTasks = db.select().from(tasks).all();
const todoCount = allTasks.filter((t) => t.status === 'todo').length;
const inProgressCount = allTasks.filter((t) => t.status === 'in_progress').length;
const doneCount = allTasks.filter((t) => t.status === 'done').length;
const now = Date.now();
const weekFromNow = now + 7 * 24 * 60 * 60 * 1000;
const upcomingTasks = allTasks
.filter((t) => t.dueDate && t.dueDate >= now && t.dueDate <= weekFromNow && t.status !== 'done')
.sort((a, b) => (a.dueDate ?? 0) - (b.dueDate ?? 0));
const projectMap = new Map(allProjects.map((p) => [p.id, p.name]));
const lines: string[] = [
`## Workspace Overview`,
`Active projects: ${allProjects.length}`,
`Tasks: ${allTasks.length} total (${todoCount} todo, ${inProgressCount} in progress, ${doneCount} done)`,
'',
`### Active Projects`,
...allProjects.map((p) => `- ${p.name}`),
'',
`### Tasks Due This Week (${upcomingTasks.length})`,
...upcomingTasks.map((t) => {
const projectName = t.projectId ? (projectMap.get(t.projectId) ?? 'Unknown') : 'No project';
const due = t.dueDate ? new Date(t.dueDate).toLocaleDateString() : '';
return `- ${t.title} [${projectName}] — due ${due}`;
}),
];
return lines.join('\n');
}
// ---------------------------------------------------------------------------
// System prompts
// ---------------------------------------------------------------------------
function makeProjectAgentPrompt(contextData: string): string {
return `You are @ProjectAgent, an AI assistant specialized in a specific project within Adiuva.
You have access to the following project data:
${contextData}
Answer the user's question based on this project context. Be concise and helpful.
When referencing tasks, notes, or checkpoints, mention them by name.
If you don't have enough information, say so.`;
}
function makeGeneralAgentPrompt(contextData: string): string {
return `You are @GeneralAgent, an AI assistant for the Adiuva workspace.
You have access to the following workspace data:
${contextData}
Help the user with their question based on this workspace context. Provide concise, actionable answers.
When discussing tasks or projects, reference them by name.`;
}
function makeKnowledgeAgentPrompt(contextData: string): string {
return `You are @KnowledgeAgent, an AI assistant that searches across all project knowledge in Adiuva.
You have access to the following workspace data:
${contextData}
Note: Semantic vector search is not yet available. Answer based on the workspace summary data above.
If the user asks about specific note contents that aren't included here, let them know that full cross-project search will be available soon.`;
}
// ---------------------------------------------------------------------------
// LangGraph State
// ---------------------------------------------------------------------------
const RouteSchema = z.object({
route: z.enum(['project', 'knowledge', 'general']).describe(
'Which specialist agent should handle this request',
),
});
const OrchestratorState = Annotation.Root({
/** The user's original message */
userMessage: Annotation<string>(),
/** Chat context (global vs project-scoped) */
chatContext: Annotation<{ type: 'global' | 'project'; projectId?: string }>(),
/** The route chosen by the orchestrator */
route: Annotation<'project' | 'knowledge' | 'general'>(),
/** Messages for the specialist agent */
messages: Annotation<BaseMessage[]>({
reducer: (existing, incoming) =>
Array.isArray(incoming) ? existing.concat(incoming) : existing.concat([incoming]),
default: () => [],
}),
/** The final response text */
response: Annotation<string>(),
});
type State = typeof OrchestratorState.State;
// ---------------------------------------------------------------------------
// Graph nodes
// ---------------------------------------------------------------------------
/** Node 1: Classify intent using structured output */
async function classifyIntent(state: State): Promise<Partial<State>> {
const llm = await getLLM();
if (!llm) throw new Error('AI provider not configured. Please add your token in Settings.');
const routerLLM = llm.withStructuredOutput(RouteSchema);
const result = await routerLLM.invoke([
new SystemMessage(
`You are a routing classifier for Adiuva, a project management workspace.
Classify the user's message into one of these categories:
- "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),
]);
return { route: result.route };
}
/** Node 2a: Project agent — answer project-scoped questions */
async function projectAgent(state: State): Promise<Partial<State>> {
const llm = await getLLM();
if (!llm) throw new Error('AI provider not configured.');
const projectId = state.chatContext.projectId;
const contextData = projectId ? buildProjectContext(projectId) : buildGlobalContext();
const systemPrompt = projectId
? makeProjectAgentPrompt(contextData)
: makeGeneralAgentPrompt(contextData);
const response = await llm.invoke([
new SystemMessage(systemPrompt),
new HumanMessage(state.userMessage),
]);
const content = typeof response.content === 'string' ? response.content : '';
return {
messages: [response],
response: content,
};
}
/** Node 2b: Knowledge agent — cross-project search */
async function knowledgeAgent(state: State): Promise<Partial<State>> {
const llm = await getLLM();
if (!llm) throw new Error('AI provider not configured.');
const contextData = buildGlobalContext();
const systemPrompt = makeKnowledgeAgentPrompt(contextData);
const response = await llm.invoke([
new SystemMessage(systemPrompt),
new HumanMessage(state.userMessage),
]);
const content = typeof response.content === 'string' ? response.content : '';
return {
messages: [response],
response: content,
};
}
/** Node 2c: General agent — workspace-wide questions */
async function generalAgent(state: State): Promise<Partial<State>> {
const llm = await getLLM();
if (!llm) throw new Error('AI provider not configured.');
const contextData = buildGlobalContext();
const systemPrompt = makeGeneralAgentPrompt(contextData);
const response = await llm.invoke([
new SystemMessage(systemPrompt),
new HumanMessage(state.userMessage),
]);
const content = typeof response.content === 'string' ? response.content : '';
return {
messages: [response],
response: content,
};
}
/** Routing function: reads state.route and returns the next node name */
function routeDecision(state: State): string {
switch (state.route) {
case 'project': return 'projectAgent';
case 'knowledge': return 'knowledgeAgent';
case 'general': return 'generalAgent';
default: return 'generalAgent';
}
}
// ---------------------------------------------------------------------------
// Compile the graph (singleton, reused across calls)
// ---------------------------------------------------------------------------
function buildGraph() {
return new StateGraph(OrchestratorState)
.addNode('classifyIntent', classifyIntent)
.addNode('projectAgent', projectAgent)
.addNode('knowledgeAgent', knowledgeAgent)
.addNode('generalAgent', generalAgent)
.addEdge(START, 'classifyIntent')
.addConditionalEdges('classifyIntent', routeDecision, [
'projectAgent', 'knowledgeAgent', 'generalAgent',
])
.addEdge('projectAgent', END)
.addEdge('knowledgeAgent', END)
.addEdge('generalAgent', END)
.compile();
}
let compiledGraph: ReturnType<typeof buildGraph> | null = null;
function getGraph() {
if (!compiledGraph) {
compiledGraph = buildGraph();
}
return compiledGraph;
}
// ---------------------------------------------------------------------------
// Streaming helper
// ---------------------------------------------------------------------------
function sendStreamChunk(sender: Electron.WebContents | undefined, token: string, done: boolean): void {
if (!sender || sender.isDestroyed()) return;
sender.send(AI_STREAM_CHANNEL, { token, done });
}
// ---------------------------------------------------------------------------
// Orchestrate (public entry point)
// ---------------------------------------------------------------------------
export async function orchestrate(input: OrchestrateInput): Promise<OrchestrateResult> {
const { message, context, sender } = input;
// Quick check: is an LLM available?
const llm = await getLLM();
if (!llm) {
return { response: '', error: 'AI provider not configured. Please add your token in Settings.' };
}
try {
const graph = getGraph();
// Use streaming to push tokens to the renderer in real-time
const stream = await graph.stream(
{
userMessage: message,
chatContext: context,
route: 'general' as const,
response: '',
},
{ streamMode: 'messages' as const },
);
let fullResponse = '';
for await (const [chunk, metadata] of stream) {
// Only stream tokens from the specialist agent nodes (not the classifier)
if (
metadata.langgraph_node !== 'classifyIntent' &&
chunk.content &&
typeof chunk.content === 'string'
) {
fullResponse += chunk.content;
sendStreamChunk(sender, chunk.content, false);
}
}
// Signal stream completion
sendStreamChunk(sender, '', true);
return { response: fullResponse };
} catch (err) {
sendStreamChunk(sender, '', true);
const errMsg = err instanceof Error ? err.message : String(err);
if (errMsg.includes('401') || errMsg.includes('403') || errMsg.includes('auth') || errMsg.includes('Unauthorized')) {
return { response: '', error: 'Authentication failed. Please check your token in Settings.' };
}
if (errMsg.includes('timeout') || errMsg.includes('Timeout')) {
return { response: '', error: 'Request timed out. Please try again.' };
}
return { response: '', error: errMsg };
}
}

View File

@@ -16,6 +16,12 @@ import {
export const IPC_CHANNEL = 'trpc'; export const IPC_CHANNEL = 'trpc';
/** Context passed to every tRPC procedure via the IPC bridge. */
export type TRPCContext = {
/** The IPC sender — available for streaming chunks back to the renderer. */
sender?: Electron.WebContents;
};
interface IPCRequest { interface IPCRequest {
method: 'request'; method: 'request';
operation: { operation: {
@@ -57,7 +63,7 @@ export function createIPCHandler<TRouter extends AnyRouter>({
router, router,
path, path,
getRawInput: async () => rawInput, getRawInput: async () => rawInput,
ctx: {}, ctx: { sender: event.sender } satisfies TRPCContext,
type, type,
signal: undefined as unknown as AbortSignal, signal: undefined as unknown as AbortSignal,
batchIndex: 0, batchIndex: 0,

View File

@@ -6,8 +6,10 @@ import { getDb } from '../db';
import { clients, projects, tasks, checkpoints, notes, taskComments } from '../db/schema'; import { clients, projects, tasks, checkpoints, notes, taskComments } from '../db/schema';
import { getStore } from '../store'; import { getStore } from '../store';
import { saveTokenAndInit, hasActiveToken } from '../ai/provider'; import { saveTokenAndInit, hasActiveToken } from '../ai/provider';
import { orchestrate } from '../ai/orchestrator';
import type { TRPCContext } from '../ipc';
const t = initTRPC.create(); const t = initTRPC.context<TRPCContext>().create();
const router = t.router; const router = t.router;
const publicProcedure = t.procedure; const publicProcedure = t.procedure;
@@ -492,7 +494,18 @@ const aiRouter = router({
projectId: z.string().optional(), projectId: z.string().optional(),
}), }),
})) }))
.mutation(() => ({ response: '' })), .mutation(async ({ input, ctx }) => {
try {
return await orchestrate({
message: input.message,
context: input.context,
sender: ctx.sender,
});
} catch (err) {
const msg = err instanceof Error ? err.message : 'Unknown error';
return { response: '', error: msg };
}
}),
setToken: publicProcedure setToken: publicProcedure
.input(z.object({ token: z.string() })) .input(z.object({ token: z.string() }))
.mutation(async ({ input }) => { .mutation(async ({ input }) => {

View File

@@ -18,3 +18,16 @@ contextBridge.exposeInMainWorld('electronTRPC', {
}; };
}, },
}); });
const AI_STREAM_CHANNEL = 'ai:stream';
contextBridge.exposeInMainWorld('electronAI', {
/** Subscribe to AI streaming chunks. Returns an unsubscribe function. */
onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => {
const handler = (_event: Electron.IpcRendererEvent, data: { token: string; done: boolean }) => cb(data);
ipcRenderer.on(AI_STREAM_CHANNEL, handler);
return () => {
ipcRenderer.removeListener(AI_STREAM_CHANNEL, handler);
};
},
});

View File

@@ -13,9 +13,14 @@ interface ElectronTRPC {
onMessage: (cb: (data: unknown) => void) => (() => void) | void; onMessage: (cb: (data: unknown) => void) => (() => void) | void;
} }
interface ElectronAI {
onStreamChunk: (cb: (data: { token: string; done: boolean }) => void) => () => void;
}
declare global { declare global {
interface Window { interface Window {
electronTRPC: ElectronTRPC; electronTRPC: ElectronTRPC;
electronAI: ElectronAI;
} }
} }

View File

@@ -5,7 +5,19 @@ export default defineConfig({
build: { build: {
rollupOptions: { rollupOptions: {
// Externalize native Node modules — they're rebuilt by electron-forge // Externalize native Node modules — they're rebuilt by electron-forge
external: ['better-sqlite3', 'keytar'], external: [
'better-sqlite3',
'keytar',
'@github/copilot-sdk',
'@github/copilot',
// LangChain — externalize to avoid bundling Node.js-specific code
'@langchain/core',
'@langchain/langgraph',
'@langchain/openai',
'@langchain/anthropic',
'@langchain/langgraph-checkpoint',
'@langchain/langgraph-sdk',
],
output: { output: {
entryFileNames: 'main.js', entryFileNames: 'main.js',
}, },