chore: release v2.2.46 with critical bug fixes for AI communication and brain management
This commit is contained in:
+449
-52
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user