From dcbba522c4ea4b8887f06eb5e12201c0b94e3789 Mon Sep 17 00:00:00 2001 From: Jay Date: Sat, 11 Apr 2026 23:16:38 +0900 Subject: [PATCH] Bump version to v1.0.3 with Information Message and fresh View ID --- check.js | 619 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +- patch.py | 25 ++ src/extension.ts | 9 +- 4 files changed, 653 insertions(+), 4 deletions(-) create mode 100644 check.js create mode 100644 patch.py diff --git a/check.js b/check.js new file mode 100644 index 0000000..aa8dd30 --- /dev/null +++ b/check.js @@ -0,0 +1,619 @@ +"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 \ No newline at end of file diff --git a/package.json b/package.json index eb60191..a4d6a2e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "connect-ai-lab", "displayName": "Connect AI LAB", "description": "100% 로컬 AI 코딩 에이전트 — 파일 생성, 코드 편집, 터미널 실행을 오프라인으로. Ollama + Gemma/Llama/DeepSeek 지원.", - "version": "1.0.2", + "version": "1.0.3", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", @@ -80,7 +80,7 @@ "connect-ai-lab-sidebar": [ { "type": "webview", - "id": "local-ai-chat-view", + "id": "connect-ai-lab-v2-view", "name": "Chat" } ] diff --git a/patch.py b/patch.py new file mode 100644 index 0000000..bcf7f9b --- /dev/null +++ b/patch.py @@ -0,0 +1,25 @@ +import sys + +with open('src/extension.ts', 'r') as f: + text = f.read() + +# Replace the beginning of " +script_try_end = """} catch(err) { + document.body.innerHTML = '

\u26a0\ufe0f WEBVIEW JS CRASH

' + err.name + ': ' + err.message + '\\n' + err.stack + '
'; +} +""" + +text = text.replace(script_start, script_try_start) +text = text.replace(script_end, script_try_end) + +# Also let's rename the view and extension id to completely bypass any cache or conflicts +text = text.replace("'local-ai-chat-view'", "'connect-ai-lab-v2-view'") + +with open('src/extension.ts', 'w') as f: + f.write(text) + +print("Patch applied.") diff --git a/src/extension.ts b/src/extension.ts index e30d76b..ede633b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -58,11 +58,12 @@ RULES: // ============================================================ export function activate(context: vscode.ExtensionContext) { - console.log('Connect AI LAB extension activated.'); + vscode.window.showInformationMessage('🔥 Connect AI LAB V2 활성화 완료!'); +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, { + vscode.window.registerWebviewViewProvider('connect-ai-lab-v2-view', provider, { webviewOptions: { retainContextWhenHidden: true } }) ); @@ -602,6 +603,7 @@ textarea::placeholder{color:var(--text-dim)} `; } }