Bump version to v1.0.3 with Information Message and fresh View ID

This commit is contained in:
Jay
2026-04-11 23:16:38 +09:00
parent 537c3c10ca
commit dcbba522c4
4 changed files with 653 additions and 4 deletions
+619
View File
@@ -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 ━━━
<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
// ============================================================
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 = /<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;
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 = /<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;
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 = /<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) {
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 `<!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>
<style>
*{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:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,sans-serif;font-size:13px;background:var(--bg);color:var(--text);display:flex;flex-direction:column;overflow:hidden}
.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;color:#adbac7}
.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}
.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}
.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>
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(new RegExp('<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(new RegExp('<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(new RegExp('<run_command>([\\s\\S]*?)<\\/run_command>', 'g'),(_,c)=>'<div class="cmd-badge">\u25B6 '+esc(c)+'</div>');
t=t.replace(new RegExp('\\x60\\x60\\x60(\\w*)\\n([\\s\\S]*?)\\x60\\x60\\x60', 'g'),(_,lang,c)=>{const l=lang||'code';return '<div class="code-wrap"><span class="code-lang">'+l+'</span><pre><code>'+esc(c)+'</code></pre><button class="copy-btn" onclick="copyCode(this)">Copy</button></div>'});
t=t.replace(new RegExp('\\x60([^\\x60]+)\\x60', 'g'),(_,c)=>'<code>'+esc(c)+'</code>');
t=t.replace(new RegExp('\\*\\*([^*]+)\\*\\*', '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;
}
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>`;
}
}
//# sourceMappingURL=extension.js.map
+2 -2
View File
@@ -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"
}
]
+25
View File
@@ -0,0 +1,25 @@
import sys
with open('src/extension.ts', 'r') as f:
text = f.read()
# Replace the beginning of <script> and end of <script> with try-catch reporting
script_start = "<script>\n"
script_try_start = "<script>\ntry {\n"
script_end = "</script></body></html>"
script_try_end = """} catch(err) {
document.body.innerHTML = '<div style="color:#ff4444;padding:20px;background:#111;height:100%;font-size:14px;overflow:auto;"><h2>\u26a0\ufe0f WEBVIEW JS CRASH</h2><pre>' + err.name + ': ' + err.message + '\\n' + err.stack + '</pre></div>';
}
</script></body></html>"""
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.")
+7 -2
View File
@@ -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)}
<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>
try {
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');
@@ -647,6 +649,9 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
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;
} });
} catch(err) {
document.body.innerHTML = '<div style="color:#ff4444;padding:20px;background:#111;height:100%;font-size:14px;overflow:auto;"><h2>⚠️ WEBVIEW JS CRASH</h2><pre>' + err.name + ': ' + err.message + '\n' + err.stack + '</pre></div>';
}
</script></body></html>`;
}
}