Bump version to v1.0.3 with Information Message and fresh View ID
This commit is contained in:
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
@@ -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>`;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user