From 6333ca91b411104450b11d5cdebc78a071c08cda Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 11 Apr 2026 21:58:24 +0900 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Connect=20AI=20LAB=20v1.0.0=20?= =?UTF-8?q?=E2=80=94=20100%=20=EB=A1=9C=EC=BB=AC=20AI=20=EC=BD=94=EB=94=A9?= =?UTF-8?q?=20=EC=97=90=EC=9D=B4=EC=A0=84=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에이전트 코어: 파일 생성, 코드 편집, 터미널 실행 - 프리미엄 UI: 글래스모피즘, shimmer 로딩, 구문 강조 - Cmd+L 포커스, 코드 설명(우클릭), 대화 내보내기 - VS Code Settings 통합 (Ollama URL, 모델, 타임아웃) - 대화 기록 영구 저장 (workspaceState) - 멀티파일 프로젝트 컨텍스트 자동 분석 --- .gitignore | 6 + .vscode/launch.json | 16 ++ .vscodeignore | 18 ++ LICENSE | 21 ++ README.md | 109 ++++++++ assets/icon.png | Bin 0 -> 3626 bytes assets/icon.svg | 10 + package-lock.json | 343 +++++++++++++++++++++++ package.json | 126 +++++++++ src/extension.ts | 655 ++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 11 + 11 files changed, 1315 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscodeignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/icon.png create mode 100644 assets/icon.svg create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/extension.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..60c4d6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +out/ +*.vsix +.vscode-test/ +preview.html +.DS_Store diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..85573b7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ] + } + ] +} diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..78ec7b1 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,18 @@ +.vscode/** +src/** +node_modules/** +!node_modules/axios/** +!node_modules/follow-redirects/** +!node_modules/form-data/** +!node_modules/proxy-from-env/** +!node_modules/mime-db/** +!node_modules/mime-types/** +!node_modules/combined-stream/** +!node_modules/delayed-stream/** +*.ts +*.map +.gitignore +tsconfig.json +*.vsix +assets/icon.svg +preview.html diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..df1bca3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Connect AI LAB (Jay) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc59008 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# ✦ Connect AI LAB + +**100% 로컬 · 100% 오프라인 · 100% 무료** +VS Code / Cursor / Antigravity에서 작동하는 프리미엄 AI 코딩 에이전트 + +--- + +## ✨ 핵심 기능 + +| 기능 | 설명 | +|:--|:--| +| 📁 **파일 자동 생성** | "포트폴리오 사이트 만들어줘" → 폴더/파일 자동 생성 및 에디터 오픈 | +| ✏️ **기존 파일 편집** | "배경색 바꿔줘" → 해당 코드를 찾아 정확히 교체 | +| 🖥️ **터미널 명령 실행** | "express 설치해줘" → `npm install express` 자동 실행 | +| 🔍 **프로젝트 자동 분석** | 파일 구조 + 핵심 파일 내용을 AI가 자동으로 읽고 이해 | +| 💾 **대화 기록 저장** | VS Code를 닫았다 열어도 이전 대화가 그대로 유지 | +| 🎨 **코드 구문 강조** | highlight.js 기반 전문 코드 하이라이팅 | + +## 📥 설치 방법 + +### 방법 1: VSIX 파일 설치 (가장 간단) + +1. [Releases](https://github.com/YOUR_REPO/releases)에서 `.vsix` 파일 다운로드 +2. VS Code / Cursor / Antigravity 열기 +3. `Cmd+Shift+P` (Mac) 또는 `Ctrl+Shift+P` (Windows) +4. `Extensions: Install from VSIX` 검색 → 다운받은 파일 선택 +5. 완료! 🎉 + +### 방법 2: 소스에서 빌드 + +```bash +git clone https://github.com/YOUR_REPO/connect-ai-lab.git +cd connect-ai-lab +npm install +npm run compile +``` + +## ⚙️ 사전 준비: Ollama 설치 + +Connect AI LAB은 로컬 AI 서버인 **Ollama**를 사용합니다. + +### 1. Ollama 설치 +```bash +# Mac (Homebrew) +brew install ollama + +# 또는 공식 사이트에서 다운로드 +# https://ollama.com +``` + +### 2. AI 모델 다운로드 +```bash +# Gemma 4 (추천, Google 최신 모델) +ollama pull gemma4:e2b + +# 또는 다른 모델 +ollama pull llama3.3 +ollama pull deepseek-r1 +ollama pull codestral +``` + +### 3. Ollama 서버 실행 +```bash +ollama serve +``` + +## 🚀 사용 방법 + +1. **폴더 열기**: `File → Open Folder` → 프로젝트 폴더 선택 +2. **사이드바 클릭**: 왼쪽 로봇 아이콘 (🤖) +3. **대화 시작**: 자연어로 요청하면 AI가 자동으로 파일을 만들고 편집합니다! + +### 예시 프롬프트: +``` +간단한 Express 서버를 만들어줘 +이 프로젝트에 라우터 추가해줘 +package.json의 description을 바꿔줘 +express 패키지 설치해줘 +``` + +## ⚙️ 설정 변경 + +`File > Preferences > Settings`에서 "Connect AI LAB" 검색: + +| 설정 | 기본값 | 설명 | +|:--|:--|:--| +| `ollamaUrl` | `http://127.0.0.1:11434` | Ollama 서버 주소 | +| `defaultModel` | `gemma4:e2b` | 기본 AI 모델 | +| `maxContextFiles` | `200` | 컨텍스트에 포함할 최대 파일 수 | +| `requestTimeout` | `300` | AI 응답 대기 시간 (초) | + +## 🔒 프라이버시 + +- ❌ 클라우드 서버 없음 +- ❌ 데이터 수집 없음 +- ❌ 인터넷 연결 불필요 +- ✅ 모든 데이터는 내 컴퓨터 안에서만 처리 + +## 🤝 기여 + +Pull Request와 Issue를 환영합니다! + +## 📄 라이선스 + +MIT License — 자유롭게 사용, 수정, 배포 가능합니다. + +--- + +**Made with ❤️ by Connect AI LAB — 여러분의 AI 멘토 Jay** diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..da584e8ff83249d529147260f8e6a3463f4faf9c GIT binary patch literal 3626 zcmbW3cT`i^7Kd+0=t`stD9uPm6zN4GC^$4}DpCf9rUY>4(h0&K7@8D8L8OK#9gV_B zm#%aOK|qkudnkcWLSEp_o43~c>%I3|_nfoux%=#UzjO9p>rzLkKY=s)x_Y_*2n2v+ zKr#S8odUvj0uT=Xz{m)Y0sw#sfPnY`dYT2IHBHdDzinNR1OPsnrvrd!1OWNV=QeFT zHfhn0*Zehtb3lK2)8=yM{`L;f0sq@RUPoO8R7_kvd_87#+ImKilZzJ^E-F%S=_g@a&&Tj z@X!U}>f`J8*gqgJDDsb}r_nLbP{}E&FVfOqzRJkUFF+S!-o7h({{j2)Q&sh!pT9J> zw6?W(bbcKe9KsFbN4}3v5vFHm=jIm{mo_%Hwn;m?htP}2F>q*^GTM7_Uby;%iAy^vx3Y;DCT~XMcJS_J;kl?VCAx8p zc0%^wfkpgZWPgMGn`;cX0nnWYOh*T%2ZO=%4D>WHFfkm9iG}GzEPv_LiP(+>{aaL8 z5D+Z}1Oj2CU1wOCSqW++zB@=ZNI0XjLk_pTXzyS(bKqALCFPuf9;36A_3&5J6 zs$?sJs~Ia@UN4`&`?WC{^)ORtw|)NBE1})!`CBm1wz)>j5%F-*A49u0c#+Wjws75e zodqnppu=NtKV+&SLeFJ-U5MI8*%gp<5s+kV6dwrJ=p|IOB3VL&i6s*f8-}wc0?)6% zl(*5>z3KH1CLWg@08aL@OK9Mx0&d2!+3aP7sKGla%^5d1XGib<(yuhRk5D7ry686} zd9;wf^MPFKw;@EBz&twNip^&GCK~nnx5Gc5ed-o3w7nj=u*52!3+ z?YfW6$L&an#e&NBizRV62?l1@+@gk{Vl|hv4d<$3cY54@%IjUaz2q{p4z-$-wJuj2 zuoX1&QHGU<@Ti;DGZ?l{hmJ{9OB(1+j7K7b$f2|5Pa}DQLyOm_09o9lz0}}m`YK=U z;=ni;aXx-7T6btld^Z^L=+lRN2Gj!~Wv7P%%d=bVWgL;Oxib#6P2j~}CGzU8v{3tL zi6NOx1&WZln1mPS8jc^o>8ca%)L+toTW!P2Fmvsw!rDf^!J3>hJ%0%T`dC&)0*=U%q^fQ8CI9 zy(D;H*gXVZaP#-eB=wlmQi5%{LJvDvaGLO)^mM*n#ht`M&hn|~Lm7`z3w_+W$d@FE_75B&A#R>F1dA^wN@os*%8#eH5ZN^L%tW2S+TmsjvMp!l+y5x6G zS*{l$WpIwIXrsaz{)F2}D`tHSB#Oq-#raK z*Jg^3P4e_drz&G3;H4pJZY=>pEF_Ghi3aZ&xjzoId71|~lc9se>$%HYJt-schlZxyDhBrK|WHMQ0r~qE&eixFjA{a#x zA>yB6p^Pp(nrDf1aaIpf3U5}7S!>rgq7)a3;627-4I+dxDNGz@#bpAI+f_5G+(T0V zdQ0-X5NqNM19FNp&p|I_5~>%d$ zo(EH~@`GmwRgZu^k%Dg*wS% z15T4l6JwDSF4fj{cJHf~4CXDO(Il z9Xu(((6)X4fJGx@_2iAa0&R(emPv12SFt+rI-dtCGpM*94;qmByRTRn5e4LIOX6BS zPx%x@d)lxed`>;q(w6V}esS(8IN0v@5Lqe^k$N=Hz!s+z)b@@sZhNHSlvQo;{Mh89 zE@#n8>mn(Oc3ExH3bW}G)pEWI_gW8_GuxYi_mWu8%bz@KZ^1b9-s+>)rN)&v*}(TTVcqQz>7- z95EYgm>+TUM(|^r-M;(V28faOq>SMXL##$Zc6xs_k2usb#=PghgE(+yEqgnFmX^*& zPl#^FM*du7IA|4=sE;)oz>Z|NWxmRE_tLGij1TiXG@^m!AMz5L4V;P726Y=*12Zxs zN<2=qMAWb29|)JUf3>!&s~%b}V{@wh{9^7^Z?SrE3&rRX`i)ns$JoexPWkZr zmE?GCR}+K2{ntu#a#pZ6iN-s{(wGAaN4D&?$GA$&PHPZubNz?jo_t>#>yL+|r)n%# zT772tO#`?LoH|4y7dDK0JMB;cHPLF~ z=1r-0BemM#V5ZL9mhqj6N$0jK2(H82Y$z|K+!_2r^daX%LgJxx_NcM`z-6&bY%iad zSke{o*{y62>+{Hfud%#TAOt_}SEW)m-Khk7>h zFPBE%M_n!0&+mIGlp!lV*+4XDcy^0$c~!Is!IKjB*&}i)QST4%=3eJ3+1v11Q6D}k z;AzqR_+HL5>gl@qkcLjlq}zUNA%O~*SEnrzZs6u#eX9{F=5Q;h2>;FEyHj8gT9+$b z$8ph<*C{X0_QQw8RzIFk*Hj8etWUA2mc1Vy62$HD8j5U>xXbCD6G?dCe*eJY27OLD z9TrxJ5OCD0u4G|+r?}|g`OY+R6PYw=&i1EJE=86tBJH^^*T+MdFg7F=_pe_ec%t3d zt)D#@qWn(6Ms$?yqb{pQl=07R(#T)*?1NS>S{ox}(w-&mKF5C(#*=mrY>qmPm?*`y zP{H_;4ux_Kgw~`UE=jqrrg|?wDPT(^1?)X@=DM?R0x}~zZnDyU^&C;zy!z9IUF6z2 zcI5sFPNCz?&X=1j6Yj2tfk!Jv@kiaEJ(d|?3b-Dubmzn7xPB+0p{~7GzHwsBVX${i zrW?SJA&bXCee_lUImq_J+IR@@yQikKR3HUdug z(J8q*W@P+CBDxtDy#>=}D*mAf9BR~%pKX|H_uI$RmDVl>28`^PjsA$N4^=)dylZb7 zqD}^Hm=hGouPkJ6?d&AzE`P(i1v}Z!2Pb9<#_%3kEN3@TfhR|T + + + + + + + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1282c45 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,343 @@ +{ + "name": "local-ai-coder", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "local-ai-coder", + "version": "0.0.1", + "dependencies": { + "axios": "^1.15.0" + }, + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.80.0", + "typescript": "^5.1.3" + }, + "engines": { + "vscode": "^1.80.0" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/vscode": { + "version": "1.115.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.115.0.tgz", + "integrity": "sha512-/M8cdznOlqtMqduHKKlIF00v4eum4ZWKgn8YoPRKcN6PDdvoWeeqDaQSnw63ipDbq1Uzz78Wndk/d0uSPwORfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..267054e --- /dev/null +++ b/package.json @@ -0,0 +1,126 @@ +{ + "name": "connect-ai-lab", + "displayName": "Connect AI LAB", + "description": "100% 로컬 AI 코딩 에이전트 — 파일 생성, 코드 편집, 터미널 실행을 오프라인으로. Ollama + Gemma/Llama/DeepSeek 지원.", + "version": "1.0.0", + "publisher": "connectailab", + "license": "MIT", + "icon": "assets/icon.png", + "engines": { + "vscode": "^1.80.0" + }, + "categories": [ + "Machine Learning", + "Programming Languages", + "Chat" + ], + "keywords": [ + "ai", + "local", + "ollama", + "gemma", + "llama", + "deepseek", + "offline", + "agent", + "code-generation", + "connect-ai-lab", + "copilot" + ], + "activationEvents": [], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "connect-ai-lab.newChat", + "title": "Connect AI LAB: New Chat", + "icon": "$(add)" + }, + { + "command": "connect-ai-lab.exportChat", + "title": "Connect AI LAB: Export Chat as Markdown" + }, + { + "command": "connect-ai-lab.explainSelection", + "title": "Connect AI LAB: Explain Selected Code" + }, + { + "command": "connect-ai-lab.focusChat", + "title": "Connect AI LAB: Focus Chat Input" + } + ], + "keybindings": [ + { + "command": "connect-ai-lab.focusChat", + "key": "cmd+l", + "mac": "cmd+l" + } + ], + "menus": { + "editor/context": [ + { + "command": "connect-ai-lab.explainSelection", + "when": "editorHasSelection", + "group": "1_modification" + } + ] + }, + "viewsContainers": { + "activitybar": [ + { + "id": "connect-ai-lab-sidebar", + "title": "Connect AI LAB", + "icon": "$(hubot)" + } + ] + }, + "views": { + "connect-ai-lab-sidebar": [ + { + "type": "webview", + "id": "local-ai-chat-view", + "name": "Chat" + } + ] + }, + "configuration": { + "title": "Connect AI LAB", + "properties": { + "connectAiLab.ollamaUrl": { + "type": "string", + "default": "http://127.0.0.1:11434", + "description": "Ollama 서버 URL (기본값: http://127.0.0.1:11434)" + }, + "connectAiLab.defaultModel": { + "type": "string", + "default": "gemma4:e2b", + "description": "기본 AI 모델 이름 (예: gemma4:e2b, llama3.3, deepseek-r1)" + }, + "connectAiLab.maxContextFiles": { + "type": "number", + "default": 200, + "description": "프로젝트 컨텍스트에 포함할 최대 파일 수" + }, + "connectAiLab.requestTimeout": { + "type": "number", + "default": 300, + "description": "AI 응답 대기 시간 (초, 기본값: 300초)" + } + } + } + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "npm run compile" + }, + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.80.0", + "typescript": "^5.1.3" + }, + "dependencies": { + "axios": "^1.15.0" + } +} diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..ecca13a --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,655 @@ +import * as vscode from 'vscode'; +import axios from 'axios'; +import * as fs from 'fs'; +import * as path from 'path'; + +// ============================================================ +// Connect AI LAB — Full Agentic Local AI for VS Code +// 100% Offline · File Create · File Edit · Terminal · Multi-file Context +// ============================================================ + +// Settings are read from VS Code configuration (File > Preferences > Settings) +function getConfig() { + const cfg = vscode.workspace.getConfiguration('connectAiLab'); + return { + ollamaBase: cfg.get('ollamaUrl', 'http://127.0.0.1:11434'), + defaultModel: cfg.get('defaultModel', 'gemma4:e2b'), + maxTreeFiles: cfg.get('maxContextFiles', 200), + timeout: cfg.get('requestTimeout', 300) * 1000, + }; +} + +const EXCLUDED_DIRS = new Set([ + 'node_modules', '.git', '.vscode', 'out', 'dist', 'build', + '.next', '.cache', '__pycache__', '.DS_Store', 'coverage', + '.turbo', '.nuxt', '.output', 'vendor', 'target' +]); +const MAX_CONTEXT_SIZE = 40_000; // chars + +const SYSTEM_PROMPT = `You are "Connect AI LAB", a premium agentic AI coding assistant running 100% offline on the user's machine. + +You have THREE powerful agent actions. Use them whenever appropriate: + +━━━ ACTION 1: CREATE NEW FILES ━━━ + +file content here + + +━━━ ACTION 2: EDIT EXISTING FILES ━━━ + +exact text to find in the file +replacement text + +You can have multiple / pairs inside one block. + +━━━ ACTION 3: RUN TERMINAL COMMANDS ━━━ +npm install express + +RULES: +1. ALWAYS respond in the same language the user uses. +2. Use agent actions automatically when the user's request requires creating, editing files, or running commands. +3. Outside of action blocks, briefly explain what you did. +4. For code that is just for explanation (not to be saved), use standard markdown code fences. +5. Be concise, professional, and helpful. +6. When editing files, the text must EXACTLY match existing content in the file.`; + +// ============================================================ +// Extension Activation +// ============================================================ + +export function activate(context: vscode.ExtensionContext) { + console.log('Connect AI LAB extension activated.'); + + const provider = new SidebarChatProvider(context.extensionUri, context); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider('local-ai-chat-view', provider, { + webviewOptions: { retainContextWhenHidden: true } + }) + ); + + // New Chat + context.subscriptions.push( + vscode.commands.registerCommand('connect-ai-lab.newChat', () => { + provider.resetChat(); + }) + ); + + // Export Chat as Markdown + context.subscriptions.push( + vscode.commands.registerCommand('connect-ai-lab.exportChat', async () => { + await provider.exportChat(); + }) + ); + + // Focus Chat Input (Cmd+L) + context.subscriptions.push( + vscode.commands.registerCommand('connect-ai-lab.focusChat', () => { + provider.focusInput(); + }) + ); + + // Explain Selected Code (right-click menu) + context.subscriptions.push( + vscode.commands.registerCommand('connect-ai-lab.explainSelection', () => { + const editor = vscode.window.activeTextEditor; + if (!editor) { return; } + const selection = editor.document.getText(editor.selection); + if (selection.trim()) { + provider.sendPromptFromExtension(`이 코드를 분석하고 설명해줘:\n\`\`\`\n${selection}\n\`\`\``); + } + }) + ); +} + +export function deactivate() {} + +// ============================================================ +// Sidebar Chat Provider +// ============================================================ + +class SidebarChatProvider implements vscode.WebviewViewProvider { + private _view?: vscode.WebviewView; + private _chatHistory: { role: string; content: string }[] = []; + private _terminal?: vscode.Terminal; + private _ctx: vscode.ExtensionContext; + + // 대화 표시용 (system prompt 제외, 유저에게 보여줄 것만 저장) + private _displayMessages: { text: string; role: string }[] = []; + + constructor(private readonly _extensionUri: vscode.Uri, ctx: vscode.ExtensionContext) { + this._ctx = ctx; + this._restoreHistory(); + } + + /** 저장된 대화 기록 복원 */ + private _restoreHistory() { + const saved = this._ctx.workspaceState.get<{ chat: any[]; display: any[] }>('chatState'); + if (saved && saved.chat && saved.chat.length > 1) { + this._chatHistory = saved.chat; + this._displayMessages = saved.display || []; + } else { + this._initHistory(); + } + } + + /** 대화 기록 영구 저장 (워크스페이스 단위) */ + private _saveHistory() { + this._ctx.workspaceState.update('chatState', { + chat: this._chatHistory, + display: this._displayMessages + }); + } + + private _initHistory() { + this._chatHistory = [{ role: 'system', content: SYSTEM_PROMPT }]; + this._displayMessages = []; + } + + public resetChat() { + this._initHistory(); + this._saveHistory(); + if (this._view) { + this._view.webview.postMessage({ type: 'clearChat' }); + } + vscode.window.showInformationMessage('Connect AI LAB: 새 대화가 시작되었습니다.'); + } + + /** 대화를 Markdown 파일로 내보내기 */ + public async exportChat() { + if (this._displayMessages.length === 0) { + vscode.window.showWarningMessage('내보낼 대화가 없습니다.'); + return; + } + let md = `# Connect AI LAB — 대화 기록\n\n_${new Date().toLocaleString('ko-KR')}_\n\n---\n\n`; + for (const m of this._displayMessages) { + const label = m.role === 'user' ? '**👤 You**' : '**✦ Connect AI LAB**'; + md += `### ${label}\n\n${m.text}\n\n---\n\n`; + } + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (root) { + const filePath = path.join(root, `chat-export-${Date.now()}.md`); + fs.writeFileSync(filePath, md, 'utf-8'); + const doc = await vscode.workspace.openTextDocument(filePath); + await vscode.window.showTextDocument(doc); + vscode.window.showInformationMessage(`대화가 ${path.basename(filePath)}로 저장되었습니다.`); + } + } + + /** 채팅 입력창에 포커스 (Cmd+L) */ + public focusInput() { + if (this._view) { + this._view.show?.(true); + this._view.webview.postMessage({ type: 'focusInput' }); + } + } + + /** 외부에서 프롬프트 전송 (예: 코드 선택 → 설명) */ + public sendPromptFromExtension(prompt: string) { + if (this._view) { + this._view.show?.(true); + // 약간의 딜레이 후 전송 (뷰가 보이기를 기다림) + setTimeout(() => { + this._view?.webview.postMessage({ type: 'injectPrompt', value: prompt }); + }, 300); + } + } + + // -------------------------------------------------------- + // Webview Lifecycle + // -------------------------------------------------------- + public resolveWebviewView( + webviewView: vscode.WebviewView, + _context: vscode.WebviewViewResolveContext, + _token: vscode.CancellationToken, + ) { + this._view = webviewView; + webviewView.webview.options = { + enableScripts: true, + localResourceRoots: [this._extensionUri], + }; + webviewView.webview.html = this._getHtml(); + + webviewView.webview.onDidReceiveMessage(async (msg) => { + switch (msg.type) { + case 'prompt': + await this._handlePrompt(msg.value, msg.model); + break; + case 'getModels': + await this._sendModels(); + break; + case 'newChat': + this.resetChat(); + break; + case 'ready': + // 웹뷰가 준비되면 저장된 대화 기록 복원 + this._restoreDisplayMessages(); + break; + } + }); + } + + // -------------------------------------------------------- + // Fetch installed Ollama models + // -------------------------------------------------------- + private async _sendModels() { + if (!this._view) { return; } + const { ollamaBase, defaultModel } = getConfig(); + try { + const res = await axios.get(`${ollamaBase}/api/tags`); + const models: string[] = res.data.models.map((m: any) => m.name); + this._view.webview.postMessage({ type: 'modelsList', value: models }); + } catch { + this._view.webview.postMessage({ type: 'modelsList', value: [defaultModel] }); + } + } + + /** 저장된 대화 메시지를 웹뷰에 다시 전송 (복원) */ + private _restoreDisplayMessages() { + if (!this._view || this._displayMessages.length === 0) { return; } + this._view.webview.postMessage({ + type: 'restoreMessages', + value: this._displayMessages + }); + } + + // -------------------------------------------------------- + // Build workspace file tree + read key files + // -------------------------------------------------------- + private _getWorkspaceContext(): string { + const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + if (!root) { return ''; } + + // --- 1. File tree --- + const lines: string[] = []; + let count = 0; + + const walk = (dir: string, prefix: string) => { + if (count >= getConfig().maxTreeFiles) { return; } + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { return; } + + entries.sort((a, b) => { + if (a.isDirectory() && !b.isDirectory()) { return -1; } + if (!a.isDirectory() && b.isDirectory()) { return 1; } + return a.name.localeCompare(b.name); + }); + + for (const entry of entries) { + if (count >= getConfig().maxTreeFiles) { break; } + if (EXCLUDED_DIRS.has(entry.name)) { continue; } + if (entry.name.startsWith('.') && entry.isDirectory()) { continue; } + + if (entry.isDirectory()) { + lines.push(`${prefix}📁 ${entry.name}/`); + count++; + walk(path.join(dir, entry.name), prefix + ' '); + } else { + lines.push(`${prefix}📄 ${entry.name}`); + count++; + } + } + }; + walk(root, ''); + + let result = ''; + if (lines.length > 0) { + result += `\n\n[프로젝트 파일 구조]\n${lines.join('\n')}`; + } + + // --- 2. Auto-read key project files --- + const keyFiles = [ + 'package.json', 'tsconfig.json', 'vite.config.ts', 'vite.config.js', + 'next.config.js', 'next.config.ts', 'README.md', + 'index.html', 'app.js', 'app.ts', 'main.ts', 'main.js', + 'src/index.ts', 'src/index.js', 'src/App.tsx', 'src/App.jsx', + 'src/main.ts', 'src/main.js' + ]; + let totalRead = 0; + const MAX_AUTO_READ = 15_000; // chars total + + for (const kf of keyFiles) { + if (totalRead >= MAX_AUTO_READ) { break; } + const abs = path.join(root, kf); + if (fs.existsSync(abs)) { + try { + const content = fs.readFileSync(abs, 'utf-8'); + if (content.length < 5000) { + result += `\n\n[파일 내용: ${kf}]\n\`\`\`\n${content}\n\`\`\``; + totalRead += content.length; + } + } catch { /* skip */ } + } + } + + return result; + } + + // -------------------------------------------------------- + // Handle user prompt → Ollama → agent actions → response + // -------------------------------------------------------- + private async _handlePrompt(prompt: string, modelName: string) { + if (!this._view) { return; } + + try { + // 1. Context: active editor content + const editor = vscode.window.activeTextEditor; + let contextBlock = ''; + if (editor && editor.document.uri.scheme === 'file') { + const text = editor.document.getText(); + const name = path.basename(editor.document.fileName); + if (text.trim().length > 0 && text.length < MAX_CONTEXT_SIZE) { + contextBlock = `\n\n[Currently open file: ${name}]\n\`\`\`\n${text}\n\`\`\``; + } + } + + // 2. Context: workspace file tree + key file contents + const workspaceCtx = this._getWorkspaceContext(); + + // 3. Push user message + this._chatHistory.push({ + role: 'user', + content: prompt + contextBlock + workspaceCtx + }); + + // 저장용: 유저 메시지 기록 (프롬프트만, 컨텍스트 제외) + this._displayMessages.push({ text: prompt, role: 'user' }); + + // 4. Call Ollama + const { ollamaBase, defaultModel, timeout } = getConfig(); + const response = await axios.post(`${ollamaBase}/api/chat`, { + model: modelName || defaultModel, + messages: this._chatHistory, + stream: false, + }, { timeout }); + + const aiMessage: string = response.data.message.content; + this._chatHistory.push({ role: 'assistant', content: aiMessage }); + + // 5. Execute agent actions + const report = this._executeActions(aiMessage); + + // 6. Send to webview + let output = aiMessage; + if (report.length > 0) { + output += `\n\n---\n📦 **에이전트 작업 결과**\n${report.join('\n')}`; + } + this._view.webview.postMessage({ type: 'response', value: output }); + + // 저장용: AI 응답 기록 + this._displayMessages.push({ text: output, role: 'ai' }); + this._saveHistory(); + + } catch (error: any) { + const errMsg = error.code === 'ECONNREFUSED' + ? '⚠️ Ollama 서버에 연결할 수 없습니다.\n터미널에서 `ollama serve`를 실행해주세요.' + : `⚠️ 오류: ${error.message}`; + this._view.webview.postMessage({ type: 'error', value: errMsg }); + } + } + + // -------------------------------------------------------- + // Execute ALL agent actions from AI response + // -------------------------------------------------------- + private _executeActions(aiMessage: string): string[] { + const report: string[] = []; + const rootPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + + if (!rootPath) { + const hasActions = /([\s\S]*?)<\/create_file>/g; + let match: RegExpExecArray | null; + let firstCreatedFile = ''; + + while ((match = createRegex.exec(aiMessage)) !== null) { + const relPath = match[1].trim(); + const content = match[2].replace(/^\n/, ''); // remove leading newline only + try { + const absPath = path.join(rootPath, relPath); + const dir = path.dirname(absPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(absPath, content, 'utf-8'); + report.push(`✅ 생성: ${relPath}`); + if (!firstCreatedFile) { firstCreatedFile = absPath; } + } catch (err: any) { + report.push(`❌ 생성 실패: ${relPath} — ${err.message}`); + } + } + + // Open first created file + if (firstCreatedFile) { + vscode.window.showTextDocument(vscode.Uri.file(firstCreatedFile), { preview: false }); + } + + // ACTION 2: Edit files + const editRegex = /([\s\S]*?)<\/edit_file>/g; + while ((match = editRegex.exec(aiMessage)) !== null) { + const relPath = match[1].trim(); + const body = match[2]; + const absPath = path.join(rootPath, relPath); + + if (!fs.existsSync(absPath)) { + report.push(`❌ 편집 실패: ${relPath} — 파일이 존재하지 않습니다.`); + continue; + } + + try { + let fileContent = fs.readFileSync(absPath, 'utf-8'); + const findReplaceRegex = /([\s\S]*?)<\/find>\s*([\s\S]*?)<\/replace>/g; + let frMatch: RegExpExecArray | null; + let editCount = 0; + + while ((frMatch = findReplaceRegex.exec(body)) !== null) { + const findText = frMatch[1]; + const replaceText = frMatch[2]; + if (fileContent.includes(findText)) { + fileContent = fileContent.replace(findText, replaceText); + editCount++; + } else { + report.push(`⚠️ ${relPath}: 일치하는 텍스트를 찾지 못했습니다.`); + } + } + + if (editCount > 0) { + fs.writeFileSync(absPath, fileContent, 'utf-8'); + report.push(`✏️ 편집 완료: ${relPath} (${editCount}건 수정)`); + // Open edited file + vscode.window.showTextDocument(vscode.Uri.file(absPath), { preview: false }); + } + } catch (err: any) { + report.push(`❌ 편집 실패: ${relPath} — ${err.message}`); + } + } + + // ACTION 3: Run commands + const cmdRegex = /([\s\S]*?)<\/run_command>/g; + while ((match = cmdRegex.exec(aiMessage)) !== null) { + const cmd = match[1].trim(); + try { + if (!this._terminal || this._terminal.exitStatus !== undefined) { + this._terminal = vscode.window.createTerminal({ + name: '🚀 Connect AI LAB', + cwd: rootPath + }); + } + this._terminal.show(); + this._terminal.sendText(cmd); + report.push(`🖥️ 실행: ${cmd}`); + } catch (err: any) { + report.push(`❌ 명령 실패: ${cmd} — ${err.message}`); + } + } + + // Show notification + const successCount = report.filter(r => r.startsWith('✅') || r.startsWith('✏️') || r.startsWith('🖥️')).length; + if (successCount > 0) { + vscode.window.showInformationMessage(`Connect AI LAB: ${successCount}개 에이전트 작업 완료!`); + } + + return report; + } + + + // ============================================================ + // Webview HTML — Premium UI v2 + // ============================================================ + private _getHtml(): string { + return ` + +Connect AI LAB + + +
Connect AI LAB
+
+
+ +
Connect AI LAB
+
100% \ub85c\uceec \u00b7 100% \uc624\ud504\ub77c\uc778 \u00b7 100% \ubb34\ub8cc
\ud504\ub85c\uc81d\ud2b8\ub97c \uc774\ud574\ud558\uace0, \ucf54\ub4dc\ub97c \uc791\uc131\ud558\uace0, \uc2e4\ud589\ud569\ub2c8\ub2e4.
+
\ud83d\udcc1 \ud30c\uc77c \uc0dd\uc131
\u270f\ufe0f \ucf54\ub4dc \ud3b8\uc9d1
\ud83d\udda5\ufe0f \ud130\ubbf8\ub110
\ud83d\udd0d \ubd84\uc11d
+
+ + + + +
+
+ +
+