"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.activate = activate; exports.deactivate = deactivate; const vscode = require("vscode"); const axios_1 = require("axios"); const fs = require("fs"); const path = require("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 // ============================================================ function activate(context) { 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\`\`\``); } })); } function deactivate() { } // ============================================================ // Sidebar Chat Provider // ============================================================ class SidebarChatProvider { _extensionUri; _view; _chatHistory = []; _terminal; _ctx; // 대화 표시용 (system prompt 제외, 유저에게 보여줄 것만 저장) _displayMessages = []; constructor(_extensionUri, ctx) { this._extensionUri = _extensionUri; this._ctx = ctx; this._restoreHistory(); } /** 저장된 대화 기록 복원 */ _restoreHistory() { const saved = this._ctx.workspaceState.get('chatState'); if (saved && saved.chat && saved.chat.length > 1) { this._chatHistory = saved.chat; this._displayMessages = saved.display || []; } else { this._initHistory(); } } /** 대화 기록 영구 저장 (워크스페이스 단위) */ _saveHistory() { this._ctx.workspaceState.update('chatState', { chat: this._chatHistory, display: this._displayMessages }); } _initHistory() { this._chatHistory = [{ role: 'system', content: SYSTEM_PROMPT }]; this._displayMessages = []; } resetChat() { this._initHistory(); this._saveHistory(); if (this._view) { this._view.webview.postMessage({ type: 'clearChat' }); } vscode.window.showInformationMessage('Connect AI LAB: 새 대화가 시작되었습니다.'); } /** 대화를 Markdown 파일로 내보내기 */ 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) */ focusInput() { if (this._view) { this._view.show?.(true); this._view.webview.postMessage({ type: 'focusInput' }); } } /** 외부에서 프롬프트 전송 (예: 코드 선택 → 설명) */ sendPromptFromExtension(prompt) { if (this._view) { this._view.show?.(true); // 약간의 딜레이 후 전송 (뷰가 보이기를 기다림) setTimeout(() => { this._view?.webview.postMessage({ type: 'injectPrompt', value: prompt }); }, 300); } } // -------------------------------------------------------- // Webview Lifecycle // -------------------------------------------------------- resolveWebviewView(webviewView, _context, _token) { 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 // -------------------------------------------------------- async _sendModels() { if (!this._view) { return; } const { ollamaBase, defaultModel } = getConfig(); try { const res = await axios_1.default.get(`${ollamaBase}/api/tags`); const models = res.data.models.map((m) => m.name); this._view.webview.postMessage({ type: 'modelsList', value: models }); } catch { this._view.webview.postMessage({ type: 'modelsList', value: [defaultModel] }); } } /** 저장된 대화 메시지를 웹뷰에 다시 전송 (복원) */ _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 // -------------------------------------------------------- _getWorkspaceContext() { const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!root) { return ''; } // --- 1. File tree --- const lines = []; let count = 0; const walk = (dir, prefix) => { if (count >= getConfig().maxTreeFiles) { return; } let entries; 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 // -------------------------------------------------------- async _handlePrompt(prompt, modelName) { 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_1.default.post(`${ollamaBase}/api/chat`, { model: modelName || defaultModel, messages: this._chatHistory, stream: false, }, { timeout }); const aiMessage = 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) { 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 // -------------------------------------------------------- _executeActions(aiMessage) { const report = []; const rootPath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!rootPath) { const hasActions = /([\s\S]*?)<\/create_file>/g; let match; 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) { 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; 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) { 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) { 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 // ============================================================ // ============================================================ // Webview HTML — Premium UI v2 (Zero External Dependencies) // ============================================================ _getHtml() { 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
`; } } //# sourceMappingURL=extension.js.map