🚀 Connect AI LAB v1.0.0 — 100% 로컬 AI 코딩 에이전트

- 에이전트 코어: 파일 생성, 코드 편집, 터미널 실행
- 프리미엄 UI: 글래스모피즘, shimmer 로딩, 구문 강조
- Cmd+L 포커스, 코드 설명(우클릭), 대화 내보내기
- VS Code Settings 통합 (Ollama URL, 모델, 타임아웃)
- 대화 기록 영구 저장 (workspaceState)
- 멀티파일 프로젝트 컨텍스트 자동 분석
This commit is contained in:
Jay
2026-04-11 21:58:24 +09:00
commit 6333ca91b4
11 changed files with 1315 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules/
out/
*.vsix
.vscode-test/
preview.html
.DS_Store
+16
View File
@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
]
}
]
}
+18
View File
@@ -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
+21
View File
@@ -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.
+109
View File
@@ -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**
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#6366f1"/>
<stop offset="100%" style="stop-color:#a855f7"/>
</linearGradient>
</defs>
<rect width="128" height="128" rx="24" fill="url(#g)"/>
<text x="64" y="78" text-anchor="middle" font-family="Arial,sans-serif" font-weight="bold" font-size="52" fill="white"></text>
</svg>

After

Width:  |  Height:  |  Size: 473 B

