chore: release v2.2.46 with critical bug fixes for AI communication and brain management

This commit is contained in:
Wonseok Jung
2026-04-25 19:07:15 +09:00
parent acc6c76a4f
commit 42ca873d45
6 changed files with 1176 additions and 129 deletions
+449 -52
View File
@@ -1,31 +1,57 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
import {
getConfig,
_getBrainDir,
findBrainFiles,
buildApiUrl,
getActiveBrainProfile,
getBrainProfiles,
logError,
logInfo,
resolveEngine,
summarizeText
} from './utils';
import { AgentExecutor } from './agent';
import { AgentExecutor, ChatMessage } from './agent';
import { BridgeInterface } from './bridge';
interface LastVisibleChatSnapshot {
history: ChatMessage[];
brainProfileId: string;
sessionId: string | null;
timestamp: number;
}
interface ChatSession {
id: string;
title: string;
timestamp: number;
history: ChatMessage[];
brainProfileId: 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 _view?: vscode.WebviewView;
public brainEnabled = true;
private _currentSessionBrainId: string | null = null;
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,
@@ -42,33 +68,36 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
webviewView.webview.html = this._getHtml(webviewView.webview);
this._agent.setWebview(webviewView.webview);
// Re-hydrate existing history
const currentHistory = this._agent.getHistory();
if (currentHistory.length > 0) {
setTimeout(() => {
webviewView.webview.postMessage({ type: 'restoreHistory', value: currentHistory });
}, 500);
}
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._restoreActiveSessionIntoView();
break;
case 'getModels':
await this._sendModels();
break;
case 'newChat':
this._currentSessionId = null;
this._agent.clearHistory();
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();
@@ -82,23 +111,81 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
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 'setBrainProfile':
await this._setActiveBrainProfile(data.id);
break;
case 'refreshModels':
await this._sendModels();
break;
}
});
}
private _currentSessionId: string | null = null;
private async _restoreActiveSessionIntoView() {
if (!this._view) return;
const blankChatActive = this._context.globalState.get<boolean>(SidebarChatProvider.blankChatStateKey, false);
if (blankChatActive) {
return;
}
const activeSessionId = this._currentSessionId || this._context.globalState.get<string | null>(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<LastVisibleChatSnapshot | null>(SidebarChatProvider.lastVisibleChatStateKey, null);
if (snapshot?.history?.length) {
this._currentSessionId = snapshot.sessionId || null;
this._currentSessionBrainId = snapshot.brainProfileId || getActiveBrainProfile().id;
await this._setActiveBrainProfile(this._currentSessionBrainId, true);
this._agent.setHistory(snapshot.history);
this._view.webview.postMessage({ type: 'restoreHistory', value: snapshot.history });
}
}
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()
};
await this._context.globalState.update(SidebarChatProvider.lastVisibleChatStateKey, snapshot);
}
private async _saveCurrentSession() {
const history = this._agent.getHistory();
if (history.length === 0) return;
let sessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
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();
@@ -106,66 +193,313 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
id: this._currentSessionId,
title,
timestamp: Date.now(),
history
history,
brainProfileId
});
} 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;
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
});
}
}
// Keep only last 50 sessions
if (sessions.length > 50) sessions = sessions.slice(0, 50);
await this._context.globalState.update('chat_sessions', sessions);
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._context.globalState.get<any[]>('chat_sessions', []) || [];
const list = sessions.map(s => ({ id: s.id, title: s.title, timestamp: s.timestamp }));
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) {
const sessions = this._context.globalState.get<any[]>('chat_sessions', []) || [];
private async _loadSession(id: string, skipSessionListRefresh: boolean = false): Promise<boolean> {
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._agent.setHistory(session.history);
this._view?.webview.postMessage({ type: 'clearChat' });
this._view?.webview.postMessage({ type: 'restoreHistory', value: session.history });
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
}
});
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._context.globalState.get<any[]>('chat_sessions', []) || [];
let sessions = this._getSessions();
sessions = sessions.filter(s => s.id !== id);
await this._context.globalState.update('chat_sessions', sessions);
await this._putSessions(sessions);
if (this._currentSessionId === id) {
this._currentSessionId = null;
this._agent.clearHistory();
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<any[]>('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)
};
})
.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 _sendBrainStatus() {
if (!this._view) return;
const brainDir = _getBrainDir();
const activeBrain = getActiveBrainProfile();
const brainDir = activeBrain.localBrainPath;
const files = findBrainFiles(brainDir);
this._view.webview.postMessage({
type: 'brainStatus',
value: {
count: files.length,
path: brainDir
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<any[]>('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 {
@@ -200,7 +534,8 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
public async syncBrain() {
const brainDir = _getBrainDir();
const activeBrain = getActiveBrainProfile();
const brainDir = activeBrain.localBrainPath;
if (!fs.existsSync(brainDir)) {
vscode.window.showErrorMessage("Second Brain directory not found.");
return;
@@ -226,6 +561,7 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
if (!this._view) return;
const { value, model, internet, files } = data;
this._currentSessionBrainId = getActiveBrainProfile().id;
try {
await this._agent.handlePrompt(value, model, {
@@ -275,13 +611,19 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
}
if (models.length === 0) {
models = [defaultModel];
models = defaultModel ? [defaultModel] : [];
this._view.webview.postMessage({ type: 'engineStatus', value: { online: false, url } });
} else {
this._view.webview.postMessage({ type: 'engineStatus', value: { online: true, url } });
}
if (models.length > 0 && !models.includes(defaultModel)) {
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))) {
defaultModel = models[0];
await vscode.workspace.getConfiguration('g1nation').update('defaultModel', defaultModel, vscode.ConfigurationTarget.Global);
}
@@ -292,10 +634,11 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
models.unshift(defaultModel);
}
this._view.webview.postMessage({ type: 'modelsList', value: models });
this._view.webview.postMessage({ type: 'modelsList', value: { models, selected: defaultModel } });
} catch (err) {
logError('Model list update failed.', err);
this._view.webview.postMessage({ type: 'modelsList', value: [getConfig().defaultModel] });
const fallbackModel = getConfig().defaultModel;
this._view.webview.postMessage({ type: 'modelsList', value: { models: fallbackModel ? [fallbackModel] : [], selected: fallbackModel } });
}
}
@@ -331,11 +674,17 @@ body::before{content:'';position:fixed;top:-50%;left:-50%;width:200%;height:200%
.thinking-bar.active{background:rgba(124,106,255,.1)}
.thinking-bar.active::after{content:'';position:absolute;top:0;left:-40%;width:40%;height:100%;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),var(--accent3),transparent);animation:thinkSlide 1.5s ease-in-out infinite}
@keyframes thinkSlide{0%{left:-40%}100%{left:100%}}
.brain-strip{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:7px 14px;border-bottom:1px solid var(--border);background:rgba(0,10,2,.64);z-index:8;position:relative}
.brain-strip-main{font-size:11px;color:var(--accent);font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.brain-strip-sub{font-size:10px;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}
.header-left{display:flex;align-items:center;gap:8px}
.logo{width:26px;height:26px;border-radius:6px;background:#050505;border:1px solid rgba(0,255,65,.3);display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--accent);box-shadow:0 0 15px rgba(0,255,65,.15);animation:logoPulse 3s ease-in-out infinite;position:relative;text-shadow:0 0 8px var(--accent)}
@keyframes logoPulse{0%,100%{box-shadow:0 0 10px rgba(0,255,65,.1)}50%{box-shadow:0 0 25px rgba(0,255,65,.3)}}
.brand{font-weight:800;font-size:14px;color:var(--text-bright);letter-spacing:-.5px;background:linear-gradient(135deg,#fff 40%,var(--accent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.header-right{display:flex;align-items:center;gap:5px}
.header-right{display:flex;align-items:center;gap:5px;flex-wrap:wrap;justify-content:flex-end}
.select-wrap{display:flex;align-items:center;gap:5px}
.select-wrap select{max-width:140px}
.brain-meta{font-size:10px;color:var(--text-dim);max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.history-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);backdrop-filter:blur(15px);z-index:100;display:none;flex-direction:column;padding:20px;animation:fadeIn 0.3s ease-out}
.history-overlay.visible{display:flex}
.history-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}
@@ -382,6 +731,7 @@ select:hover,select:focus{border-color:var(--accent);box-shadow:0 0 12px var(--a
.loading-dots span:nth-child(3){animation-delay:.4s;background:var(--accent3)}
@keyframes dotBounce{0%,80%,100%{transform:scale(.6);opacity:.4}40%{transform:scale(1.2);opacity:1}}
.loading-text{font-size:11px;color:var(--text-dim);animation:pulse 2s ease-in-out infinite;letter-spacing:.3px}
.auto-status{margin:12px 0 4px 29px;padding:10px 12px;border:1px solid rgba(255,255,255,.16);border-radius:10px;background:rgba(0,255,65,.08);color:#fff;font-size:13px;font-weight:700;line-height:1.5;box-shadow:0 0 18px rgba(0,255,65,.12)}
.input-wrap{padding:8px 14px 14px;flex-shrink:0;position:relative;z-index:1}
.input-box{background:var(--input-bg);border:1px solid var(--border2);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;transition:all .3s;position:relative;backdrop-filter:blur(12px)}
.input-box:focus-within{border-color:var(--accent);box-shadow:0 0 24px rgba(0,255,65,.15)}
@@ -414,19 +764,19 @@ body.init .input-wrap{max-width:680px;width:100%;margin:0 auto}
.regen-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:none;color:var(--text-dim);padding:4px 6px;border-radius:4px;font-size:11px;cursor:pointer;transition:color 0.2s;margin-top:6px;margin-left:29px;opacity:0.7}
.regen-btn:hover{color:var(--text);opacity:1}
</style></head><body class="init">
<div class="header"><div class="header-left"><div class="logo">✦</div><span class="brand">G1nation</span></div><div class="header-right"><div id="engineStatusDot" style="width:8px;height:8px;border-radius:50%;background:var(--text-dim);margin-right:-2px" title="Checking AI Engine..."></div><select id="modelSel"></select><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠<span id="brainCountBadge" style="position:absolute;top:-5px;right:-5px;background:var(--accent);color:#000;font-size:8px;padding:1px 3px;border-radius:4px;display:none;font-weight:bold">0</span></button><button class="btn-icon" id="historyBtn" title="Chat History">📜</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
<div class="header"><div class="header-left"><div class="logo">✦</div><span class="brand">G1nation</span></div><div class="header-right"><div id="engineStatusDot" style="width:8px;height:8px;border-radius:50%;background:var(--text-dim);margin-right:-2px" title="Checking AI Engine..."></div><div class="select-wrap"><select id="modelSel" title="Active AI model"></select><button class="btn-icon" id="refreshModelsBtn" title="Refresh Models">↻</button></div><div class="select-wrap"><select id="brainSel" title="Active Second Brain"></select><button class="btn-icon" id="manageBrainsBtn" title="Add or manage brains">✎</button></div><button class="btn-icon" id="internetBtn" title="Internet Access: OFF">🌐</button><button class="btn-icon" id="brainBtn" title="Sync Second Brain">🧠<span id="brainCountBadge" style="position:absolute;top:-5px;right:-5px;background:var(--accent);color:#000;font-size:8px;padding:1px 3px;border-radius:4px;display:none;font-weight:bold">0</span></button><button class="btn-icon" id="historyBtn" title="Chat History">📜</button><button class="btn-icon" id="settingsBtn" title="Settings">⚙️</button><button class="btn-icon" id="newChatBtn" title="New Chat">+</button></div></div>
<div id="historyOverlay" class="history-overlay">
<div class="history-header"><span class="history-title">Chat Sessions</span><button class="btn-icon" id="closeHistoryBtn">✕</button></div>
<div class="history-list" id="historyList"></div>
</div>
<div class="thinking-bar" id="thinkingBar"></div>
<div class="brain-strip"><div id="brainStatusInfo" class="brain-strip-main">Brain: checking...</div><div id="brainStatusMeta" class="brain-strip-sub"></div></div>
<div class="main-view" id="mainView">
<div class="chat" id="chat">
<div class="welcome">
<div class="welcome-logo">✦</div>
<div class="welcome-title">G1nation</div>
<div class="welcome-sub">Security · Optimized · Knowledge Mesh<br>Understands your project, writes code, and executes tasks.</div>
<div id="brainStatusInfo" style="margin-top:10px;font-size:11px;color:var(--accent);opacity:0.8;display:none"></div>
<div class="welcome-sub">Security · Optimized · Knowledge Mesh<br>Choose a brain above, then ask what it knows.</div>
</div></div>
<div class="input-wrap"><div class="input-box">
<div class="attach-preview" id="attachPreview"></div>
@@ -439,7 +789,7 @@ body.init .input-wrap{max-width:680px;width:100%;margin:0 auto}
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'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
modelSel=document.getElementById('modelSel'),brainSel=document.getElementById('brainSel'),refreshModelsBtn=document.getElementById('refreshModelsBtn'),manageBrainsBtn=document.getElementById('manageBrainsBtn'),newChatBtn=document.getElementById('newChatBtn'),settingsBtn=document.getElementById('settingsBtn'),brainBtn=document.getElementById('brainBtn'),
historyBtn=document.getElementById('historyBtn'),historyOverlay=document.getElementById('historyOverlay'),closeHistoryBtn=document.getElementById('closeHistoryBtn'),historyList=document.getElementById('historyList'),
internetBtn=document.getElementById('internetBtn'),attachBtn=document.getElementById('attachBtn'),fileInput=document.getElementById('fileInput'),attachPreview=document.getElementById('attachPreview'),
thinkingBar=document.getElementById('thinkingBar');
@@ -447,6 +797,9 @@ let loader=null,sending=false,pendingFiles=[],internetEnabled=false;
historyBtn.addEventListener('click', ()=>historyOverlay.classList.add('visible'));
closeHistoryBtn.addEventListener('click', ()=>historyOverlay.classList.remove('visible'));
refreshModelsBtn.addEventListener('click', ()=>vscode.postMessage({type:'refreshModels'}));
manageBrainsBtn.addEventListener('click', ()=>vscode.postMessage({type:'manageBrains'}));
brainSel.addEventListener('change', ()=>vscode.postMessage({type:'setBrainProfile',id:brainSel.value}));
internetBtn.addEventListener('click', ()=>{
internetEnabled=!internetEnabled;
@@ -467,6 +820,27 @@ function fmt(t){
return formatted;
}
function normalizeMsgContent(content){
if(Array.isArray(content)) return content.map(part=>typeof part==='string'?part:(part?.text||part?.name||JSON.stringify(part))).join('\\n');
return String(content||'');
}
function resetChatView(){
chat.innerHTML='';
streamBody=null;hideLoader();setSending(false);
document.body.classList.remove('init');
}
function renderHistory(history){
resetChatView();
(history||[]).filter(m => !m.internal).forEach(m => addMsg(normalizeMsgContent(m.content), m.role === 'assistant' ? 'ai' : 'user'));
if((history||[]).length===0){
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">No messages in this session.</div></div>';
document.body.classList.add('init');
}
chat.scrollTop=chat.scrollHeight;
}
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':'');
@@ -488,7 +862,7 @@ function send(){
const w=document.querySelector('.welcome');if(w)w.remove();
addMsg(text,'user');
input.value='';input.style.height='auto';setSending(true);showLoader();
vscode.postMessage({type:'prompt',value:text,model:modelSel.value,internet:internetEnabled,files:pendingFiles});
vscode.postMessage({type:'prompt',value:text,model:modelSel.value,internet:internetEnabled,files:pendingFiles.length?pendingFiles:undefined});
pendingFiles=[];renderPreview();
}
@@ -525,22 +899,32 @@ settingsBtn.addEventListener('click',()=>vscode.postMessage({type:'openSettings'
brainBtn.addEventListener('click',()=>vscode.postMessage({type:'syncBrain'}));
stopBtn.addEventListener('click',()=>{vscode.postMessage({type:'stopGeneration'});hideLoader();setSending(false);});
let streamBody=null;
let streamBody=null,sessionCache={};
window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
case 'restoreHistory':
chat.innerHTML='';
document.body.classList.remove('init');
msg.value.filter(m => !m.internal).forEach(m => addMsg(m.content, m.role === 'assistant' ? 'ai' : 'user'));
renderHistory(msg.value||[]);
break;
case 'sessionLoaded':
renderHistory(msg.value.history||[]);
historyOverlay.classList.remove('visible');
break;
case 'sessionList':
historyList.innerHTML='';
sessionCache={};
if(!msg.value || msg.value.length===0){
historyList.innerHTML='<div class="history-item"><div class="history-item-info"><div class="history-item-title">No saved chats yet</div><div class="history-item-date">Start a conversation and it will appear here.</div></div></div>';
break;
}
msg.value.forEach(s=>{
sessionCache[s.id]=s;
const el=document.createElement('div');el.className='history-item';
el.innerHTML='<div class="history-item-info"><div class="history-item-title">'+esc(s.title)+'</div><div class="history-item-date">'+new Date(s.timestamp).toLocaleString()+'</div></div><button class="history-del-btn" title="Delete">🗑</button>';
el.dataset.sessionId=s.id;
el.innerHTML='<div class="history-item-info"><div class="history-item-title">'+esc(s.title)+'</div><div class="history-item-date">'+new Date(s.timestamp).toLocaleString()+' · '+(s.messageCount||0)+' messages</div></div><button class="history-del-btn" title="Delete">🗑</button>';
el.addEventListener('click',(e)=>{
if(e.target.classList.contains('history-del-btn')) return;
if(e.target.closest('.history-del-btn')) return;
const cached=sessionCache[s.id];
if(cached && cached.history){renderHistory(cached.history);historyOverlay.classList.remove('visible');}
vscode.postMessage({type:'loadSession',id:s.id});
historyOverlay.classList.remove('visible');
});
el.querySelector('.history-del-btn').addEventListener('click',()=>{
vscode.postMessage({type:'deleteSession',id:s.id});
@@ -563,7 +947,16 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
break;
case 'modelsList':
modelSel.innerHTML='';
msg.value.forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
(msg.value.models||[]).forEach(m=>{const o=document.createElement('option');o.value=m;o.textContent=m;modelSel.appendChild(o)});
if(msg.value.selected) modelSel.value=msg.value.selected;
break;
case 'brainProfiles':
brainSel.innerHTML='';
(msg.value.profiles||[]).forEach(profile=>{
const o=document.createElement('option');o.value=profile.id;o.textContent=profile.name;brainSel.appendChild(o);
o.title=profile.path;
});
if(msg.value.activeBrainId) brainSel.value=msg.value.activeBrainId;
break;
case 'engineStatus':
const dot = document.getElementById('engineStatusDot');
@@ -575,18 +968,24 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
case 'brainStatus':
const badge = document.getElementById('brainCountBadge');
const info = document.getElementById('brainStatusInfo');
const meta = document.getElementById('brainStatusMeta');
if(badge){
badge.innerText = msg.value.count > 999 ? '999+' : msg.value.count;
badge.style.display = msg.value.count > 0 ? 'block' : 'none';
}
if(info){
info.innerText = \`🧠 Second Brain: \${msg.value.count} knowledge files connected\`;
info.style.display = 'block';
info.innerText = \`Brain: \${msg.value.name} · \${msg.value.count} files\`;
info.title = msg.value.path;
}
const brainBtn = document.getElementById('brainBtn');
if(brainBtn) brainBtn.title = \`Sync Brain (Connected: \${msg.value.count} files at \${msg.value.path})\`;
if(meta){
meta.innerText = msg.value.description ? \`\${msg.value.description} · \${msg.value.path}\` : msg.value.path;
meta.title = msg.value.path;
}
const syncBrainBtn = document.getElementById('brainBtn');
if(syncBrainBtn) syncBrainBtn.title = \`Sync Brain: \${msg.value.name} (\${msg.value.count} files at \${msg.value.path})\`;
break;
case 'clearChat':
streamBody=null;hideLoader();setSending(false);
chat.innerHTML='<div class="welcome"><div class="welcome-logo">✦</div><div class="welcome-title">G1nation</div><div class="welcome-sub">System Cleaned.</div></div>';
document.body.classList.add('init');
break;
@@ -594,11 +993,9 @@ window.addEventListener('message',e=>{const msg=e.data;switch(msg.type){
input.value=msg.value;send();
break;
case 'autoContinue':
showLoader();
showLoader();setSending(true);
const hint = document.createElement('div');
hint.className = 'input-hint';
hint.style.textAlign = 'center';
hint.style.margin = '10px 0';
hint.className = 'auto-status';
hint.innerText = msg.value;
chat.appendChild(hint);
chat.scrollTop = chat.scrollHeight;