import * as vscode from 'vscode'; import * as fs from 'fs'; import * as path from 'path'; import { _getBrainDir, findBrainFiles, buildApiUrl, getActiveBrainProfile, getBrainProfiles, logError, logInfo, resolveEngine, summarizeText } from './utils'; import { getConfig } from './config'; import { AgentExecutor, ChatMessage } from './agent'; import { BridgeInterface } from './bridge'; interface LastVisibleChatSnapshot { history: ChatMessage[]; brainProfileId: string; sessionId: string | null; timestamp: number; negativePrompt?: string; } interface ChatSession { id: string; title: string; timestamp: number; history: ChatMessage[]; brainProfileId: string; negativePrompt?: string; } /** * Sidebar UI Provider implementing BridgeInterface for BridgeServer */ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeInterface { public static readonly viewType = 'g1nation-v2-view'; private static readonly activeSessionStateKey = 'g1nation.activeSessionId'; private static readonly lastVisibleChatStateKey = 'g1nation.lastVisibleChat'; private static readonly blankChatStateKey = 'g1nation.blankChatActive'; private static readonly lastAgentStateKey = 'g1nation.lastAgentPath'; private _view?: vscode.WebviewView; public brainEnabled = true; private _currentSessionBrainId: string | null = null; private _currentNegativePrompt: string = ''; constructor( private readonly _extensionUri: vscode.Uri, private readonly _context: vscode.ExtensionContext, private readonly _agent: AgentExecutor ) { this._agent.setHistoryChangeListener((history) => { void this._persistLastVisibleChat(history); }); } public resolveWebviewView( webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, _token: vscode.CancellationToken, ) { this._view = webviewView; webviewView.webview.options = { enableScripts: true, localResourceRoots: [this._extensionUri] }; webviewView.webview.html = this._getHtml(webviewView.webview); this._agent.setWebview(webviewView.webview); void this._restoreActiveSessionIntoView(); webviewView.webview.onDidReceiveMessage(async (data) => { switch (data.type) { case 'prompt': case 'promptWithFile': await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); await this._handlePrompt(data); // After prompt, save the session automatically await this._saveCurrentSession(); break; case 'ready': await this._sendBrainStatus(); await this._sendBrainProfiles(); await this._sendSessionList(); await this._sendModels(); await this._sendConfig(); await this._restoreActiveSessionIntoView(); break; case 'toggleMultiAgent': await vscode.workspace.getConfiguration('g1nation').update('multiAgentEnabled', data.value, vscode.ConfigurationTarget.Global); break; case 'getModels': await this._sendModels(); break; case 'getAgents': await this._sendAgentsList(); break; case 'createAgent': await this._createAgent(); break; case 'newChat': this._currentSessionId = null; this._currentSessionBrainId = getActiveBrainProfile().id; this._agent.resetConversation(); await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null); await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null); await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, true); this.clearChat(); await this._sendBrainStatus(); break; case 'stopGeneration': this._agent.stop(); break; case 'loadSession': await this._loadSession(data.id); break; case 'deleteSession': await this._deleteSession(data.id); break; case 'openSettings': vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation'); break; case 'manageBrains': await this._manageBrains(); break; case 'syncBrain': await this.syncBrain(); await this._sendBrainStatus(); break; case 'addMessage': this._view?.webview.postMessage({ type: 'addMessage', role: data.role, value: data.value, rationale: data.rationale }); break; case 'addBrain': await this._addBrainProfile(); break; case 'setBrainProfile': await this._setActiveBrainProfile(data.id); break; case 'getAgentContent': await this._sendAgentContent(data.path); break; case 'updateAgent': await this._updateAgent(data.path, data.content, data.negativePrompt); break; case 'refreshModels': await this._sendModels(); break; case 'model': await vscode.workspace.getConfiguration('g1nation').update('defaultModel', data.value, vscode.ConfigurationTarget.Global); logInfo(`Default model updated to: ${data.value}`); break; case 'proactiveTrigger': await this._handleProactiveSuggestion(data.context); break; case 'exportResponse': const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath || ''; const defaultPath = path.join(workspacePath, 'g1_response.md'); const uri = await vscode.window.showSaveDialog({ defaultUri: vscode.Uri.file(defaultPath), filters: { 'Markdown': ['md'] } }); if (uri) { await vscode.workspace.fs.writeFile(uri, Buffer.from(data.text, 'utf8')); vscode.window.showInformationMessage(`βœ… Exported to ${path.basename(uri.fsPath)}`); } break; case 'approveAction': await this._agent.approveTransaction(); break; case 'rejectAction': await this._agent.rejectTransaction(); break; } }); } private _currentSessionId: string | null = null; private async _restoreActiveSessionIntoView() { if (!this._view) return; const blankChatActive = this._context.globalState.get(SidebarChatProvider.blankChatStateKey, false); if (blankChatActive) { return; } const activeSessionId = this._currentSessionId || this._context.globalState.get(SidebarChatProvider.activeSessionStateKey, null); if (activeSessionId) { const loaded = await this._loadSession(activeSessionId, true); if (loaded) return; await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null); } const currentHistory = this._agent.getHistory(); if (currentHistory.length > 0) { this._view.webview.postMessage({ type: 'restoreHistory', value: currentHistory }); await this._persistLastVisibleChat(currentHistory); return; } const snapshot = this._context.globalState.get(SidebarChatProvider.lastVisibleChatStateKey, null); if (snapshot?.history?.length) { this._currentSessionId = snapshot.sessionId || null; this._currentSessionBrainId = snapshot.brainProfileId || getActiveBrainProfile().id; this._currentNegativePrompt = snapshot.negativePrompt || ''; await this._setActiveBrainProfile(this._currentSessionBrainId, true); this._agent.setHistory(snapshot.history); this._view.webview.postMessage({ type: 'restoreHistory', value: snapshot.history, negativePrompt: this._currentNegativePrompt }); } } private async _persistLastVisibleChat(history: ChatMessage[] = this._agent.getHistory()) { if (history.length === 0) { await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null); return; } const snapshot: LastVisibleChatSnapshot = { history, brainProfileId: this._currentSessionBrainId || getActiveBrainProfile().id, sessionId: this._currentSessionId, timestamp: Date.now(), negativePrompt: this._currentNegativePrompt }; await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, snapshot); } private async _saveCurrentSession() { const history = this._agent.getHistory(); if (history.length === 0) return; let sessions = this._getSessions(); const firstMsg = history.find(m => m.role === 'user')?.content; const title = typeof firstMsg === 'string' ? firstMsg.substring(0, 50).replace(/\n/g, ' ') + (firstMsg.length > 50 ? '...' : '') : 'New Chat'; const brainProfileId = this._currentSessionBrainId || getActiveBrainProfile().id; if (!this._currentSessionId) { this._currentSessionId = Date.now().toString(); sessions.unshift({ id: this._currentSessionId, title, timestamp: Date.now(), history, brainProfileId, negativePrompt: this._currentNegativePrompt }); } else { const idx = sessions.findIndex(s => s.id === this._currentSessionId); if (idx >= 0) { sessions[idx].history = history; sessions[idx].timestamp = Date.now(); sessions[idx].brainProfileId = brainProfileId; sessions[idx].negativePrompt = this._currentNegativePrompt; if (!sessions[idx].title || sessions[idx].title === 'New Chat') { sessions[idx].title = title; } } else { sessions.unshift({ id: this._currentSessionId, title, timestamp: Date.now(), history, brainProfileId, negativePrompt: this._currentNegativePrompt }); } } // Keep only last 50 sessions if (sessions.length > 50) sessions = sessions.slice(0, 50); await this._putSessions(sessions); await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, this._currentSessionId); await this._persistLastVisibleChat(history); await this._sendSessionList(); } private async _sendSessionList() { if (!this._view) return; const sessions = this._getSessions(); const list = sessions.map(s => ({ id: s.id, title: s.title, timestamp: s.timestamp, brainProfileId: s.brainProfileId || '', messageCount: s.history.length, history: s.history })); this._view.webview.postMessage({ type: 'sessionList', value: list }); } private async _loadSession(id: string, skipSessionListRefresh: boolean = false): Promise { if (!id) { logError('Session load requested without an id.'); this._view?.webview.postMessage({ type: 'error', value: 'Chat session id is missing.' }); return false; } const sessions = this._getSessions(); const session = sessions.find(s => s.id === id); if (session) { const history = Array.isArray(session.history) ? session.history : []; if (history.length === 0) { logError('Session load failed because history is empty or invalid.', { id }); this._view?.webview.postMessage({ type: 'error', value: 'This chat session has no saved messages.' }); return false; } this._agent.stop(); this._currentSessionId = id; this._currentNegativePrompt = session.negativePrompt || ''; const sessionBrainId = session.brainProfileId || getActiveBrainProfile().id; await this._setActiveBrainProfile(sessionBrainId, true); this._agent.setHistory(history); await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, id); await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, false); await this._persistLastVisibleChat(history); this._view?.webview.postMessage({ type: 'sessionLoaded', value: { id, title: session.title || 'Chat Session', history, negativePrompt: this._currentNegativePrompt } }); if (!skipSessionListRefresh) { await this._sendSessionList(); } logInfo('Chat session loaded.', { id, messages: history.length }); return true; } logError('Session load failed because id was not found.', { id, sessionCount: sessions.length }); this._view?.webview.postMessage({ type: 'error', value: 'Chat session was not found.' }); return false; } private async _deleteSession(id: string) { let sessions = this._getSessions(); sessions = sessions.filter(s => s.id !== id); await this._putSessions(sessions); if (this._currentSessionId === id) { this._currentSessionId = null; this._agent.resetConversation(); await this._context.globalState.update(SidebarChatProvider.activeSessionStateKey, null); await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, null); await this._context.globalState.update(SidebarChatProvider.blankChatStateKey, true); this.clearChat(); } await this._sendSessionList(); } private _getSessions(): ChatSession[] { const rawSessions = this._context.globalState.get('chat_sessions', []) || []; return rawSessions .map((session, index): ChatSession | null => { const history = Array.isArray(session?.history) ? session.history.filter((message: any) => message && (message.role === 'user' || message.role === 'assistant' || message.role === 'system') && message.content !== undefined ) : []; if (!session?.id || history.length === 0) return null; const firstMsg = history.find((message: ChatMessage) => message.role === 'user')?.content; const fallbackTitle = typeof firstMsg === 'string' ? firstMsg.substring(0, 50).replace(/\n/g, ' ') + (firstMsg.length > 50 ? '...' : '') : `Chat ${index + 1}`; return { id: String(session.id), title: String(session.title || fallbackTitle), timestamp: typeof session.timestamp === 'number' ? session.timestamp : Date.now(), history, brainProfileId: String(session.brainProfileId || getActiveBrainProfile().id), negativePrompt: String(session.negativePrompt || '') }; }) .filter((session): session is ChatSession => !!session) .sort((a, b) => b.timestamp - a.timestamp) .slice(0, 50); } private async _putSessions(sessions: ChatSession[]) { await this._context.globalState.update('chat_sessions', sessions.slice(0, 50)); } private async _sendConfig() { if (!this._view) return; const config = getConfig(); this._view.webview.postMessage({ type: 'configUpdate', value: { multiAgentEnabled: config.multiAgentEnabled } }); } private async _sendBrainStatus() { if (!this._view) return; const activeBrain = getActiveBrainProfile(); const brainDir = activeBrain.localBrainPath; const files = findBrainFiles(brainDir); this._view.webview.postMessage({ type: 'brainStatus', value: { count: files.length, path: brainDir, name: activeBrain.name, description: activeBrain.description || '', repo: activeBrain.secondBrainRepo || '' } }); } private async _sendBrainProfiles() { if (!this._view) return; const activeBrain = getActiveBrainProfile(); this._currentSessionBrainId = this._currentSessionBrainId || activeBrain.id; const profiles = getBrainProfiles().map((profile) => ({ id: profile.id, name: profile.name, path: profile.localBrainPath, description: profile.description || '', repo: profile.secondBrainRepo || '' })); this._view.webview.postMessage({ type: 'brainProfiles', value: { activeBrainId: activeBrain.id, profiles } }); } private async _setActiveBrainProfile(profileId: string, silent: boolean = false) { const profiles = getBrainProfiles(); const nextProfile = profiles.find((profile) => profile.id === profileId) || profiles[0]; if (!nextProfile) return; await vscode.workspace.getConfiguration('g1nation').update('activeBrainId', nextProfile.id, vscode.ConfigurationTarget.Global); this._currentSessionBrainId = nextProfile.id; await this._sendBrainProfiles(); await this._sendBrainStatus(); if (!silent) { this.injectSystemMessage(`**[Brain Switched]** ${nextProfile.name}\n\`${nextProfile.localBrainPath}\``); } } private async _manageBrains() { const activeBrain = getActiveBrainProfile(); const choice = await vscode.window.showQuickPick([ { label: 'Add Brain Folder', description: 'Create a new brain profile from a local folder' }, { label: 'Open Active Brain Folder', description: activeBrain.localBrainPath }, { label: 'Open Brain Settings', description: 'Edit names, paths, repos, and descriptions' } ], { placeHolder: `Active Brain: ${activeBrain.name} (${activeBrain.localBrainPath})` }); if (!choice) return; if (choice.label === 'Add Brain Folder') { await this._addBrainProfile(); return; } if (choice.label === 'Open Active Brain Folder') { if (!fs.existsSync(activeBrain.localBrainPath)) { fs.mkdirSync(activeBrain.localBrainPath, { recursive: true }); } await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.file(activeBrain.localBrainPath)); return; } vscode.commands.executeCommand('workbench.action.openSettings', 'g1nation.brainProfiles'); } private async _addBrainProfile() { const selected = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, openLabel: 'Use as Brain' }); const folder = selected?.[0]?.fsPath; if (!folder) return; const defaultName = path.basename(folder) || 'New Brain'; const name = await vscode.window.showInputBox({ prompt: 'Name this brain profile', value: defaultName, validateInput: (value) => value.trim() ? null : 'Brain name is required.' }); if (!name) return; const description = await vscode.window.showInputBox({ prompt: 'Optional description shown in the G1nation sidebar', value: '' }); const repo = await vscode.window.showInputBox({ prompt: 'Optional Second Brain Git repository URL', value: '' }); // Read raw settings directly to avoid virtual default-brain (injected in memory by getConfig()) // being saved into the settings file and corrupting the profile list on next load. const cfg = vscode.workspace.getConfiguration('g1nation'); const existingRaw: any[] = cfg.get('brainProfiles', []) || []; const idBase = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'brain'; let id = idBase; let suffix = 2; while (existingRaw.some((p: any) => p.id === id)) { id = `${idBase}-${suffix++}`; } const newProfile = { id, name: name.trim(), localBrainPath: folder, secondBrainRepo: (repo || '').trim(), description: (description || '').trim() }; const nextProfiles = [...existingRaw, newProfile]; await cfg.update('brainProfiles', nextProfiles, vscode.ConfigurationTarget.Global); await cfg.update('activeBrainId', id, vscode.ConfigurationTarget.Global); this._currentSessionBrainId = id; // Directly post the freshly-built profile list to the webview. // cfg.update() is async and VSCode's config cache may not reflect the new value // immediately, so we avoid re-reading via getBrainProfiles() here. if (this._view) { this._view.webview.postMessage({ type: 'brainProfiles', value: { activeBrainId: id, profiles: nextProfiles.map((p: any) => ({ id: p.id || '', name: p.name || '', path: p.localBrainPath || '', description: p.description || '', repo: p.secondBrainRepo || '' })) } }); } await this._sendBrainStatus(); this.injectSystemMessage(`**[Brain Added]** ${name.trim()}\n\`${folder}\``); } // --- BridgeInterface Methods --- public injectSystemMessage(msg: string): void { this._view?.webview.postMessage({ type: 'streamStart' }); this._view?.webview.postMessage({ type: 'streamChunk', value: msg }); this._view?.webview.postMessage({ type: 'streamEnd' }); } public getHistoryText(): string { return "Conversation history placeholder for evaluation."; } public sendPromptFromExtension(prompt: string): void { if (this._view) { this._view.show?.(true); this._view.webview.postMessage({ type: 'injectPrompt', value: prompt }); } } public findBrainFiles(dir: string): string[] { return findBrainFiles(dir); } // --- End BridgeInterface --- public focusInput() { this._view?.webview.postMessage({ type: 'focusInput' }); } public clearChat() { this._view?.webview.postMessage({ type: 'clearChat' }); } public async syncBrain() { const activeBrain = getActiveBrainProfile(); const brainDir = activeBrain.localBrainPath; if (!fs.existsSync(brainDir)) { vscode.window.showErrorMessage("Second Brain directory not found."); return; } vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: "G1nation: Syncing Second Brain...", cancellable: false }, async () => { try { const { execSync } = require('child_process'); execSync(`git add .`, { cwd: brainDir }); execSync(`git commit -m "[G1-Sync] Manual knowledge update"`, { cwd: brainDir }); execSync(`git push`, { cwd: brainDir }); vscode.window.showInformationMessage("Second Brain synced successfully."); } catch (err: any) { vscode.window.showInformationMessage("Brain Synced: Local knowledge is up-to-date (no changes to push)."); } }); } private _getAgentsDir(): string { const defaultPath = 'E:\\Wiki\\Agent\\.agent\\skills'; if (fs.existsSync(defaultPath)) return defaultPath; const workspaceFolders = vscode.workspace.workspaceFolders; if (workspaceFolders) { const localPath = path.join(workspaceFolders[0].uri.fsPath, '.agent', 'skills'); if (!fs.existsSync(localPath)) { fs.mkdirSync(localPath, { recursive: true }); } return localPath; } return ''; } private async _sendAgentsList() { if (!this._view) return; const dir = this._getAgentsDir(); const agents = []; if (dir && fs.existsSync(dir)) { const files = fs.readdirSync(dir); for (const f of files) { if (f.endsWith('.md')) { agents.push({ name: f.replace('.md', ''), path: path.join(dir, f) }); } } } const lastPath = this._context.globalState.get(SidebarChatProvider.lastAgentStateKey, 'none'); this._view.webview.postMessage({ type: 'agentsList', value: agents, selected: lastPath }); } private async _handleProactiveSuggestion(context: string) { if (!this._view) return; let suggestion = ''; switch (context) { case 'settings_exploration': suggestion = 'πŸ’‘ **Tip:** λͺ¨λΈ 섀정을 μ΅œμ ν™”ν•˜μ—¬ λ‹΅λ³€ 속도λ₯Ό 2λ°° 이상 높일 수 μžˆμŠ΅λ‹ˆλ‹€. μ„€μ •μ—μ„œ `Max Context Size`λ₯Ό μ‘°μ •ν•΄λ³΄μ„Έμš”!'; break; case 'brain_sync_exploration': suggestion = '🧠 **Knowledge Sync:** μ΅œκ·Όμ— μˆ˜μ •ν•œ 파일이 지식 λ² μ΄μŠ€μ— λ°˜μ˜λ˜μ§€ μ•Šμ•˜λ‚˜μš”? μ§€κΈˆ 동기화 λ²„νŠΌμ„ 눌러 μ΅œμ‹  정보λ₯Ό μ—…λ°μ΄νŠΈν•˜μ„Έμš”.'; break; case 'agent_selection_exploration': suggestion = 'πŸ€– **Agent Skills:** νŠΉμ • μ–Έμ–΄λ‚˜ ν”„λ ˆμž„μ›Œν¬μ— νŠΉν™”λœ μ—μ΄μ „νŠΈ μŠ€ν‚¬μ„ μ„ νƒν•˜λ©΄ 더 μ •ν™•ν•œ μ½”λ“œλ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€.'; break; default: suggestion = 'πŸ’‘ μƒˆλ‘œμš΄ κΈ°λŠ₯을 λ°œκ²¬ν•˜μ…¨λ‚˜μš”? κΆκΈˆν•œ 점이 μžˆλ‹€λ©΄ μ–Έμ œλ“  λ¬Όμ–΄λ³΄μ„Έμš”!'; } this._view.webview.postMessage({ type: 'streamStart' }); this._view.webview.postMessage({ type: 'streamChunk', value: `\n\n> [!TIP]\n> ${suggestion}\n` }); this._view.webview.postMessage({ type: 'streamEnd' }); } private async _createAgent() { const name = await vscode.window.showInputBox({ prompt: 'Name of the new Agent (e.g., frontend_expert)', placeHolder: 'Agent name...' }); if (!name) return; const safeName = name.trim().replace(/[^a-zA-Z0-9_\\-\\u3131-\\uD79D]/g, '_'); if (!safeName) return; const dir = this._getAgentsDir(); if (!dir) { vscode.window.showErrorMessage('Agent directory could not be determined.'); return; } const filePath = path.join(dir, `${safeName}.md`); if (!fs.existsSync(filePath)) { fs.writeFileSync(filePath, `# Agent Persona: ${safeName}\\n\\nAdd your instructions here...\\n`, 'utf8'); } const doc = await vscode.workspace.openTextDocument(filePath); await vscode.window.showTextDocument(doc); await this._sendAgentsList(); } private async _sendAgentContent(agentPath: string) { if (!this._view || !agentPath || agentPath === 'none') return; if (fs.existsSync(agentPath)) { const content = fs.readFileSync(agentPath, 'utf8'); const negativePrompt = this._context.globalState.get(`negativePrompt:${agentPath}`, ''); this._view.webview.postMessage({ type: 'agentContent', value: content, negativePrompt: negativePrompt }); await this._context.globalState.update(SidebarChatProvider.lastAgentStateKey, agentPath); } } private async _updateAgent(agentPath: string, content: string, negativePrompt?: string) { if (!agentPath || agentPath === 'none') return; try { fs.writeFileSync(agentPath, content, 'utf8'); if (negativePrompt !== undefined) { await this._context.globalState.update(`negativePrompt:${agentPath}`, negativePrompt); } vscode.window.showInformationMessage('Agent skill updated successfully.'); } catch (err: any) { vscode.window.showErrorMessage(`Failed to update agent: ${err.message}`); } } private async _handlePrompt(data: any) { if (!this._view) return; const { value, model, internet, files, agentFile, negativePrompt } = data; this._currentNegativePrompt = negativePrompt || ''; this._currentSessionBrainId = getActiveBrainProfile().id; let agentSkillContext = undefined; if (agentFile && fs.existsSync(agentFile)) { agentSkillContext = fs.readFileSync(agentFile, 'utf8'); } try { await this._agent.handlePrompt(value, model, { internetEnabled: internet, visionContent: files, agentSkillContext, negativePrompt }); } catch (error: any) { logError('Prompt handling failed in sidebar provider.', { error: error?.message || String(error), promptPreview: summarizeText(value || '', 200) }); this._view.webview.postMessage({ type: 'error', value: error.message }); } } private async _sendModels() { if (!this._view) return; try { const config = getConfig(); const url = config.ollamaUrl; let defaultModel = config.defaultModel; let models: string[] = []; const primaryEngine = resolveEngine(url); const engines = primaryEngine === 'lmstudio' ? ['lmstudio', 'ollama'] as const : ['ollama', 'lmstudio'] as const; for (const engine of engines) { const modelsUrl = buildApiUrl(url, engine, 'models'); try { logInfo('Model discovery started.', { engine, modelsUrl }); const res = await fetch(modelsUrl, { signal: AbortSignal.timeout(5000) }); const rawText = await res.text(); if (!res.ok) { logError('Model discovery returned non-OK status.', { engine, modelsUrl, status: res.status, body: summarizeText(rawText, 300) }); continue; } const data = rawText ? JSON.parse(rawText) as any : {}; models = engine === 'lmstudio' ? (data.data || []).map((m: any) => m.id) : (data.models || []).map((m: any) => m.name); if (models.length > 0) { logInfo('Model discovery succeeded.', { engine, count: models.length, modelsPreview: models.slice(0, 5) }); break; } } catch (e: any) { logError('Model discovery failed.', { engine, modelsUrl, error: e?.message || String(e) }); } } if (models.length === 0) { models = defaultModel ? [defaultModel] : []; this._view.webview.postMessage({ type: 'engineStatus', value: { online: false, url } }); } else { this._view.webview.postMessage({ type: 'engineStatus', value: { online: true, url } }); } const baseModel = defaultModel?.replace(/:\d+$/, ''); if (baseModel && baseModel !== defaultModel && models.includes(baseModel)) { defaultModel = baseModel; await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global); } if (models.length > 0 && (!defaultModel || !models.includes(defaultModel))) { // [State Persistence Fix] μ €μž₯된 λͺ¨λΈμ΄ λͺ©λ‘μ— 없을 λ•Œ: // μ¦‰μ‹œ κ°•μ œ λ¦¬μ…‹ν•˜λŠ” λŒ€μ‹ , ν˜„μž¬ λͺ¨λΈ λͺ©λ‘μ˜ 첫 번째λ₯Ό '폴백 후보'둜만 μ‚¬μš©. // 단, defaultModel이 μ™„μ „νžˆ μ—†λŠ” κ²½μš°μ—λ§Œ μ‹€μ œλ‘œ μ €μž₯함. if (!defaultModel) { defaultModel = models[0]; await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global); } else { // μ €μž₯된 λͺ¨λΈλͺ…은 μœ μ§€ν•˜κ³ , UIμ—λŠ” 첫 번째 λͺ¨λΈμ„ λ³΄μ—¬μ£Όλ˜ // 섀정은 κ±΄λ“œλ¦¬μ§€ μ•Šμ•„ λ‹€μŒ λ²ˆμ— 같은 λͺ¨λΈμ΄ λ‹€μ‹œ λ‘œλ“œλ  경우 볡원 κ°€λŠ₯ν•˜λ„λ‘ 함 logInfo('Saved model not found in current list, using first available as fallback.', { saved: defaultModel, fallback: models[0] }); defaultModel = models[0]; } } const defaultIdx = models.indexOf(defaultModel); if (defaultIdx > 0) { models.splice(defaultIdx, 1); models.unshift(defaultModel); } this._view.webview.postMessage({ type: 'modelsList', value: { models, selected: defaultModel } }); } catch (err) { logError('Model list update failed.', err); const fallbackModel = getConfig().defaultModel; this._view.webview.postMessage({ type: 'modelsList', value: { models: fallbackModel ? [fallbackModel] : [], selected: fallbackModel } }); } } private _getHtml(webview: vscode.Webview): string { return ` G1nation
G1nation

Chat History

Analyze
Plan
Execute
Verify
Welcome to G1nation

Your premium local AI assistant.
Ready to analyze projects and build reports.

`; } }