+343
View File
@@ -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"
}
}
}
+126
View File
@@ -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"
}
}
+655
View File
@@ -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<string>('ollamaUrl', 'http://127.0.0.1:11434'),
defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b'),
maxTreeFiles: cfg.get<number>('maxContextFiles', 200),
timeout: cfg.get<number>('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 ━━━
<create_file path="relative/path/file.ext">
file content here
</create_file>
━━━ ACTION 2: EDIT EXISTING FILES ━━━
<edit_file path="relative/path/file.ext">
<find>exact text to find in the file</find>
<replace>replacement text</replace>
</edit_file>
You can have multiple <find>/<replace> pairs inside one <edit_file> block.
━━━ ACTION 3: RUN TERMINAL COMMANDS ━━━
<run_command>npm install express</run_command>
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 <find> 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 = /<create_file|<edit_file|<run_command/.test(aiMessage);
if (hasActions) {
report.push('❌ 폴더가 열려있지 않습니다. File → Open Folder로 폴더를 먼저 열어주세요.');
}
return report;
}
// ACTION 1: Create files
const createRegex = /<create_file\s+path="([^"]+)">([\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 = /<edit_file\s+path="([^"]+)">([\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 = /<find>([\s\S]*?)<\/find>\s*<replace>([\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 = /<run_command>([\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 `<!DOCTYPE html>
<html lang="ko"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Connect AI LAB</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark-dimmed.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#111113;--bg2:#18181b;--surface:#1e1e22;--surface2:#27272b;
--border:rgba(255,255,255,.08);--border2:rgba(255,255,255,.12);
--text:#a1a1aa;--text-bright:#fafafa;--text-dim:#52525b;
--accent:#818cf8;--accent2:#c084fc;--accent-glow:rgba(129,140,248,.15);
--input-bg:#1a1a1e;--code-bg:#0c0c0e;
--green:#34d399;--yellow:#fbbf24;--cyan:#22d3ee;--red:#fb7185;
}
html,body{height:100%;font-family:'Inter',-apple-system,system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);display:flex;flex-direction:column;overflow:hidden}
.header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:rgba(17,17,19,.85);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid var(--border);flex-shrink:0;position:relative;z-index:10}
.header::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),transparent);opacity:.3}
.header-left{display:flex;align-items:center;gap:10px}
.logo{width:24px;height:24px;border-radius:6px;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:14px;color:#fff;box-shadow:0 0 12px rgba(129,140,248,.3)}
.brand{font-weight:700;font-size:13px;color:var(--text-bright);letter-spacing:-.3px}
.header-right{display:flex;align-items:center;gap:6px}
select{background:var(--surface);color:var(--text-bright);border:1px solid var(--border2);padding:5px 10px;border-radius:6px;font-size:11px;font-family:inherit;cursor:pointer;outline:none;max-width:140px;transition:border-color .2s}
select:hover,select:focus{border-color:var(--accent)}
.btn-icon{background:var(--surface);border:1px solid var(--border2);color:var(--text-dim);width:28px;height:28px;border-radius:6px;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s}
.btn-icon:hover{background:var(--surface2);color:var(--text-bright);border-color:var(--accent);box-shadow:0 0 8px var(--accent-glow)}
.chat{flex:1;overflow-y:auto;padding:20px 16px;display:flex;flex-direction:column;gap:20px}
.chat::-webkit-scrollbar{width:3px}.chat::-webkit-scrollbar-track{background:transparent}.chat::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
.msg{display:flex;flex-direction:column;gap:6px;animation:msgIn .3s ease-out}
.msg-head{display:flex;align-items:center;gap:8px;font-weight:600;font-size:11.5px;color:var(--text)}
.msg-time{font-weight:400;font-size:10px;color:var(--text-dim);margin-left:auto}
.av{width:22px;height:22px;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0}
.av-user{background:var(--surface2);color:var(--text)}.av-ai{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 0 8px rgba(129,140,248,.2)}
.msg-body{padding-left:30px;line-height:1.7;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:12px;padding:10px 14px;margin-left:30px;color:var(--text-bright)}
.msg-body pre{background:var(--code-bg);border:1px solid var(--border2);border-radius:8px;padding:14px 16px;overflow-x:auto;margin:8px 0;font-size:12px;line-height:1.55}
.msg-body code{font-family:'SF Mono','Fira Code','Cascadia Code','Menlo',monospace;font-size:12px}
.msg-body :not(pre)>code{background:rgba(129,140,248,.1);color:var(--accent);padding:1px 6px;border-radius:4px;border:1px solid rgba(129,140,248,.15)}
.code-wrap{position:relative}
.code-lang{position:absolute;top:0;left:14px;background:var(--surface2);color:var(--text-dim);padding:1px 8px;border-radius:0 0 4px 4px;font-size:9px;font-family:'SF Mono',monospace;text-transform:uppercase;letter-spacing:.5px}
.copy-btn{position:absolute;top:6px;right:8px;background:var(--surface2);border:1px solid var(--border2);color:var(--text-dim);padding:3px 10px;border-radius:5px;font-size:10px;cursor:pointer;opacity:0;transition:all .2s;font-family:inherit;z-index:1}
.code-wrap:hover .copy-btn{opacity:1}.copy-btn:hover{background:var(--accent);color:#fff;border-color:var(--accent)}
.copy-btn.copied{background:var(--green);color:#fff;border-color:var(--green);opacity:1}
.file-badge{background:rgba(251,191,36,.06);border:1px solid rgba(251,191,36,.2);border-radius:8px 8px 0 0;border-bottom:none;padding:8px 14px;font-size:11px;font-weight:600;color:var(--yellow);display:flex;align-items:center;gap:6px}
.file-badge+.code-wrap pre{border-top-left-radius:0;border-top-right-radius:0;margin-top:0}
.edit-badge{background:rgba(34,211,238,.06);border:1px solid rgba(34,211,238,.2);border-radius:8px 8px 0 0;border-bottom:none;padding:8px 14px;font-size:11px;font-weight:600;color:var(--cyan);display:flex;align-items:center;gap:6px}
.edit-badge+.code-wrap pre{border-top-left-radius:0;border-top-right-radius:0;margin-top:0}
.cmd-badge{background:rgba(129,140,248,.06);border:1px solid rgba(129,140,248,.2);border-radius:8px;padding:10px 14px;margin:8px 0;font-size:12px;color:var(--accent);font-family:'SF Mono','Menlo',monospace;display:flex;align-items:center;gap:8px}
.agent-report{background:rgba(52,211,153,.06);border:1px solid rgba(52,211,153,.2);border-radius:8px;padding:12px 14px;margin-top:8px;font-size:12px;line-height:1.7}
.msg-error .msg-body{color:var(--red)}
.welcome{text-align:center;padding:30px 20px 10px}
.welcome-logo{width:48px;height:48px;border-radius:14px;margin:0 auto 14px;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:26px;color:#fff;box-shadow:0 0 30px rgba(129,140,248,.25)}
.welcome-title{font-size:18px;font-weight:800;letter-spacing:-.5px;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:8px}
.welcome-sub{color:var(--text-dim);font-size:12px;line-height:1.6;margin-bottom:16px}
.welcome-features{display:flex;justify-content:center;gap:16px;flex-wrap:wrap;margin-bottom:18px}
.wf{display:flex;align-items:center;gap:4px;font-size:11px;color:var(--text)}.wf-icon{font-size:14px}
.quick-actions{display:flex;flex-wrap:wrap;gap:6px;justify-content:center}
.qa-btn{background:var(--surface);border:1px solid var(--border2);color:var(--text);padding:8px 14px;border-radius:8px;font-size:11px;cursor:pointer;transition:all .2s;font-family:inherit}
.qa-btn:hover{border-color:var(--accent);color:var(--text-bright);background:var(--surface2);box-shadow:0 0 12px var(--accent-glow)}
.loading-wrap{padding-left:30px;padding-top:6px;display:flex;align-items:center;gap:8px}
.loading-bar{width:120px;height:3px;background:var(--surface2);border-radius:3px;overflow:hidden;position:relative}
.loading-bar::after{content:'';position:absolute;top:0;left:-40px;width:40px;height:100%;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),transparent);animation:shimmer 1.2s ease-in-out infinite}
.loading-text{font-size:11px;color:var(--text-dim);animation:pulse 2s ease-in-out infinite}
.input-wrap{padding:10px 16px 16px;flex-shrink:0;position:relative}
.input-box{background:var(--input-bg);border:1px solid var(--border2);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:all .2s;position:relative}
.input-box::before{content:'';position:absolute;inset:-1px;border-radius:13px;background:linear-gradient(135deg,var(--accent),var(--accent2));opacity:0;transition:opacity .3s;z-index:-1}
.input-box:focus-within{border-color:transparent}.input-box:focus-within::before{opacity:.4}
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)}
.input-btns{display:flex;gap:5px}
.send-btn{background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff;width:30px;height:30px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;transition:all .15s;box-shadow:0 2px 8px rgba(129,140,248,.25)}
.send-btn:hover{transform:translateY(-1px);box-shadow:0 4px 16px rgba(129,140,248,.35)}.send-btn:active{transform:scale(.94)}.send-btn:disabled{opacity:.25;cursor:not-allowed;transform:none;box-shadow:none}
.stop-btn{background:var(--red);border:none;color:#fff;width:30px;height:30px;border-radius:8px;cursor:pointer;display:none;align-items:center;justify-content:center;font-size:11px}
.stop-btn.visible{display:flex}
@keyframes msgIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}
@keyframes shimmer{0%{left:-40px}100%{left:120px}}
@keyframes pulse{0%,100%{opacity:.5}50%{opacity:1}}
</style></head><body>
<div class="header"><div class="header-left"><div class="logo">\u2726</div><span class="brand">Connect AI LAB</span></div><div class="header-right"><select id="modelSel"></select><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
<div class="chat" id="chat">
<div class="welcome">
<div class="welcome-logo">\u2726</div>
<div class="welcome-title">Connect AI LAB</div>
<div class="welcome-sub">100% \ub85c\uceec \u00b7 100% \uc624\ud504\ub77c\uc778 \u00b7 100% \ubb34\ub8cc<br>\ud504\ub85c\uc81d\ud2b8\ub97c \uc774\ud574\ud558\uace0, \ucf54\ub4dc\ub97c \uc791\uc131\ud558\uace0, \uc2e4\ud589\ud569\ub2c8\ub2e4.</div>
<div class="welcome-features"><div class="wf"><span class="wf-icon">\ud83d\udcc1</span> \ud30c\uc77c \uc0dd\uc131</div><div class="wf"><span class="wf-icon">\u270f\ufe0f</span> \ucf54\ub4dc \ud3b8\uc9d1</div><div class="wf"><span class="wf-icon">\ud83d\udda5\ufe0f</span> \ud130\ubbf8\ub110</div><div class="wf"><span class="wf-icon">\ud83d\udd0d</span> \ubd84\uc11d</div></div>
<div class="quick-actions">
<button class="qa-btn" data-prompt="\uac04\ub2e8\ud55c \ud3ec\ud2b8\ud3f4\ub9ac\uc624 \uc6f9\uc0ac\uc774\ud2b8\ub97c \ub9cc\ub4e4\uc5b4\uc918">\ud83c\udf10 \uc6f9\uc0ac\uc774\ud2b8 \uc0dd\uc131</button>
<button class="qa-btn" data-prompt="Express API \uc11c\ubc84\ub97c \ub9cc\ub4e4\uc5b4\uc918">\u26a1 API \uc11c\ubc84</button>
<button class="qa-btn" data-prompt="\uc774 \ud504\ub85c\uc81d\ud2b8\uc758 \uad6c\uc870\ub97c \ubd84\uc11d\ud574\uc918">\ud83d\udd0d \ud504\ub85c\uc81d\ud2b8 \ubd84\uc11d</button>
<button class="qa-btn" data-prompt="README.md\ub97c \uc791\uc131\ud574\uc918">\ud83d\udcdd README</button>
</div></div></div>
<div class="input-wrap"><div class="input-box">
<textarea id="input" rows="1" placeholder="\ubb34\uc5c7\uc744 \ub9cc\ub4e4\uc5b4 \ub4dc\ub9b4\uae4c\uc694?"></textarea>
<div class="input-footer"><span class="input-hint">Enter \uc804\uc1a1 \u00b7 Shift+Enter \uc904\ubc14\uafc8</span>
<div class="input-btns"><button class="stop-btn" id="stopBtn">\u25a0</button><button class="send-btn" id="sendBtn">\u2191</button></div></div></div></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"><\/script>
<script>
const vscode=acquireVsCodeApi(),chat=document.getElementById('chat'),input=document.getElementById('input'),
sendBtn=document.getElementById('sendBtn'),stopBtn=document.getElementById('stopBtn'),
modelSel=document.getElementById('modelSel'),newChatBtn=document.getElementById('newChatBtn');
let loader=null,sending=false;
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('ko-KR',{hour:'2-digit',minute:'2-digit'})}
function esc(s){const d=document.createElement('div');d.innerText=s;return d.innerHTML}
function fmt(t){
t=t.replace(/<create_file\\s+path="([^"]+)">([\\s\\S]*?)<\\/create_file>/g,(_,p,c)=>'<div class="file-badge">\ud83d\udcc1 '+esc(p)+' \u2014 \uc790\ub3d9 \uc0dd\uc131\ub428</div><div class="code-wrap"><pre><code>'+esc(c)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>');
t=t.replace(/<edit_file\\s+path="([^"]+)">([\\s\\S]*?)<\\/edit_file>/g,(_,p,c)=>'<div class="edit-badge">\u270f\ufe0f '+esc(p)+' \u2014 \ud3b8\uc9d1\ub428</div><div class="code-wrap"><pre><code>'+esc(c)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>');
t=t.replace(/<run_command>([\\s\\S]*?)<\\/run_command>/g,(_,c)=>'<div class="cmd-badge">\u25b6 '+esc(c)+'</div>');
t=t.replace(/---\\n\ud83d\udce6 \\*\\*\uc5d0\uc774\uc804\ud2b8 \uc791\uc5c5 \uacb0\uacfc\\*\\*\\n([\\s\\S]*?)$/,(_,c)=>'<div class="agent-report">\ud83d\udce6 <strong>\uc5d0\uc774\uc804\ud2b8 \uc791\uc5c5 \uacb0\uacfc</strong>\\n'+c+'</div>');
t=t.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,(_,lang,c)=>{const l=lang||'code';return '<div class="code-wrap"><span class="code-lang">'+l+'</span><pre><code class="language-'+lang+'">'+esc(c)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>'});
t=t.replace(/\`([^\`]+)\`/g,(_,c)=>'<code>'+esc(c)+'</code>');
t=t.replace(/\\*\\*([^*]+)\\*\\*/g,'<strong>$1</strong>');
return t;
}
function copyCode(btn){const code=btn.parentElement.querySelector('code');if(!code)return;navigator.clipboard.writeText(code.innerText).then(()=>{btn.textContent='\u2713 Copied';btn.classList.add('copied');setTimeout(()=>{btn.textContent='Copy';btn.classList.remove('copied')},1500)})}
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">\ud83d\udc64</div><span>You</span>':'<div class="av av-ai">\u2726</div><span>Connect AI LAB</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;
el.querySelectorAll('pre code').forEach(b=>{try{hljs.highlightElement(b)}catch(e){}});
}
function showLoader(){loader=document.createElement('div');loader.className='msg';loader.innerHTML='<div class="msg-head"><div class="av av-ai">\u2726</div><span>Connect AI LAB</span><span class="msg-time">'+getTime()+'</span></div><div class="loading-wrap"><div class="loading-bar"></div><span class="loading-text">\uc0dd\uac01\ud558\ub294 \uc911...</span></div>';chat.appendChild(loader);chat.scrollTop=chat.scrollHeight}
function hideLoader(){if(loader&&loader.parentNode)loader.parentNode.removeChild(loader);loader=null}
function setSending(v){sending=v;sendBtn.disabled=v;stopBtn.classList.toggle('visible',v);input.disabled=v;if(!v)input.focus()}
function send(){const text=input.value.trim();if(!text||sending)return;const w=document.querySelector('.welcome');if(w)w.remove();document.querySelectorAll('.quick-actions').forEach(e=>e.remove());addMsg(text,'user');input.value='';input.style.height='auto';setSending(true);showLoader();vscode.postMessage({type:'prompt',value:text,model:modelSel.value})}
document.addEventListener('click',e=>{if(e.target.classList.contains('qa-btn')){const p=e.target.getAttribute('data-prompt');if(p){input.value=p;send()}}});
sendBtn.addEventListener('click',send);
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
newChatBtn.addEventListener('click',()=>vscode.postMessage({type:'newChat'}));
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
case 'response':hideLoader();setSending(false);addMsg(msg.value,'ai');break;
case 'error':hideLoader();setSending(false);addMsg(msg.value,'error');break;
case 'modelsList':modelSel.innerHTML='';msg.value.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});break;
case 'clearChat':chat.innerHTML='';addMsg('\uc0c8 \ub300\ud654\uac00 \uc2dc\uc791\ub418\uc5c8\uc2b5\ub2c8\ub2e4.','ai');break;
case 'restoreMessages':chat.innerHTML='';if(msg.value&&msg.value.length>0){msg.value.forEach(m=>addMsg(m.text,m.role))}break;
case 'focusInput':input.focus();break;
case 'injectPrompt':input.value=msg.value;input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px';send();break;
}});
<\/script></body></html>`;
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2022",
"outDir": "out",
"lib": ["ES2022"],
"sourceMap": true,
"strict": true
},
"exclude": ["node_modules", ".vscode-test"]
}