feat: Premium UI overhaul, brain management, history persistence, and IME fix

This commit is contained in:
Wonseok Jung
2026-04-28 00:09:39 +09:00
parent 42ca873d45
commit 72666ea0bc
4 changed files with 554 additions and 866 deletions
+16 -488
View File
@@ -1,17 +1,18 @@
{
"name": "g1nation",
"version": "2.2.46",
"version": "2.2.47",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "g1nation",
"version": "2.2.46",
"version": "2.2.47",
"license": "MIT",
"dependencies": {
"jsdom": "^29.0.2"
"marked": "^18.0.2"
},
"devDependencies": {
"@types/marked": "^5.0.2",
"@types/node": "18.x",
"@types/vscode": "^1.80.0",
"@vercel/ncc": "^0.38.4",
@@ -22,188 +23,6 @@
"vscode": "^1.80.0"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "5.1.10",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.10.tgz",
"integrity": "sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^3.1.1",
"@csstools/css-color-parser": "^4.0.2",
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/dom-selector": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.9.tgz",
"integrity": "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/nwsapi": "^2.3.9",
"bidi-js": "^1.0.3",
"css-tree": "^3.2.1",
"is-potential-custom-element-name": "^1.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/@asamuzakjp/nwsapi": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"license": "MIT"
},
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
"license": "MIT",
"dependencies": {
"css-tree": "^3.0.0"
},
"bin": {
"specificity": "bin/cli.js"
}
},
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@csstools/css-calc": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz",
"integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz",
"integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^6.0.2",
"@csstools/css-calc": "^3.1.1"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^4.0.0",
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^4.0.0"
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz",
"integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"peerDependencies": {
"css-tree": "^3.2.1"
},
"peerDependenciesMeta": {
"css-tree": {
"optional": true
}
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
@@ -646,22 +465,12 @@
"node": ">=18"
}
},
"node_modules/@exodus/bytes": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
"license": "MIT",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
"@noble/hashes": "^1.8.0 || ^2.0.0"
},
"peerDependenciesMeta": {
"@noble/hashes": {
"optional": true
}
}
"node_modules/@types/marked": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-OucS4KMHhFzhz27KxmWg7J+kIYqyqoW5kdIEI319hqARQQUTqhao3M/F+uFnDXD0Rg72iDDZxZNxq5gvctmLlg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "18.19.130",
@@ -690,59 +499,6 @@
"ncc": "dist/ncc/cli.js"
}
},
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
"license": "MIT",
"dependencies": {
"require-from-string": "^2.0.2"
}
},
"node_modules/css-tree": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.27.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/data-urls": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^16.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
@@ -785,176 +541,16 @@
"@esbuild/win32-x64": "0.28.0"
}
},
"node_modules/html-encoding-sniffer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
"node_modules/marked": {
"version": "18.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.2.tgz",
"integrity": "sha512-NsmlUYBS/Zg57rgDWMYdnre6OTj4e+qq/JS2ot3KrYLSoHLw+sDu0Nm1ZGpRgYAq6c+b1ekaY5NzVchMCQnzcg==",
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.6.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/jsdom": {
"version": "29.0.2",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.2.tgz",
"integrity": "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^5.1.5",
"@asamuzakjp/dom-selector": "^7.0.6",
"@bramus/specificity": "^2.4.2",
"@csstools/css-syntax-patches-for-csstree": "^1.1.1",
"@exodus/bytes": "^1.15.0",
"css-tree": "^3.2.1",
"data-urls": "^7.0.0",
"decimal.js": "^10.6.0",
"html-encoding-sniffer": "^6.0.0",
"is-potential-custom-element-name": "^1.0.1",
"lru-cache": "^11.2.7",
"parse5": "^8.0.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^6.0.1",
"undici": "^7.24.5",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^8.0.1",
"whatwg-mimetype": "^5.0.0",
"whatwg-url": "^16.0.1",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/lru-cache": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.3.tgz",
"integrity": "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/mdn-data": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
"license": "CC0-1.0"
},
"node_modules/parse5": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tldts": {
"version": "7.0.28",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.28.tgz",
"integrity": "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==",
"license": "MIT",
"dependencies": {
"tldts-core": "^7.0.28"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "7.0.28",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.28.tgz",
"integrity": "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==",
"license": "MIT"
},
"node_modules/tough-cookie": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^7.0.5"
"marked": "bin/marked.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=20"
"node": ">= 20"
}
},
"node_modules/typescript": {
@@ -971,80 +567,12 @@
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
"integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/webidl-conversions": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/whatwg-url": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
"license": "MIT",
"dependencies": {
"@exodus/bytes": "^1.11.0",
"tr46": "^6.0.0",
"webidl-conversions": "^8.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlchars": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"license": "MIT"
}
}
}
+3 -1
View File
@@ -2,7 +2,7 @@
"name": "g1nation",
"displayName": "G1nation",
"description": "100% local AI coding agent for VS Code. Create files, edit code, run commands, and work offline with Ollama or LM Studio.",
"version": "2.2.46",
"version": "2.2.57",
"publisher": "connectailab",
"license": "MIT",
"icon": "assets/icon.png",
@@ -180,6 +180,7 @@
"pretest": "npm run compile"
},
"devDependencies": {
"@types/marked": "^5.0.2",
"@types/node": "18.x",
"@types/vscode": "^1.80.0",
"@vercel/ncc": "^0.38.4",
@@ -187,5 +188,6 @@
"typescript": "^5.1.3"
},
"dependencies": {
"marked": "^18.0.2"
}
}
+43 -31
View File
@@ -465,25 +465,32 @@ export class AgentExecutor {
return [
`제가 실제로 \`${projectPath}\` 폴더를 읽고 1차 분석했습니다.`,
'',
'**제품 설명**',
pkg?.name
? `- 이 프로젝트는 \`${pkg.name}\`${pkg.description ? ` (${pkg.description})` : ''}로 식별됩니다.`
: '- `package.json` 기준의 제품명은 확인되지 않았습니다.',
readmeSummary ? `- README 기준 핵심 설명: ${readmeSummary}` : '- README 기반 제품 설명은 부족합니다. 제품 소개 문서를 보강하는 편이 좋습니다.',
stack.length ? `- 감지된 기술 스택: ${stack.join(', ')}` : '- 기술 스택은 파일 구조만으로는 명확하지 않습니다.',
'### 📋 제품 개요',
'| 항목 | 내용 |',
'| :--- | :--- |',
pkg?.name ? `| **제품명** | \`${pkg.name}\` |` : '| **제품명** | 식별되지 않음 |',
pkg?.description ? `| **설명** | ${pkg.description} |` : '| **설명** | - |',
stack.length ? `| **기술 스택** | ${stack.join(', ')} |` : '| **기술 스택** | 파악 중 |',
readmeSummary ? `| **핵심 요약** | ${readmeSummary} |` : '| **핵심 요약** | README 정보 부족 |',
'',
'**설계 구조**',
topDirs.length ? topDirs.map(item => `- ${item}`).join('\n') : '- 상위 디렉구조가 단순하거나 비어 있습니다.',
entryPoints.length ? `- 주요 진입점 후보: ${entryPoints.join(', ')}` : '- 명확한 앱 진입점은 아직 식별되지 않았습니다.',
configFiles.length ? `- 주요 설정 파일: ${configFiles.slice(0, 12).join(', ')}` : '- 주요 설정 파일이 많지 않습니다.',
'### 🏗️ 설계 구조',
'| 디렉| 파일 수 |',
'| :--- | :--- |',
...(topDirs.length ? topDirs : ['| - | - |']),
'',
'**코드 리뷰 관점의 1차 소견**',
reviewFindings.join('\n'),
'**주요 진입점 및 설정**',
entryPoints.length ? `- 진입점: ${entryPoints.map(e => `\`${e}\``).join(', ')}` : '- 명확한 진입점 미식별',
configFiles.length ? `- 설정: ${configFiles.slice(0, 8).map(f => `\`${f}\``).join(', ')}` : '- 주요 설정 파일 없음',
'',
'**다음에 더 깊게 볼 부분**',
'- 실제 비즈니스 플로우는 `src`, `app`, `lib`, `components` 내부 핵심 파일을 2차로 읽어야 정확히 판단할 수 있습니다.',
'- 지금 단계에서는 “아무것도 안 하고 말만 하는” 답변이 아니라, 실제 파일 시스템을 기준으로 프로젝트 형태를 먼저 파악한 결과입니다.',
'- 원하시면 다음 턴에서 제가 핵심 소스 파일을 더 읽어 아키텍처 다이어그램 수준으로 이어서 정리하겠습니다.'
'### 🔍 코드 리뷰 1차 소견',
...reviewFindings.map(f => f.startsWith('-') ? f : `- ${f}`),
'',
'### 🚀 향후 분석 제안',
'1. **비즈니스 로직 분석**: `src`, `app`, `lib` 내부의 핵심 비즈니스 흐름 파악',
'2. **아키텍처 시각화**: 파일 간 의존성 및 데이터 흐름 다이어그램 작성',
'3. **상세 코드 리뷰**: 특정 파일이나 기능에 대한 심층 분석',
'',
'*분석을 계속하시려면 궁금한 파일이나 모듈을 말씀해 주세요.*'
].join('\n');
}
@@ -521,7 +528,7 @@ export class AgentExecutor {
.slice(0, 12)
.map(entry => {
const count = this.collectProjectFiles(path.join(projectPath, entry.name), 200, projectPath).length;
return `\`${entry.name}/\`:${count}파일`;
return `| \`${entry.name}/\` |${count}|`;
});
}
@@ -638,27 +645,32 @@ export class AgentExecutor {
.map((entry) => {
const dirPath = path.join(brainDir, entry.name);
const count = findBrainFiles(dirPath).length;
return `- ${entry.name}/: ${count}개 문서`;
return `| \`${entry.name}/\` | ${count}개 문서 |`;
});
const fileSummaries = entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
.slice(0, 8)
.slice(0, 10)
.map((entry) => `- ${entry.name}`);
const sampleFiles = files
.slice(0, 10)
.map((file) => `- ${path.relative(brainDir, file)}`);
const sections = [
`현재 연결된 제2뇌 **${activeBrain.name}**입니다.`,
`경로: \`${brainDir}\``,
`총 Markdown 지식 문서: **${files.length}개**`,
activeBrain.description ? `설명: ${activeBrain.description}` : '',
directorySummaries.length ? `\n상위 폴더 구조는 이렇게 보입니다.\n${directorySummaries.join('\n')}` : '',
fileSummaries.length ? `\n루트에 있는 주요 문서는 이런 것들이 있습니다.\n${fileSummaries.join('\n')}` : '',
sampleFiles.length ? `\n제가 우선 참고할 수 있는 샘플 문서는 다음과 같습니다.\n${sampleFiles.join('\n')}` : '',
`\n이 자료는 충분히 도움이 됩니다. 앞으로 질문을 받으면 먼저 이 제2뇌에서 관련 기준이나 맥락을 찾고, 부족할 때만 제 일반 지식으로 보완하겠습니다.`
`현재 연결된 제2뇌 **${activeBrain.name}**의 개요입니다.`,
'',
'### 🧠 지식 베이스 현황',
'| 항목 | 정보 |',
'| :--- | :--- |',
`| **위치** | \`${brainDir}\` |`,
`| **문서 총계** | **${files.length}개** |`,
activeBrain.description ? `| **설명** | ${activeBrain.description} |` : '| **설명** | 설정된 설명 없음 |',
'',
'### 📂 주요 카테고리',
'| 폴더명 | 문서 수 |',
'| :--- | :--- |',
...(directorySummaries.length ? directorySummaries : ['| - | - |']),
'',
fileSummaries.length ? `### 📄 최근/주요 문서\n${fileSummaries.join('\n')}` : '',
'',
'이 자료들을 기반으로 답변을 최적화하겠습니다. 특정 주제에 대해 깊이 있는 분석이 필요하시면 말씀해 주세요.'
].filter(Boolean);
return sections.join('\n');
+492 -346
View File
@@ -118,6 +118,9 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
await this.syncBrain();
await this._sendBrainStatus();
break;
case 'addBrain':
await this._addBrainProfile();
break;
case 'setBrainProfile':
await this._setActiveBrainProfile(data.id);
break;
@@ -644,367 +647,510 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
private _getHtml(webview: vscode.Webview): string {
return `<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>G1nation</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#000000;--bg2:#050505;--surface:rgba(0,18,5,.75);--surface2:rgba(0,35,10,.6);
--border:rgba(255,255,255,.08);--border2:rgba(255,255,255,.12);
--text:#A1A1AA;--text-bright:#FFFFFF;--text-dim:#71717A;
--accent:#00FF41;--accent2:#008F11;--accent3:#00FF41;
--accent-glow:rgba(0,255,65,.25);--accent2-glow:rgba(0,143,17,.2);
--input-bg:rgba(0,10,2,.9);--code-bg:#020502;
--green:#00FF41;--yellow:#ffab40;--cyan:#00e5ff;--red:#ff5252;
}
body.vscode-light {
--bg:#fafafa;--bg2:#ffffff;--surface:rgba(255,255,255,.8);--surface2:rgba(240,240,245,.8);
--border:rgba(0,0,0,.08);--border2:rgba(0,0,0,.15);
--text:#454555;--text-bright:#111118;--text-dim:#888899;
--accent-glow:rgba(124,106,255,.1);--accent2-glow:rgba(224,64,251,.08);
--input-bg:rgba(255,255,255,.9);--code-bg:#f5f5f7;
}
html,body{height:100%;font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);display:flex;flex-direction:column;overflow:hidden;min-height:0}
body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%;background:radial-gradient(ellipse at 20% 50%,rgba(124,106,255,.06) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(224,64,251,.04) 0%,transparent 50%),radial-gradient(ellipse at 50% 80%,rgba(0,229,255,.03) 0%,transparent 50%);animation:aurora 20s ease-in-out infinite;z-index:0;pointer-events:none}
@keyframes aurora{0%,100%{transform:translate(0,0) rotate(0deg)}33%{transform:translate(2%,-1%) rotate(.5deg)}66%{transform:translate(-1%,2%) rotate(-.5deg)}}
.header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:rgba(10,10,12,.8);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);flex-shrink:0;position:relative;z-index:10}
.header::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent 5%,var(--accent) 30%,var(--accent2) 50%,var(--accent3) 70%,transparent 95%);opacity:.5;animation:headerGlow 4s ease-in-out infinite alternate}
@keyframes headerGlow{0%{opacity:.3}100%{opacity:.6}}
.thinking-bar{height:2px;background:transparent;position:relative;overflow:hidden;flex-shrink:0;z-index:10}
.thinking-bar.active{background:rgba(124,106,255,.1)}
.thinking-bar.active::after{content:'';position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),var(--accent3),transparent);animation:thinkSlide 1.5s ease-in-out infinite}
@keyframes thinkSlide{0%{left:-40%}100%{left:100%}}
.brain-strip{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 14px;border-bottom:1px solid var(--border);background:rgba(0,10,2,.64);z-index:8;position:relative}
.brain-strip-main{font-size:11px;color:var(--accent);font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.brain-strip-sub{font-size:10px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}
.header-left{display:flex;align-items:center;gap:8px}
.logo{width:26px;height:26px;border-radius:6px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);box-shadow:0 0 15px rgba(0,255,65,.15);animation:logoPulse 3s ease-in-out infinite;position:relative;text-shadow:0 0 8px var(--accent)}
@keyframes logoPulse{0%,100%{box-shadow:0 0 10px rgba(0,255,65,.1)}50%{box-shadow:0 0 25px rgba(0,255,65,.3)}}
.brand{font-weight:800;font-size:14px;color:var(--text-bright);letter-spacing:-.5px;background:linear-gradient(135deg,#fff 40%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.header-right{display:flex;align-items:center;gap:5px;flex-wrap:wrap;justify-content:flex-end}
.select-wrap{display:flex;align-items:center;gap:5px}
.select-wrap select{max-width:140px}
.brain-meta{font-size:10px;color:var(--text-dim);max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.history-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);backdrop-filter:blur(15px);z-index:100;display:none;flex-direction:column;padding:20px;animation:fadeIn 0.3s ease-out}
.history-overlay.visible{display:flex}
.history-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
.history-title{font-size:18px;font-weight:bold;color:var(--accent)}
.history-list{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:10px}
.history-item{background:rgba(255,255,255,0.05);border:1px solid var(--border);padding:12px;border-radius:10px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:all 0.2s}
.history-item:hover{border-color:var(--accent);background:rgba(0,255,65,0.05)}
.history-item-info{display:flex;flex-direction:column;gap:4px;flex:1}
.history-item-title{font-weight:600;font-size:12px;color:var(--text-bright)}
.history-item-date{font-size:10px;color:var(--text-dim)}
.history-del-btn{background:transparent;border:none;color:var(--text-dim);cursor:pointer;padding:8px;font-size:14px;transition:color 0.2s}
.history-del-btn:hover{color:var(--red)}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
select{background:rgba(22,22,28,.9);color:var(--text-bright);border:1px solid var(--border2);padding:5px 8px;border-radius:8px;font-size:10px;cursor:pointer;outline:none;max-width:120px;transition:all .3s;backdrop-filter:blur(8px)}
select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)}
.btn-icon{background:rgba(22,22,28,.7);border:1px solid var(--border2);color:var(--text-dim);width:28px;height:28px;border-radius:8px;font-size:13px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s;backdrop-filter:blur(8px);position:relative;overflow:hidden}
.btn-icon:hover{color:var(--text-bright);border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 15px var(--accent-glow)}
.chat{flex:1;overflow-y:auto;padding:16px 14px;display:flex;flex-direction:column;gap:16px;position:relative;z-index:1;min-height:0}
.chat::-webkit-scrollbar{width:2px}.chat::-webkit-scrollbar-track{background:transparent}.chat::-webkit-scrollbar-thumb{background:var(--accent);border-radius:2px;opacity:.5}
.msg{display:flex;flex-direction:column;gap:5px;animation:msgIn .5s cubic-bezier(.16,1,.3,1)}
.msg-head{display:flex;align-items:center;gap:7px;font-weight:600;font-size:11px;color:var(--text)}
.msg-time{font-weight:400;font-size:9px;color:var(--text-dim);margin-left:auto;opacity:.6}
.av{width:22px;height:22px;border-radius:7px;display:flex;align-items:center;justify-content:center;font-size:11px;flex-shrink:0}
.av-user{background:var(--surface2);color:var(--text);border:1px solid var(--border2)}
.av-ai{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 0 10px rgba(124,106,255,.3)}
.msg-body{padding-left:29px;line-height:1.75;color:var(--text);white-space:pre-wrap;word-break:break-word;font-size:13px}
.msg-user .msg-body{background:var(--surface);border:1px solid var(--border2);border-radius:14px;padding:10px 14px;margin-left:29px;color:var(--text-bright);backdrop-filter:blur(8px)}
.msg-body pre{background:var(--code-bg);border:1px solid var(--border2);border-radius:10px;padding:14px 16px;overflow-x:auto;margin:8px 0;font-size:12px;line-height:1.6;color:#c9d1d9;position:relative}
.msg-body code{font-family:'SF Mono','JetBrains Mono','Fira Code','Menlo',monospace;font-size:11.5px}
.msg-body :not(pre)>code{background:rgba(124,106,255,.1);color:var(--accent);padding:2px 7px;border-radius:5px;border:1px solid rgba(124,106,255,.15)}
.code-wrap{position:relative}
.code-lang{position:absolute;top:0;left:14px;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;padding:2px 10px;border-radius:0 0 6px 6px;font-size:9px;font-family:'SF Mono',monospace;text-transform:uppercase;letter-spacing:.5px;font-weight:600}
.copy-btn{position:absolute;top:8px;right:8px;background:var(--surface2);border:1px solid var(--border2);color:var(--text-dim);padding:4px 12px;border-radius:6px;font-size:10px;cursor:pointer;opacity:0;transition:all .3s;z-index:1;backdrop-filter:blur(8px)}
.code-wrap:hover .copy-btn{opacity:1}.copy-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)}
.welcome{text-align:center;padding:0 20px 20px;position:relative}
.welcome-logo{width:56px;height:56px;border-radius:16px;margin:0 auto 16px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:32px;color:var(--accent);box-shadow:inset 0 0 15px rgba(0,255,65,.1), 0 0 30px rgba(0,255,65,.2);animation:welcomeFloat 4s ease-in-out infinite;position:relative;text-shadow:0 0 15px var(--accent)}
@keyframes welcomeFloat{0%,100%{transform:translateY(0) scale(1)}50%{transform:translateY(-6px) scale(1.03)}}
.welcome-title{font-size:22px;font-weight:900;letter-spacing:-1px;color:var(--text-bright);margin-bottom:8px}
.welcome-sub{color:var(--text-dim);font-size:12px;line-height:1.7;margin-bottom:18px;letter-spacing:-.2px}
.loading-wrap{padding-left:29px;padding-top:6px;display:flex;align-items:center;gap:10px}
.loading-dots{display:flex;gap:4px}
.loading-dots span{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:dotBounce 1.4s ease-in-out infinite}
.loading-dots span:nth-child(2){animation-delay:.2s;background:var(--accent2)}
.loading-dots span:nth-child(3){animation-delay:.4s;background:var(--accent3)}
@keyframes dotBounce{0%,80%,100%{transform:scale(.6);opacity:.4}40%{transform:scale(1.2);opacity:1}}
.loading-text{font-size:11px;color:var(--text-dim);animation:pulse 2s ease-in-out infinite;letter-spacing:.3px}
.auto-status{margin:12px 0 4px 29px;padding:10px 12px;border:1px solid rgba(255,255,255,.16);border-radius:10px;background:rgba(0,255,65,.08);color:#fff;font-size:13px;font-weight:700;line-height:1.5;box-shadow:0 0 18px rgba(0,255,65,.12)}
.input-wrap{padding:8px 14px 14px;flex-shrink:0;position:relative;z-index:1}
.input-box{background:var(--input-bg);border:1px solid var(--border2);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:all .3s;position:relative;backdrop-filter:blur(12px)}
.input-box:focus-within{border-color:var(--accent);box-shadow:0 0 24px rgba(0,255,65,.15)}
textarea{width:100%;background:transparent;border:none;color:var(--text-bright);font-family:inherit;font-size:13px;line-height:1.5;resize:none;outline:none;min-height:22px;max-height:150px}
textarea::placeholder{color:var(--text-dim)}
.input-footer{display:flex;align-items:center;justify-content:space-between}
.input-hint{font-size:10px;color:var(--text-dim);opacity:.5}
.input-btns{display:flex;gap:5px}
.send-btn{background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff;width:32px;height:32px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .2s;box-shadow:0 2px 12px rgba(124,106,255,.35);position:relative;overflow:hidden}
.send-btn:hover{transform:translateY(-2px) scale(1.05);box-shadow:0 6px 24px rgba(124,106,255,.45)}
.send-btn:active{transform:scale(.92)}.send-btn:disabled{opacity:.2;cursor:not-allowed}
.stop-btn{background:var(--red);border:none;color:#fff;width:32px;height:32px;border-radius:10px;cursor:pointer;display:none;align-items:center;justify-content:center;font-size:11px;box-shadow:0 0 12px rgba(255,82,82,.3)}
.stop-btn.visible{display:flex}
@keyframes msgIn{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
.stream-active::after{content:'';display:inline-block;width:2px;height:14px;background:var(--accent);margin-left:2px;animation:blink .6s step-end infinite;vertical-align:text-bottom;box-shadow:0 0 6px var(--accent)}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
.main-view{flex:1;display:flex;flex-direction:column;overflow:hidden;transition:all .5s cubic-bezier(.16,1,.3,1);min-height:0;max-height:100%}
body.init .main-view{justify-content:center;margin-top:-6vh}
body.init .chat{flex:0 0 auto;overflow:visible;padding-bottom:15px}
body.init .input-wrap{max-width:680px;width:100%;margin:0 auto}
.attach-btn{background:transparent;border:1px solid var(--border2);color:var(--text-dim);width:32px;height:32px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .3s;flex-shrink:0}
.attach-btn:hover{color:var(--accent);border-color:var(--accent);box-shadow:0 0 12px var(--accent-glow)}
.attach-preview{display:none;gap:6px;padding:0 0 6px;flex-wrap:wrap}
.attach-preview.visible{display:flex}
.attach-chip{display:flex;align-items:center;gap:5px;background:var(--surface2);border:1px solid var(--border2);border-radius:8px;padding:4px 10px;font-size:10px;color:var(--text)}
.attach-chip .chip-remove{cursor:pointer;color:var(--text-dim);font-size:12px;margin-left:2px}
.attach-chip .chip-remove:hover{color:var(--red)}
.attach-thumb{width:28px;height:28px;border-radius:5px;object-fit:cover;border:1px solid var(--border2)}
.regen-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;color:var(--text-dim);padding:4px 6px;border-radius:4px;font-size:11px;cursor:pointer;transition:color 0.2s;margin-top:6px;margin-left:29px;opacity:0.7}
.regen-btn:hover{color:var(--text);opacity:1}
</style></head><body class="init">
<div class="header"><div class="header-left"><div class="logo">✦</div><span class="brand">G1nation</span></div><div class="header-right"><div id="engineStatusDot" style="width:8px;height:8px;border-radius:50%;background:var(--text-dim);margin-right:-2px" title="Checking AI Engine..."></div><div class="select-wrap"><select id="modelSel" title="Active AI model"></select><button class="btn-icon" id="refreshModelsBtn" title="Refresh Models">↻</button></div><div class="select-wrap"><select id="brainSel" title="Active Second Brain"></select><button class="btn-icon" id="manageBrainsBtn" title="Add or manage brains">✎</button></div><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠<span id="brainCountBadge" style="position:absolute;top:-5px;right:-5px;background:var(--accent);color:#000;font-size:8px;padding:1px 3px;border-radius:4px;display:none;font-weight:bold">0</span></button><button class="btn-icon" id="historyBtn" title="Chat History">📜</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
<div id="historyOverlay" class="history-overlay">
<div class="history-header"><span class="history-title">Chat Sessions</span><button class="btn-icon" id="closeHistoryBtn">✕</button></div>
<div class="history-list" id="historyList"></div>
</div>
<div class="thinking-bar" id="thinkingBar"></div>
<div class="brain-strip"><div id="brainStatusInfo" class="brain-strip-main">Brain: checking...</div><div id="brainStatusMeta" class="brain-strip-sub"></div></div>
<div class="main-view" id="mainView">
<div class="chat" id="chat">
<div class="welcome">
<div class="welcome-logo">✦</div>
<div class="welcome-title">G1nation</div>
<div class="welcome-sub">Security · Optimized · Knowledge Mesh<br>Choose a brain above, then ask what it knows.</div>
</div></div>
<div class="input-wrap"><div class="input-box">
<div class="attach-preview" id="attachPreview"></div>
<textarea id="input" rows="1" placeholder="What shall we build today?"></textarea>
<div class="input-footer"><span class="input-hint">Enter to Send · Shift+Enter for New Line</span>
<div class="input-btns"><button class="attach-btn" id="attachBtn" title="Attach Files">+</button><button class="stop-btn" id="stopBtn">■</button><button class="send-btn" id="sendBtn">↑</button></div></div></div>
<input type="file" id="fileInput" multiple accept="image/*,.txt,.md,.csv,.json,.js,.ts,.jsx,.tsx,.html,.css,.py,.java,.rs,.go,.yaml,.yml,.xml,.toml,.sql,.sh" hidden></div>
</div>
<script>
try {
const vscode=acquireVsCodeApi(),chat=document.getElementById('chat'),input=document.getElementById('input'),
sendBtn=document.getElementById('sendBtn'),stopBtn=document.getElementById('stopBtn'),
modelSel=document.getElementById('modelSel'),brainSel=document.getElementById('brainSel'),refreshModelsBtn=document.getElementById('refreshModelsBtn'),manageBrainsBtn=document.getElementById('manageBrainsBtn'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
historyBtn=document.getElementById('historyBtn'),historyOverlay=document.getElementById('historyOverlay'),closeHistoryBtn=document.getElementById('closeHistoryBtn'),historyList=document.getElementById('historyList'),
internetBtn=document.getElementById('internetBtn'),attachBtn=document.getElementById('attachBtn'),fileInput=document.getElementById('fileInput'),attachPreview=document.getElementById('attachPreview'),
thinkingBar=document.getElementById('thinkingBar');
let loader=null,sending=false,pendingFiles=[],internetEnabled=false;
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>G1nation</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--bg: #0d1117;
--bg-secondary: #0d1117;
--surface: #161b22;
--border: #30363d;
--border-bright: #484f58;
--text-primary: #c9d1d9;
--text-bright: #ffffff;
--text-dim: #8b949e;
--accent: #58a6ff;
--accent-glow: rgba(88, 166, 255, 0.15);
--success: #238636;
--warning: #d29922;
--error: #f85149;
--code-bg: #161b22;
--table-header-bg: #161b22;
--table-row-hover: #21262d;
--input-bg: #0d1117;
}
historyBtn.addEventListener('click', ()=>historyOverlay.classList.add('visible'));
closeHistoryBtn.addEventListener('click', ()=>historyOverlay.classList.remove('visible'));
refreshModelsBtn.addEventListener('click', ()=>vscode.postMessage({type:'refreshModels'}));
manageBrainsBtn.addEventListener('click', ()=>vscode.postMessage({type:'manageBrains'}));
brainSel.addEventListener('change', ()=>vscode.postMessage({type:'setBrainProfile',id:brainSel.value}));
body.vscode-light {
--bg: #ffffff;
--bg-secondary: #f6f8fa;
--surface: #ffffff;
--border: #d0d7de;
--border-bright: #afb8c1;
--text-primary: #24292f;
--text-bright: #111118;
--text-dim: #57606a;
--accent: #0969da;
--accent-glow: rgba(9, 105, 218, 0.1);
--success: #1a7f37;
--warning: #9a6700;
--error: #cf222e;
--code-bg: #f6f8fa;
--table-header-bg: #f6f8fa;
--table-row-hover: #f3f4f6;
--input-bg: #ffffff;
}
internetBtn.addEventListener('click', ()=>{
internetEnabled=!internetEnabled;
internetBtn.style.opacity=internetEnabled?'1':'0.4';
internetBtn.style.filter=internetEnabled?'none':'grayscale(1)';
});
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Helvetica, Arial, sans-serif;
font-size: 13px;
line-height: 1.6;
background: var(--bg);
color: var(--text-primary);
display: flex;
flex-direction: column;
overflow: hidden;
}
vscode.postMessage({type:'getModels'});
setTimeout(()=>vscode.postMessage({type:'ready'}),300);
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px'});
function getTime(){return new Date().toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit'})}
function esc(s){const d=document.createElement('div');d.innerText=s;return d.innerHTML}
/* --- Header --- */
.header {
display: flex;
flex-direction: column;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
flex-shrink: 0;
z-index: 100;
}
function fmt(t){
let formatted = esc(t);
formatted = formatted.replace(/\`\`\`([\s\S]*?)\`\`\`/g, '<pre><code>$1</code></pre>');
formatted = formatted.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
return formatted;
}
.header-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
}
function normalizeMsgContent(content){
if(Array.isArray(content)) return content.map(part=>typeof part==='string'?part:(part?.text||part?.name||JSON.stringify(part))).join('\\n');
return String(content||'');
}
.header-controls {
display: flex;
align-items: center;
gap: 6px;
padding: 0 14px 10px;
flex-wrap: wrap;
}
function resetChatView(){
chat.innerHTML='';
streamBody=null;hideLoader();setSending(false);
document.body.classList.remove('init');
}
.brand { font-weight: 700; font-size: 14px; color: var(--text-bright); letter-spacing: -0.5px; display: flex; align-items: center; gap: 8px; }
.logo { width: 22px; height: 22px; background: var(--accent); color: #fff; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 900; }
function renderHistory(history){
resetChatView();
(history||[]).filter(m => !m.internal).forEach(m => addMsg(normalizeMsgContent(m.content), m.role === 'assistant' ? 'ai' : 'user'));
if((history||[]).length===0){
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">No messages in this session.</div></div>';
document.body.classList.add('init');
}
chat.scrollTop=chat.scrollHeight;
}
.chat {
flex: 1;
overflow-y: auto;
padding: 16px 14px;
display: flex;
flex-direction: column;
gap: 24px;
scroll-behavior: smooth;
}
function addMsg(text,role){
const isUser=role==='user',isErr=role==='error';
const el=document.createElement('div');el.className='msg'+(isUser?' msg-user':'')+(isErr?' msg-error':'');
const head=document.createElement('div');head.className='msg-head';
head.innerHTML=(isUser?'<div class="av av-user">👤</div><span>You</span>':'<div class="av av-ai">✦</div><span>G1nation</span>')+'<span class="msg-time">'+getTime()+'</span>';
const body=document.createElement('div');body.className='msg-body';
if(isUser){body.innerText=text}else{body.innerHTML=fmt(text)}
el.appendChild(head);el.appendChild(body);chat.appendChild(el);chat.scrollTop=chat.scrollHeight;
}
/* --- Messages --- */
.msg {
display: flex;
flex-direction: column;
gap: 8px;
position: relative;
animation: msgIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
function showLoader(){thinkingBar.classList.add('active')}
function hideLoader(){thinkingBar.classList.remove('active')}
function setSending(v){sending=v;sendBtn.disabled=v;stopBtn.classList.toggle('visible',v);input.disabled=v;}
@keyframes msgIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
function send(){
const text=input.value.trim();
if(!text && pendingFiles.length===0) return;
document.body.classList.remove('init');
const w=document.querySelector('.welcome');if(w)w.remove();
addMsg(text,'user');
input.value='';input.style.height='auto';setSending(true);showLoader();
vscode.postMessage({type:'prompt',value:text,model:modelSel.value,internet:internetEnabled,files:pendingFiles.length?pendingFiles:undefined});
pendingFiles=[];renderPreview();
}
.msg-head { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 11px; color: var(--text-dim); }
.av { width: 22px; height: 22px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; }
.av-user { background: var(--border); color: var(--text-primary); }
.av-ai { background: var(--accent); color: #fff; }
attachBtn.addEventListener('click',()=>fileInput.click());
fileInput.addEventListener('change',()=>{
const files=Array.from(fileInput.files);
files.forEach(file=>{
const reader=new FileReader();
reader.onload=()=>{
const base64=reader.result.split(',')[1];
pendingFiles.push({name:file.name,type:file.type,data:base64});
renderPreview();
};
reader.readAsDataURL(file);
});
});
.msg-body {
padding-left: 30px;
color: var(--text-primary);
font-size: 13.5px;
word-break: break-word;
}
function renderPreview(){
attachPreview.innerHTML='';
if(pendingFiles.length===0){attachPreview.classList.remove('visible');return;}
attachPreview.classList.add('visible');
pendingFiles.forEach((f,i)=>{
const chip=document.createElement('div');chip.className='attach-chip';
chip.innerHTML='<span>📎</span><span class="chip-name">'+f.name+'</span><span class="chip-remove">✕</span>';
chip.querySelector('.chip-remove').addEventListener('click',()=>{pendingFiles.splice(i,1);renderPreview();});
attachPreview.appendChild(chip);
});
}
.msg-user .msg-body {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px 14px;
margin-left: 30px;
white-space: pre-wrap;
}
sendBtn.addEventListener('click',send);
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
newChatBtn.addEventListener('click',()=>vscode.postMessage({type:'newChat'}));
settingsBtn.addEventListener('click',()=>vscode.postMessage({type:'openSettings'}));
brainBtn.addEventListener('click',()=>vscode.postMessage({type:'syncBrain'}));
stopBtn.addEventListener('click',()=>{vscode.postMessage({type:'stopGeneration'});hideLoader();setSending(false);});
/* --- Markdown Style --- */
.markdown-body h1, .markdown-body h2, .markdown-body h3 {
color: var(--text-bright);
margin: 1.5em 0 0.8em;
font-weight: 600;
line-height: 1.3;
}
let streamBody=null,sessionCache={};
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
case 'restoreHistory':
renderHistory(msg.value||[]);
break;
case 'sessionLoaded':
renderHistory(msg.value.history||[]);
historyOverlay.classList.remove('visible');
break;
case 'sessionList':
historyList.innerHTML='';
sessionCache={};
if(!msg.value || msg.value.length===0){
historyList.innerHTML='<div class="history-item"><div class="history-item-info"><div class="history-item-title">No saved chats yet</div><div class="history-item-date">Start a conversation and it will appear here.</div></div></div>';
break;
}
msg.value.forEach(s=>{
sessionCache[s.id]=s;
const el=document.createElement('div');el.className='history-item';
el.dataset.sessionId=s.id;
el.innerHTML='<div class="history-item-info"><div class="history-item-title">'+esc(s.title)+'</div><div class="history-item-date">'+new Date(s.timestamp).toLocaleString()+' · '+(s.messageCount||0)+' messages</div></div><button class="history-del-btn" title="Delete">🗑</button>';
el.addEventListener('click',(e)=>{
if(e.target.closest('.history-del-btn')) return;
const cached=sessionCache[s.id];
if(cached && cached.history){renderHistory(cached.history);historyOverlay.classList.remove('visible');}
vscode.postMessage({type:'loadSession',id:s.id});
.markdown-body h1 { font-size: 1.5em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; color: var(--accent); }
.markdown-body h2 { font-size: 1.3em; border-bottom: 1px solid var(--border); padding-bottom: 0.3em; }
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin: 1.2em 0;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.markdown-body th { background: var(--table-header-bg); color: var(--accent); font-weight: 600; text-align: left; padding: 10px 12px; border: 1px solid var(--border); }
.markdown-body td { padding: 8px 12px; border: 1px solid var(--border); color: var(--text-primary); }
.markdown-body tr:nth-child(even) { background: rgba(255, 255, 255, 0.02); }
.markdown-body pre { background: var(--code-bg); border: 1px solid var(--border); border-radius: 8px; padding: 14px; overflow-x: auto; margin: 12px 0; }
.markdown-body code { font-family: 'SF Mono', monospace; font-size: 11.5px; background: rgba(175, 184, 193, 0.2); padding: 0.2em 0.4em; border-radius: 4px; }
/* --- UI Elements --- */
.copy-btn {
position: absolute; top: 0; right: 0; background: var(--bg-secondary); border: 1px solid var(--border);
color: var(--text-dim); padding: 4px 10px; border-radius: 6px; font-size: 10px; cursor: pointer; opacity: 0; transition: 0.2s;
z-index: 20;
}
.msg:hover .copy-btn { opacity: 1; }
.copy-btn:hover { color: var(--text-bright); border-color: var(--accent); background: var(--accent-glow); }
.icon-btn {
background: var(--surface); border: 1px solid var(--border); color: var(--text-dim); width: 28px; height: 28px;
border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; font-size: 13px;
}
.icon-btn:hover { color: var(--text-bright); border-color: var(--accent); background: var(--accent-glow); }
.icon-btn.active { color: var(--accent); border-color: var(--accent); background: var(--accent-glow); }
select { background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); padding: 4px 8px; border-radius: 6px; font-size: 10.5px; cursor: pointer; }
/* --- Input & Attachments --- */
.input-wrap {
padding: 12px 14px 16px; background: var(--bg); border-top: 1px solid var(--border); flex-shrink: 0;
}
.input-box {
background: var(--input-bg); border: 1px solid var(--border); border-radius: 12px; padding: 10px 14px;
display: flex; flex-direction: column; gap: 8px; transition: 0.2s;
}
.input-box:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-glow); }
textarea {
width: 100%; background: transparent; border: none; color: var(--text-bright);
font-family: inherit; font-size: 13.5px; resize: none; outline: none; min-height: 24px; max-height: 160px;
}
.attachment-preview {
display: none; gap: 8px; padding-bottom: 8px; border-bottom: 1px solid var(--border); flex-wrap: wrap;
}
.attachment-preview.visible { display: flex; }
.file-chip {
display: flex; align-items: center; gap: 6px; background: var(--surface); border: 1px solid var(--border);
padding: 4px 8px; border-radius: 6px; font-size: 11px; color: var(--text-primary);
}
.file-chip .remove { cursor: pointer; color: var(--text-dim); }
.file-chip .remove:hover { color: var(--error); }
.input-footer { display: flex; align-items: center; justify-content: space-between; }
.footer-left { display: flex; align-items: center; gap: 8px; }
.send-btn {
background: var(--accent); color: #fff; border: none; padding: 6px 14px; border-radius: 6px;
font-weight: 600; font-size: 12px; cursor: pointer;
}
.send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
/* --- Overlays & Others --- */
.thinking-bar { height: 2px; background: transparent; position: relative; overflow: hidden; margin-top: -1px; }
.thinking-bar.active::after {
content: ''; position: absolute; top: 0; left: -40%; width: 40%; height: 100%;
background: linear-gradient(90deg, transparent, var(--accent), transparent); animation: think 1.5s infinite;
}
@keyframes think { 0% { left: -40%; } 100% { left: 100%; } }
.history-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8);
backdrop-filter: blur(10px); z-index: 1000; display: none; flex-direction: column; padding: 20px;
}
.history-overlay.visible { display: flex; }
.stream-active::after {
content: ''; display: inline-block; width: 6px; height: 14px; background: var(--accent);
margin-left: 4px; animation: blink 0.8s step-end infinite; vertical-align: middle;
}
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
.welcome { text-align: center; padding: 40px 20px; color: var(--text-dim); }
.welcome-logo { font-size: 48px; color: var(--accent); margin-bottom: 16px; opacity: 0.8; }
.welcome-title { font-size: 20px; font-weight: 700; color: var(--text-bright); margin-bottom: 8px; }
/* --- History List --- */
.history-item {
padding: 12px; border-radius: 8px; background: var(--surface); border: 1px solid var(--border);
margin-bottom: 10px; cursor: pointer; transition: 0.2s;
}
.history-item:hover { border-color: var(--accent); background: var(--accent-glow); }
</style>
</head>
<body>
<div class="header">
<div class="header-top">
<div class="brand"><div class="logo">✦</div> G1nation</div>
<div style="display: flex; gap: 6px;">
<button class="icon-btn" id="newChatBtn" title="New Chat">+</button>
<button class="icon-btn" id="settingsBtn" title="Settings">⚙️</button>
</div>
</div>
<div class="header-controls">
<div id="statusDot" style="width:6px; height:6px; border-radius:50%; background:var(--text-dim);"></div>
<select id="modelSel" title="Select Model"></select>
<select id="brainSel" title="Select Brain"></select>
<button class="icon-btn" id="internetBtn" title="Internet Access">🌐</button>
<button class="icon-btn" id="brainBtn" title="Sync Knowledge">🧠</button>
<button class="icon-btn" id="historyBtn" title="History">📜</button>
</div>
</div>
<div id="historyOverlay" class="history-overlay">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px;">
<h2 style="color:var(--text-bright);">Chat History</h2>
<button class="icon-btn" id="closeHistoryBtn">✕</button>
</div>
<div id="historyList" style="flex:1; overflow-y:auto;"></div>
</div>
<div class="thinking-bar" id="thinkingBar"></div>
<div class="chat" id="chat">
<div class="welcome">
<div class="welcome-logo">✦</div>
<div class="welcome-title">Welcome to G1nation</div>
<p>Your premium local AI assistant.<br>Ready to analyze projects and build reports.</p>
</div>
</div>
<div class="input-wrap">
<div class="input-box">
<div id="attachPreview" class="attachment-preview"></div>
<textarea id="input" rows="1" placeholder="Type your request..."></textarea>
<div class="input-footer">
<div class="footer-left">
<button class="icon-btn" id="attachBtn" title="Attach Files">📎</button>
<span id="statusLabel" style="font-size:10px; color:var(--text-dim);">Ready</span>
</div>
<button id="sendBtn" class="send-btn">Send</button>
</div>
</div>
<input type="file" id="fileInput" multiple hidden accept="image/*,.txt,.md,.pdf,.csv,.json,.js,.ts,.py,.java,.rs,.go">
</div>
<script>
const vscode = acquireVsCodeApi();
const chat = document.getElementById('chat');
const input = document.getElementById('input');
const sendBtn = document.getElementById('sendBtn');
const thinkingBar = document.getElementById('thinkingBar');
const statusLabel = document.getElementById('statusLabel');
const modelSel = document.getElementById('modelSel');
const brainSel = document.getElementById('brainSel');
const historyOverlay = document.getElementById('historyOverlay');
const historyList = document.getElementById('historyList');
const statusDot = document.getElementById('statusDot');
const attachBtn = document.getElementById('attachBtn');
const fileInput = document.getElementById('fileInput');
const attachPreview = document.getElementById('attachPreview');
let streamBody = null;
let internetEnabled = false;
let pendingFiles = [];
function fmt(text) { return marked.parse(text || ''); }
function copyToClipboard(text, btn) {
const textarea = document.createElement('textarea');
textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0';
document.body.appendChild(textarea); textarea.select();
try {
if (document.execCommand('copy')) {
btn.innerText = '✅ Copied!'; setTimeout(() => { btn.innerText = '📋 Copy'; }, 2000);
}
} catch (err) { console.error('Copy failed', err); }
document.body.removeChild(textarea);
}
function addMsg(text, role) {
const isUser = role === 'user';
const msgEl = document.createElement('div');
msgEl.className = 'msg ' + (isUser ? 'msg-user' : 'msg-ai');
msgEl._raw = text;
const head = document.createElement('div');
head.className = 'msg-head';
head.innerHTML = isUser ? '<div class="av av-user">U</div> You' : '<div class="av av-ai">✦</div> G1nation';
const body = document.createElement('div');
body.className = 'msg-body markdown-body';
if (isUser) {
body.innerText = text;
} else {
body.innerHTML = fmt(text);
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-btn'; copyBtn.innerText = '📋 Copy';
copyBtn.onclick = (e) => { e.stopPropagation(); copyToClipboard(msgEl._raw, copyBtn); };
msgEl.appendChild(copyBtn);
}
msgEl.appendChild(head); msgEl.appendChild(body);
chat.appendChild(msgEl); chat.scrollTop = chat.scrollHeight;
return { body, msgEl };
}
window.addEventListener('message', e => {
const msg = e.data;
switch(msg.type) {
case 'streamStart':
thinkingBar.classList.remove('active');
if (document.querySelector('.welcome')) document.querySelector('.welcome').remove();
const res = addMsg('', 'assistant');
streamBody = res.body; streamBody._parent = res.msgEl; streamBody._parent._raw = '';
streamBody.classList.add('stream-active');
break;
case 'streamChunk':
if (streamBody) {
streamBody._parent._raw += msg.value;
streamBody.innerHTML = fmt(streamBody._parent._raw);
chat.scrollTop = chat.scrollHeight;
}
break;
case 'streamEnd':
if (streamBody) streamBody.classList.remove('stream-active');
streamBody = null; sendBtn.disabled = false;
break;
case 'restoreHistory':
case 'sessionLoaded':
const history = msg.type === 'sessionLoaded' ? msg.value.history : msg.value;
if (history && history.length > 0) {
chat.innerHTML = '';
history.forEach(m => addMsg(m.content, m.role === 'assistant' ? 'assistant' : 'user'));
}
historyOverlay.classList.remove('visible');
break;
case 'clearChat':
chat.innerHTML = '<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">Welcome to G1nation</div><p>Your premium local AI assistant.<br>Ready to analyze projects and build reports.</p></div>';
break;
case 'focusInput':
input.focus();
break;
case 'modelsList':
modelSel.innerHTML = '';
msg.value.models.forEach(m => {
const o = document.createElement('option'); o.value = m; o.innerText = m;
if (m === msg.value.selected) o.selected = true;
modelSel.appendChild(o);
});
statusLabel.innerText = \`Model: \${msg.value.selected}\`;
break;
case 'brainProfiles':
brainSel.innerHTML = '';
msg.value.profiles.forEach(p => {
const o = document.createElement('option'); o.value = p.id; o.innerText = p.name;
if (p.id === msg.value.activeBrainId) o.selected = true;
brainSel.appendChild(o);
});
const addOpt = document.createElement('option');
addOpt.value = 'new'; addOpt.innerText = '+ Add New Brain...';
brainSel.appendChild(addOpt);
break;
case 'sessionList':
historyList.innerHTML = '';
msg.value.forEach(s => {
const el = document.createElement('div'); el.className = 'history-item';
el.innerHTML = \`<div style="font-weight:600; color:var(--text-bright); mb-2">\${s.title}</div><div style="font-size:10px; color:var(--text-dim)">\${new Date(s.timestamp).toLocaleString()} · \${s.messageCount} msgs</div>\`;
el.onclick = () => vscode.postMessage({ type: 'loadSession', id: s.id });
historyList.appendChild(el);
});
break;
case 'engineStatus':
statusDot.style.background = msg.value.online ? 'var(--success)' : 'var(--error)';
break;
case 'autoContinue':
statusLabel.innerText = msg.value; thinkingBar.classList.add('active');
setTimeout(() => { thinkingBar.classList.remove('active'); }, 3000);
break;
case 'error':
thinkingBar.classList.remove('active'); sendBtn.disabled = false;
addMsg(msg.value, 'error');
break;
}
});
el.querySelector('.history-del-btn').addEventListener('click',()=>{
vscode.postMessage({type:'deleteSession',id:s.id});
function renderAttachments() {
attachPreview.innerHTML = '';
if (pendingFiles.length === 0) { attachPreview.classList.remove('visible'); return; }
attachPreview.classList.add('visible');
pendingFiles.forEach((f, i) => {
const chip = document.createElement('div'); chip.className = 'file-chip';
chip.innerHTML = \`<span>📎</span> \${f.name} <span class="remove" onclick="removeFile(\${i})">✕</span>\`;
attachPreview.appendChild(chip);
});
}
window.removeFile = (i) => { pendingFiles.splice(i, 1); renderAttachments(); };
attachBtn.onclick = () => fileInput.click();
fileInput.onchange = () => {
Array.from(fileInput.files).forEach(file => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1];
pendingFiles.push({ name: file.name, type: file.type, data: base64 });
renderAttachments();
};
reader.readAsDataURL(file);
});
fileInput.value = '';
};
function send() {
const val = input.value.trim();
if (!val && pendingFiles.length === 0) return;
addMsg(val || (pendingFiles.length > 0 ? \`[Sent \${pendingFiles.length} files]\` : ''), 'user');
vscode.postMessage({ type: 'prompt', value: val, model: modelSel.value, internet: internetEnabled, files: pendingFiles.length > 0 ? pendingFiles : undefined });
input.value = ''; input.style.height = 'auto'; pendingFiles = []; renderAttachments();
sendBtn.disabled = true; thinkingBar.classList.add('active');
}
sendBtn.onclick = send;
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.isComposing) return;
e.preventDefault();
send();
}
});
historyList.appendChild(el);
});
break;
case 'streamStart':
hideLoader();
const el=document.createElement('div');el.className='msg';
el.innerHTML='<div class="msg-head"><div class="av av-ai">✦</div><span>G1nation</span><span class="msg-time">'+getTime()+'</span></div><div class="msg-body stream-active"></div>';
chat.appendChild(el);streamBody=el.querySelector('.msg-body');chat.scrollTop=chat.scrollHeight;
break;
case 'streamChunk':
if(streamBody){streamBody.innerHTML=fmt(streamBody._raw=(streamBody._raw||'')+msg.value);chat.scrollTop=chat.scrollHeight;}
break;
case 'streamEnd':
if(streamBody)streamBody.classList.remove('stream-active');
setSending(false);streamBody=null;
break;
case 'modelsList':
modelSel.innerHTML='';
(msg.value.models||[]).forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
if(msg.value.selected) modelSel.value=msg.value.selected;
break;
case 'brainProfiles':
brainSel.innerHTML='';
(msg.value.profiles||[]).forEach(profile=>{
const o=document.createElement('option');o.value=profile.id;o.textContent=profile.name;brainSel.appendChild(o);
o.title=profile.path;
});
if(msg.value.activeBrainId) brainSel.value=msg.value.activeBrainId;
break;
case 'engineStatus':
const dot = document.getElementById('engineStatusDot');
if(dot){
dot.style.background = msg.value.online ? 'var(--accent)' : 'var(--red)';
dot.title = msg.value.online ? \`AI Engine Online (\${msg.value.url})\` : \`AI Engine Offline (Check \${msg.value.url})\`;
}
break;
case 'brainStatus':
const badge = document.getElementById('brainCountBadge');
const info = document.getElementById('brainStatusInfo');
const meta = document.getElementById('brainStatusMeta');
if(badge){
badge.innerText = msg.value.count > 999 ? '999+' : msg.value.count;
badge.style.display = msg.value.count > 0 ? 'block' : 'none';
}
if(info){
info.innerText = \`Brain: \${msg.value.name} · \${msg.value.count} files\`;
info.title = msg.value.path;
}
if(meta){
meta.innerText = msg.value.description ? \`\${msg.value.description} · \${msg.value.path}\` : msg.value.path;
meta.title = msg.value.path;
}
const syncBrainBtn = document.getElementById('brainBtn');
if(syncBrainBtn) syncBrainBtn.title = \`Sync Brain: \${msg.value.name} (\${msg.value.count} files at \${msg.value.path})\`;
break;
case 'clearChat':
streamBody=null;hideLoader();setSending(false);
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">System Cleaned.</div></div>';
document.body.classList.add('init');
break;
case 'injectPrompt':
input.value=msg.value;send();
break;
case 'autoContinue':
showLoader();setSending(true);
const hint = document.createElement('div');
hint.className = 'auto-status';
hint.innerText = msg.value;
chat.appendChild(hint);
chat.scrollTop = chat.scrollHeight;
break;
case 'error':
hideLoader();setSending(false);addMsg('Error: '+msg.value,'error');
break;
} });
} catch(err) { console.error(err); }
</script></body></html>`;
input.addEventListener('input', () => { input.style.height = 'auto'; input.style.height = input.scrollHeight + 'px'; });
document.getElementById('newChatBtn').onclick = () => vscode.postMessage({ type: 'newChat' });
document.getElementById('settingsBtn').onclick = () => vscode.postMessage({ type: 'openSettings' });
document.getElementById('internetBtn').onclick = () => {
internetEnabled = !internetEnabled; document.getElementById('internetBtn').classList.toggle('active', internetEnabled);
};
document.getElementById('brainBtn').onclick = () => vscode.postMessage({ type: 'syncBrain' });
document.getElementById('historyBtn').onclick = () => vscode.postMessage({ type: 'getSessions' });
document.getElementById('historyBtn').addEventListener('click', () => historyOverlay.classList.add('visible'));
document.getElementById('closeHistoryBtn').onclick = () => historyOverlay.classList.remove('visible');
modelSel.onchange = () => vscode.postMessage({ type: 'refreshModels' });
brainSel.onchange = () => {
if (brainSel.value === 'new') {
vscode.postMessage({ type: 'addBrain' });
} else {
vscode.postMessage({ type: 'setBrainProfile', id: brainSel.value });
}
};
vscode.postMessage({ type: 'getModels' });
vscode.postMessage({ type: 'ready' });
</script>
</body>
</html>`;
}
}