replica-omnisciente/extensions/aurelio-vscode/src/webview/controlCenterPanel.ts

2646 lines
130 KiB
TypeScript

import * as vscode from 'vscode';
import { getRemoteEdaClient } from '../electronics/remoteEdaClient';
import * as path from 'path';
import * as fs from 'fs';
import axios from 'axios';
import { McpServerManager } from '../mcp/McpServerManager';
import { ConfigResolver, WorkflowEntry } from '../config/configResolver';
import { ServiceRegistry } from '../core/ServiceRegistry';
import { HeteronimoManager } from '../models/heteronimoManager';
import { BrainManager } from '../brain/brainManager';
import { Coordinator } from '../coordinator/coordinator';
import { WorkflowManager } from '../workflow/workflowManager';
import { EvolutionOrchestrator } from '../evolution/orchestrator';
import { HardwareDashboardPanel } from './hardwareDashboard';
import { HilDashboardPanel } from './hilDashboard';
import { SourcingDashboardPanel } from './sourcingDashboard';
import { GuardaLivrosDashboardPanel } from './guardaLivrosDashboard';
import { CoverageDashboardPanel } from './coverageDashboard';
import { PdfViewerPanel } from './pdfViewerPanel';
import { ConfigManager } from '../core/configManager';
import { ApprovalGate } from '../utils/approvalGate';
import { ChatPanel } from './chatPanel';
import { AntigravityService } from '../antigravity/antigravityService';
import { AntigravityLsBridge } from '../antigravity/antigravityLsBridge';
import { VertexAuthProvider } from '../models/vertexAuth';
import { JulesClient } from '../models/julesClient';
export class ControlCenterPanel {
public static currentPanel: ControlCenterPanel | undefined;
private readonly _panel: vscode.WebviewPanel;
private readonly _extensionUri: vscode.Uri;
private readonly _skills: any[];
private readonly _brainManager: BrainManager;
private readonly _heteronimoManager: HeteronimoManager;
private readonly _chatPanel?: ChatPanel;
private _chatSinkDisposable?: vscode.Disposable;
private _disposables: vscode.Disposable[] = [];
private _approvalRequestedListener?: (req: any) => void;
private _approvalResolvedListener?: (req: any) => void;
private _antigravityService: AntigravityService;
private _antigravityLsBridge: AntigravityLsBridge;
private _vertexAuth: VertexAuthProvider;
private _julesClient: JulesClient;
private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri, skills: any[], brainManager: BrainManager, heteronimoManager: HeteronimoManager, chatPanel?: ChatPanel) {
this._panel = panel;
this._extensionUri = extensionUri;
this._skills = skills;
this._brainManager = brainManager;
this._heteronimoManager = heteronimoManager;
this._chatPanel = chatPanel;
// Register this webview as an external sink so ChatPanel streams here too
if (this._chatPanel) {
this._chatSinkDisposable = this._chatPanel.registerExternalSink(this._panel.webview);
}
// Initialize Antigravity service
this._antigravityService = new AntigravityService();
this._antigravityLsBridge = new AntigravityLsBridge();
this._vertexAuth = new VertexAuthProvider();
this._julesClient = new JulesClient();
this._update();
this._panel.onDidDispose(() => this.dispose(), null, this._disposables);
ServiceRegistry.getInstance().get(McpServerManager).onDidChangeServers(() => {
this._updateMcpStatus();
}, null, this._disposables);
// ── Auto-detect hardware projects via FileSystemWatcher ──
// Watch for EDA file creation/deletion across all workspace folders
const edaGlob = '**/*.{kicad_sch,kicad_pcb,SchDoc,PcbDoc,brd,sch,dsn}';
const edaWatcher = vscode.workspace.createFileSystemWatcher(edaGlob);
let edaScanDebounce: ReturnType<typeof setTimeout> | null = null;
const debouncedRescan = () => {
if (edaScanDebounce) { clearTimeout(edaScanDebounce); }
edaScanDebounce = setTimeout(() => this._handleElecScanProjects({}), 2000);
};
edaWatcher.onDidCreate(debouncedRescan, null, this._disposables);
edaWatcher.onDidDelete(debouncedRescan, null, this._disposables);
this._disposables.push(edaWatcher);
try {
const wfm = ServiceRegistry.getInstance().get(WorkflowManager);
wfm.onActiveLoopChanged(() => {
this._updateOrchestrationStatus();
this._wireEvolutionTelemetry();
}, null, this._disposables);
} catch { /* ignored */ }
this._approvalRequestedListener = (req: any) => {
this._panel.webview.postMessage({
command: 'approvalRequested',
approval: { id: req.id, action: req.actionName, target: req.actionDetails }
});
};
this._approvalResolvedListener = (req: any) => {
this._panel.webview.postMessage({
command: 'approvalResolved',
approval: { id: req.id, approved: req.approved }
});
};
ApprovalGate.events.on('requestApproval', this._approvalRequestedListener);
ApprovalGate.events.on('resolveApproval', this._approvalResolvedListener);
// Handle messages from the webview
this._panel.webview.onDidReceiveMessage(
async message => {
switch (message.command) {
case 'reconnectMcp':
try {
vscode.window.showInformationMessage(`Reconnecting ${message.serverName}...`);
await ServiceRegistry.getInstance().get(McpServerManager).reconnectServer(message.serverName);
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to reconnect to ${message.serverName}: ${e.message}`);
}
return;
case 'connectAllMcp':
try {
vscode.window.showInformationMessage('Connecting all MCP servers...');
await ServiceRegistry.getInstance().get(McpServerManager).connectAll();
vscode.window.showInformationMessage('All MCP servers reconnected.');
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to connect all: ${e.message}`);
}
return;
case 'pingMcp':
try {
const result = await ServiceRegistry.getInstance().get(McpServerManager).getAvailableTools();
vscode.window.showInformationMessage(`Tested connection to ${message.serverName}: Found ${result.length} tools`);
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to connect to ${message.serverName}: ${e.message}`);
}
return;
case 'saveMcpConfig':
try {
const configText = message.config;
const parsed = JSON.parse(configText);
await ServiceRegistry.getInstance().get(McpServerManager).updateServerConfig(message.serverName, parsed);
vscode.window.showInformationMessage(`Saved configuration for ${message.serverName}`);
this._updateMcpStatus();
} catch(e: any) {
vscode.window.showErrorMessage(`Invalid JSON for ${message.serverName}: ${e.message}`);
}
return;
case 'saveKnowledgeSettings':
try {
await vscode.workspace.getConfiguration('aurelio').update('knowledgeSettings', message.settings, vscode.ConfigurationTarget.Global);
vscode.window.showInformationMessage('Knowledge settings saved successfully.');
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to save knowledge settings: ${e.message}`);
}
return;
case 'toggleMcp':
await ServiceRegistry.getInstance().get(McpServerManager).toggleServer(message.serverName, message.enabled);
this._updateMcpStatus();
return;
case 'resolveApproval':
try {
ApprovalGate.events.emit('resolveApproval', { id: message.approvalId, approved: message.approved });
} catch (err: any) {
vscode.window.showErrorMessage(`Failed to resolve approval: ${err.message}`);
}
return;
case 'toggleMcpTool':
await ServiceRegistry.getInstance().get(McpServerManager).toggleTool(message.serverName, message.toolName, message.enabled);
this._updateMcpStatus();
return;
case 'mcp:call':
try {
const manager = ServiceRegistry.getInstance().get(McpServerManager);
const result = await manager.callTool(message.server, message.tool, message.args);
this._panel.webview.postMessage({ command: 'mcp:result', tool: message.tool, result });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'mcp:error', tool: message.tool, error: e.message });
vscode.window.showErrorMessage(`MCP call failed for ${message.tool}: ${e.message}`);
}
return;
case 'openWorkflow':
try {
const doc = await vscode.workspace.openTextDocument(message.filePath);
await vscode.window.showTextDocument(doc, vscode.ViewColumn.One);
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to open workflow: ${e.message}`);
}
return;
case 'addRecommendedMcp':
try {
vscode.window.showInformationMessage(`Enabling recommended node: ${message.serverName}...`);
await ServiceRegistry.getInstance().get(McpServerManager).addRecommendedServer(message.serverName);
vscode.window.showInformationMessage(`Successfully registered ${message.serverName}.`);
this._updateMcpStatus();
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to add ${message.serverName}: ${e.message}`);
}
return;
case 'openArtifact':
try {
const [sessionId, artifactName] = message.filePath.split('/');
vscode.commands.executeCommand('aurelio.previewArtifact', sessionId, artifactName);
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to open artifact: ${e.message}`);
}
return;
case 'deleteSession':
const res = await vscode.window.showWarningMessage(
`Are you sure you want to delete session ${message.sessionId}? This cannot be undone.`,
{ modal: true },
'Delete'
);
if (res === 'Delete') {
this._brainManager.deleteConversation(message.sessionId);
this._updateSessions();
}
return;
case 'reviewSession':
const logEntries = this._brainManager.parseSessionLog(message.sessionId);
this._panel.webview.postMessage({ command: 'showSessionReview', entries: logEntries, sessionId: message.sessionId });
return;
case 'requestSessions':
this._updateSessions();
return;
case 'addArtifactComment':
this._brainManager.addComment(message.sessionId, message.artifactName, {
text: message.text,
comment: message.comment,
author: message.author || 'User'
});
return;
case 'syncKnowledge':
await this._handleSyncKnowledge(message.settings);
return;
case 'openKnowledgeItem':
await this._handleOpenKnowledgeItem(message.path);
return;
case 'extractKnowledgeItem':
await this._handleExtractKnowledgeItem(message.item);
return;
case 'ingestObsidian':
await this._handleIngestObsidian(message.vaultPath);
return;
case 'ingestData':
await this._handleIngestData(message.folderPath);
return;
case 'queryKnowledge':
await this._handleQueryKnowledge(message.query, message.settings);
return;
case 'syncGoogleKeep':
await this._handleSyncGoogleKeep();
return;
case 'optimizeVault':
await this._handleOptimizeVault(message.vaultPath);
return;
case 'setServerRealms':
try {
await ServiceRegistry.getInstance().get(McpServerManager).setRealmAssignment(message.serverName, message.realms);
vscode.window.showInformationMessage(`Updated realm assignments for ${message.serverName}`);
this._updateMcpStatus();
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to update realms: ${e.message}`);
}
return;
case 'webviewReady':
this._updateMcpStatus();
this._updateChronicles();
this._updateWorkflows();
this._updateSessions();
this._updateOrchestrationStatus();
this._updateSettings();
this._updateHeteronyms();
this._wireEvolutionTelemetry();
this._pushVertexStatus();
this._pushJulesStatus();
this._pushEnabledPanels();
// Auto-detect hardware projects on panel open
this._handleElecScanProjects({});
return;
case 'openHardwareDashboard':
HardwareDashboardPanel.createOrShow(this._extensionUri);
return;
case 'openHilDashboard':
HilDashboardPanel.createOrShow(this._extensionUri);
return;
case 'openSourcingDashboard':
SourcingDashboardPanel.createOrShow(this._extensionUri);
return;
case 'openGuardaLivros':
GuardaLivrosDashboardPanel.createOrShow(this._extensionUri);
return;
case 'openTests':
CoverageDashboardPanel.createOrShow(this._extensionUri);
return;
case 'openExternal':
vscode.env.openExternal(vscode.Uri.parse(message.url));
return;
// ─── Electronics Dashboard ─────────────────────────
case 'electronicsSearch':
this._handleElecSearch(message);
return;
case 'electronicsGetDetail':
this._handleElecDetail(message);
return;
case 'electronicsAlternates':
this._handleElecAlternates(message);
return;
case 'electronicsOptimizeBom':
this._handleElecBom(message);
return;
case 'electronicsRunSpice':
this._handleElecSpice(message);
return;
case 'electronicsRunErc':
this._handleElecErc(message);
return;
case 'electronicsExtractBom':
this._handleElecExtractBom(message);
return;
case 'electronicsAnalyzePower':
this._handleElecPowerRails(message);
return;
case 'electronicsScanProjects':
this._handleElecScanProjects(message);
return;
case 'electronicsGetDatasheet':
this._handleElecGetDatasheet(message);
return;
case 'electronicsValidateReplacement':
this._handleElecValidateReplacement(message);
return;
case 'electronicsConvertToSpice':
this._handleElecConvertToSpice(message);
return;
case 'fetchHilLogs':
await this._handleFetchHilLogs();
return;
case 'fetchChronicles':
case 'requestChronicles':
await this._updateChronicles();
return;
case 'fetchWorkflows':
await this._updateWorkflows();
return;
case 'startSession':
vscode.commands.executeCommand('aurelio.coordinator.start');
return;
case 'openMcpConfig':
{
const configPath = ServiceRegistry.getInstance().get(McpServerManager).getConfigResolver()?.findMcpConfigPath();
if (configPath) {
vscode.workspace.openTextDocument(configPath).then(doc => {
vscode.window.showTextDocument(doc);
});
} else {
vscode.window.showErrorMessage('MCP configuration file not found.');
}
}
return;
case 'requestMcpStatus':
this._updateOrchestrationStatus();
return;
case 'refreshStatus':
this._updateOrchestrationStatus();
return;
case 'resumeSession':
vscode.commands.executeCommand('aurelio.coordinator.resume', message.sessionId);
return;
case 'requestWorkflows':
this._updateWorkflows();
return;
case 'executeWorkflow':
ServiceRegistry.getInstance().get(WorkflowManager).executeWorkflow(message.slug);
return;
case 'fetchFinanceData':
case 'fetchFinanceRecords':
await this._handleFetchFinanceData();
return;
case 'fetchFleetAnalytics':
case 'fetchFleetData':
await this._handleFetchFleetData();
return;
case 'refreshEconomicForecasting':
case 'refreshForecasting':
await this._handleRefreshForecasting();
return;
// ═══ Escola (Source-Grounded Learning) ═══
case 'escolaAddSource':
await this._handleEscolaAddSource(message.sourceType);
return;
case 'escolaSend': // Svelte alias
case 'escolaChat':
await this._handleEscolaChat(
message.sessionId,
message.message,
message.personaId,
message.sourceIds,
message.persona
);
return;
case 'escolaAction':
await this._handleEscolaAction(message.action, message.sessionId);
return;
case 'escolaNewSession': // Svelte alias (newSession sends a full session object)
case 'createEscolaSession':
await this._handleCreateEscolaSession(message.session);
return;
case 'listEscolaSessions':
await this._handleListEscolaSessions();
return;
case 'openFile':
if (message.path) {
try {
const doc = await vscode.workspace.openTextDocument(message.path);
await vscode.window.showTextDocument(doc);
} catch {
vscode.window.showWarningMessage(`Could not open: ${message.path}`);
}
}
return;
case 'escolaOpenCitation':
await this._handleOpenKnowledgeItem(message.path);
return;
case 'escolaOpenTutorialAsset': {
// Resolve tutorial asset path against Arquivo de Orpheu
const orpheuRoot = this._getOrpheuRoot();
const assetFullPath = path.join(orpheuRoot, 'escola', 'tutorials', message.basePath || '', message.assetPath || '');
if (fs.existsSync(assetFullPath)) {
const ext = path.extname(assetFullPath).toLowerCase();
if (ext === '.fmu') {
// Open in the dedicated FMU Viewer panel
vscode.commands.executeCommand('aurelio.openFmuViewer', assetFullPath);
} else if (ext === '.pdf' || ext === '.pptx') {
// Open externally (VS Code can't render these natively)
vscode.env.openExternal(vscode.Uri.file(assetFullPath));
} else if (ext === '.ipynb') {
// Jupyter notebooks — open in VS Code's notebook renderer or externally
try {
const doc = await vscode.workspace.openTextDocument(assetFullPath);
await vscode.window.showTextDocument(doc);
} catch {
vscode.env.openExternal(vscode.Uri.file(assetFullPath));
}
} else {
// .mo (Modelica), .cir, .txt, etc. — open in text editor
const doc = await vscode.workspace.openTextDocument(assetFullPath);
await vscode.window.showTextDocument(doc);
}
} else {
vscode.window.showWarningMessage(`Tutorial asset not found: ${assetFullPath}`);
}
return;
}
// ═══ Svelte-only commands (no React equivalent) ═══
case 'diagnostics':
vscode.commands.executeCommand('aurelio.runDiagnostics');
return;
case 'openSession':
if (message.id) {
vscode.commands.executeCommand('aurelio.openSession', message.id);
}
return;
case 'openSkill':
if (message.name) {
vscode.commands.executeCommand('aurelio.openSkill', message.name);
}
return;
case 'rebootDevice':
if (message.id) {
vscode.commands.executeCommand('aurelio.rebootDevice', message.id);
}
return;
case 'refreshFleet':
await this._handleFetchFleetData();
return;
case 'refreshSessions':
this._updateSessions();
return;
case 'saveAurelioSettings':
if (message.settings) {
const config = vscode.workspace.getConfiguration('aurelio');
for (const [key, value] of Object.entries(message.settings as Record<string, unknown>)) {
config.update(key, value, vscode.ConfigurationTarget.Global);
}
}
return;
case 'syncBrainNow':
vscode.commands.executeCommand('aurelio.syncBrain');
return;
// ═══ Wiki Sync ═══
case 'wikiSync':
vscode.commands.executeCommand('aurelio.wikiSync');
return;
case 'wikiPush':
vscode.commands.executeCommand('aurelio.wikiPush');
return;
case 'wikiPull':
vscode.commands.executeCommand('aurelio.wikiPull');
return;
case 'wikiExportSession':
vscode.commands.executeCommand('aurelio.wikiExportSession');
return;
case 'wikiStatus': {
// Import WikiSync dynamically to get status
try {
const { WikiSync } = await import('../sync/wikiSync');
const ws = new WikiSync();
ws.discoverWikiRepo();
const status = ws.getStatus();
this._panel.webview.postMessage({
type: 'wikiStatusUpdate',
linked: status.linked,
repoPath: status.repoPath,
pendingChanges: status.pendingChanges,
});
ws.dispose();
} catch {
this._panel.webview.postMessage({
type: 'wikiStatusUpdate',
linked: false,
repoPath: null,
pendingChanges: 0,
});
}
return;
}
// ═══ Antigravity (Proxmox CDP Bridge) ═══
case 'antigravityConnect':
try {
const agCfg = AntigravityService.resolveConfig();
if (message.host) { agCfg.host = message.host; }
if (message.port) { agCfg.port = message.port; }
if (message.apiKey) { agCfg.apiKey = message.apiKey; }
this._antigravityService.connect(agCfg);
const status = await this._antigravityService.getStatus();
this._panel.webview.postMessage({ command: 'antigravityStatus', status });
// Start polling
this._antigravityService.startStatusPolling(5000);
this._antigravityService.onStatusChange((s) => {
this._panel.webview.postMessage({ command: 'antigravityStatus', status: s });
});
this._antigravityService.onScreenshot((base64) => {
this._panel.webview.postMessage({ command: 'antigravityScreenshot', image: base64 });
});
} catch (e: any) {
this._panel.webview.postMessage({ command: 'antigravityStatus', status: { connected: false, error: e.message } });
}
return;
case 'antigravityGrpcConnect':
try {
const config = vscode.workspace.getConfiguration('aurelio.antigravity');
const mode = config.get<string>('mode', 'auto');
if (mode === 'native') {
const diracClient = this._chatPanel?.getDiracClient();
if (diracClient) {
vscode.window.showInformationMessage('Antigravity Native: Connected to local Dirac backend');
this._panel.webview.postMessage({ command: 'antigravityGrpcStatus', status: { connected: true, frames: 0, httpStatus: 200 } });
} else {
vscode.window.showWarningMessage('Antigravity Native: Dirac backend not running');
this._panel.webview.postMessage({ command: 'antigravityGrpcStatus', status: { connected: false, error: 'Dirac backend not running' } });
}
return;
}
if (message.autoDiscover) {
const ok = await this._antigravityLsBridge.connect();
if (ok) {
vscode.window.showInformationMessage('Antigravity gRPC: Connected to local Language Server');
const result = await this._antigravityLsBridge.call('exa.language_server_pb.LanguageServerService', 'GetStatus');
this._panel.webview.postMessage({ command: 'antigravityGrpcStatus', status: { connected: true, frames: result.frames.length, httpStatus: result.status } });
} else {
vscode.window.showWarningMessage('Antigravity gRPC: No language_server process found');
this._panel.webview.postMessage({ command: 'antigravityGrpcStatus', status: { connected: false, error: 'No LS process found' } });
}
} else {
vscode.window.showInformationMessage(`Antigravity gRPC: Manual mode — ${message.host}:${message.port} (TLS: ${message.useTls})`);
this._panel.webview.postMessage({ command: 'antigravityGrpcStatus', status: { connected: false, error: 'Manual mode not yet implemented' } });
}
} catch (e: any) {
this._panel.webview.postMessage({ command: 'antigravityGrpcStatus', status: { connected: false, error: e.message } });
}
return;
case 'antigravityDisconnect':
this._antigravityService.disconnect();
this._panel.webview.postMessage({ command: 'antigravityStatus', status: { connected: false } });
return;
case 'antigravityRefreshStatus':
try {
if (!this._antigravityService.isConnected) {
this._antigravityService.connect();
}
const agStatus = await this._antigravityService.getStatus();
this._panel.webview.postMessage({ command: 'antigravityStatus', status: agStatus });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'antigravityStatus', status: { connected: false, error: e.message } });
}
return;
case 'antigravityScreenshot':
try {
if (!this._antigravityService.isConnected) { this._antigravityService.connect(); }
const screenshot = await this._antigravityService.takeScreenshot();
this._panel.webview.postMessage({ command: 'antigravityScreenshot', image: screenshot });
} catch (e: any) {
console.error('[Antigravity] Screenshot error:', e.message);
}
return;
case 'antigravityStartScreencast':
try {
if (!this._antigravityService.isConnected) { this._antigravityService.connect(); }
this._antigravityService.startScreencastPolling(message.interval ?? 3000);
} catch (e: any) {
console.error('[Antigravity] Screencast start error:', e.message);
}
return;
case 'antigravityStopScreencast':
this._antigravityService.stopScreencastPolling();
return;
case 'antigravitySendPrompt':
try {
const config = vscode.workspace.getConfiguration('aurelio.antigravity');
const mode = config.get<string>('mode', 'auto');
this._panel.webview.postMessage({ command: 'antigravityChatStarted', prompt: message.prompt });
if (mode === 'native') {
const diracClient = this._chatPanel?.getDiracClient();
if (!diracClient) {
throw new Error('Native Dirac backend is not available. Please open the Chat view first to initialize it.');
}
let fullResponse = '';
await diracClient.sendRequest('ag.sendPrompt', { text: message.prompt, model_id: 2 }, (res) => {
if (res.msg) {
fullResponse += res.msg;
this._panel.webview.postMessage({ command: 'antigravityChatChunk', chunk: res.msg });
}
});
this._panel.webview.postMessage({ command: 'antigravityChatComplete', response: fullResponse });
} else {
if (!this._antigravityService.isConnected) { this._antigravityService.connect(); }
const fullResponse = await this._antigravityService.sendPromptStream(
message.prompt,
(chunk) => {
this._panel.webview.postMessage({ command: 'antigravityChatChunk', chunk });
}
);
this._panel.webview.postMessage({ command: 'antigravityChatComplete', response: fullResponse });
}
} catch (e: any) {
this._panel.webview.postMessage({ command: 'antigravityChatError', error: e.message });
}
return;
case 'antigravityQuickAction':
try {
const config = vscode.workspace.getConfiguration('aurelio.antigravity');
const mode = config.get<string>('mode', 'auto');
this._panel.webview.postMessage({ command: 'antigravityChatStarted', prompt: `[Quick Action: ${message.action}]` });
if (mode === 'native') {
const diracClient = this._chatPanel?.getDiracClient();
if (!diracClient) {
throw new Error('Native Dirac backend is not available. Please open the Chat view first to initialize it.');
}
let fullResponse = '';
await diracClient.sendRequest('ag.sendPrompt', { text: message.action, model_id: 2 }, (res) => {
if (res.msg) {
fullResponse += res.msg;
}
});
this._panel.webview.postMessage({ command: 'antigravityChatComplete', response: fullResponse });
} else {
if (!this._antigravityService.isConnected) { this._antigravityService.connect(); }
const actionResponse = await this._antigravityService.sendPrompt(message.action);
this._panel.webview.postMessage({ command: 'antigravityChatComplete', response: actionResponse });
}
} catch (e: any) {
this._panel.webview.postMessage({ command: 'antigravityChatError', error: e.message });
}
return;
case 'antigravityToggleAutoAccept':
try {
if (!this._antigravityService.isConnected) { this._antigravityService.connect(); }
const enabled = await this._antigravityService.setAutoAccept(message.enabled);
this._panel.webview.postMessage({ command: 'antigravityAutoAccept', enabled });
} catch (e: any) {
console.error('[Antigravity] Auto-accept error:', e.message);
}
return;
// ── Vertex AI Commands ──────────────────────────────
case 'vertexHealthCheck': {
try {
const health = await this._vertexAuth.healthCheck();
this._panel.webview.postMessage({ command: 'vertexHealth', health });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'vertexHealth', health: { status: 'error', authenticated: false, projectId: null, location: '', error: e.message, checkedAt: new Date().toISOString() } });
}
return;
}
case 'vertexListModels': {
const models = this._vertexAuth.listModels();
this._panel.webview.postMessage({ command: 'vertexModels', models });
return;
}
case 'vertexInfer': {
try {
this._panel.webview.postMessage({ command: 'vertexInferenceStarted', prompt: message.prompt, model: message.model });
const result = await this._vertexAuth.infer(
message.prompt,
message.model || 'gemini-2.5-flash',
message.systemInstruction,
message.history,
);
this._panel.webview.postMessage({ command: 'vertexInferenceComplete', result });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'vertexInferenceError', error: e.message });
}
return;
}
// ── Jules Agent Commands ──────────────────────────
case 'julesStatus': {
try {
const status = await this._julesClient.getStatus();
this._panel.webview.postMessage({ command: 'julesStatus', status });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesStatus', status: { status: 'offline', available: false, sshConnected: false, activeTask: null, totalTasksCompleted: 0, error: e.message, checkedAt: new Date().toISOString() } });
}
return;
}
case 'julesDispatch': {
try {
this._panel.webview.postMessage({ command: 'julesTaskStarted', prompt: message.prompt });
const task = await this._julesClient.dispatch(
message.prompt,
{ repo: message.repo, taskId: message.taskId },
(activity, currentTask) => {
this._panel.webview.postMessage({ command: 'julesActivity', activity, task: currentTask });
}
);
this._panel.webview.postMessage({ command: 'julesTaskComplete', task });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesTaskError', error: e.message });
}
return;
}
case 'julesCancel': {
try {
const cancelled = await this._julesClient.cancel(message.taskId);
this._panel.webview.postMessage({ command: 'julesCancelled', cancelled });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesTaskError', error: e.message });
}
return;
}
case 'julesHistory': {
try {
const history = await this._julesClient.getHistory();
this._panel.webview.postMessage({ command: 'julesHistory', tasks: history });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesHistory', tasks: [], error: e.message });
}
return;
}
case 'julesListSessions': {
try {
const sessions = await this._julesClient.listSessions(message.limit || 20);
this._panel.webview.postMessage({ command: 'julesSessions', sessions });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesSessions', sessions: [], error: e.message });
}
return;
}
case 'julesResumeSession': {
try {
this._panel.webview.postMessage({ command: 'julesTaskStarted', prompt: `Resuming session ${message.sessionId}...` });
const task = await this._julesClient.resumeSession(
message.sessionId,
(activity, currentTask) => {
this._panel.webview.postMessage({ command: 'julesActivity', activity, task: currentTask });
}
);
this._panel.webview.postMessage({ command: 'julesTaskComplete', task });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesTaskError', error: e.message });
}
return;
}
default:
// Delegate chat*-prefixed commands to the ChatPanel backend
if (message.command?.startsWith('chat') && this._chatPanel) {
await this._chatPanel.handleExternalMessage(message);
return;
}
}
},
null,
this._disposables
);
// Periodic updates for orchestration status
const statusInterval = setInterval(() => {
this._updateOrchestrationStatus();
}, 2000);
this._disposables.push(new vscode.Disposable(() => clearInterval(statusInterval)));
}
private async _handleIngestObsidian(vaultPath?: string) {
try {
let actualPath = vaultPath;
if (!actualPath) {
const uris = await vscode.window.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
openLabel: 'Select Obsidian Vault'
});
if (!uris || uris.length === 0) return;
actualPath = uris[0].fsPath;
}
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Ingesting Obsidian Vault",
cancellable: false
}, async (progress) => {
progress.report({ message: `Processing ${actualPath}...` });
const result = await ServiceRegistry.getInstance().get(McpServerManager).callToolDynamic('ingest_obsidian_vault', { vault_path: actualPath }, undefined, true);
vscode.window.showInformationMessage('Obsidian ingestion complete!');
this._panel.webview.postMessage({ command: 'ingestResult', success: true, result, type: 'obsidian' });
});
} catch (e: any) {
vscode.window.showErrorMessage(`Ingestion failed: ${e.message}`);
this._panel.webview.postMessage({ command: 'ingestResult', success: false, error: e.message, type: 'obsidian' });
}
}
private async _handleIngestData(folderPath?: string) {
try {
let actualPath = folderPath;
if (!actualPath) {
const uris = await vscode.window.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
openLabel: 'Select Data Folder'
});
if (!uris || uris.length === 0) return;
actualPath = uris[0].fsPath;
}
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Ingesting Personal Data",
cancellable: false
}, async (progress) => {
progress.report({ message: `Processing ${actualPath}...` });
const result = await ServiceRegistry.getInstance().get(McpServerManager).callToolDynamic('ingest_personal_data', { folder_path: actualPath }, undefined, true);
vscode.window.showInformationMessage('Personal data ingestion complete!');
this._panel.webview.postMessage({ command: 'ingestResult', success: true, result, type: 'data' });
});
} catch (e: any) {
vscode.window.showErrorMessage(`Ingestion failed: ${e.message}`);
this._panel.webview.postMessage({ command: 'ingestResult', success: false, error: e.message, type: 'data' });
}
}
private async _handleQueryKnowledge(query: string, settings?: any) {
try {
const mcp = ServiceRegistry.getInstance().get(McpServerManager);
const promises: Promise<any[]>[] = [];
const timeoutMs = 15000;
// 1. Local Knowledge
promises.push(Promise.race([
(async () => {
try {
const localStr: any = await mcp.callToolDynamic('query_personal_knowledge', { query, hybrid: true, n_results: 5 }, undefined, true);
if (localStr?.isError) throw new Error(localStr.content?.[0]?.text || "Unknown error in local knowledge query");
let localResult = localStr;
try {
if (localStr?.content?.[0]?.text) {
localResult = JSON.parse(localStr.content[0].text);
} else if (typeof localStr === 'string') {
localResult = JSON.parse(localStr);
}
} catch(e) {}
const returnArr = Array.isArray(localResult) ? localResult : (localResult?.results ? localResult.results : [localResult]);
return returnArr.map((r: any, i: number) => ({
id: `local_${i}_${Date.now()}`,
title: r.title || r.name || 'Local Document',
type: 'Local Knowledge',
date: r.date || new Date().toISOString().split('T')[0],
excerpt: r.excerpt || r.summary || (typeof r === 'object' ? JSON.stringify(r).substring(0, 100) : String(r)),
provider: 'Obsidian / Keep',
path: r.path || undefined
}));
} catch (e) {
console.warn("Local query fail", e);
return [];
}
})(),
new Promise<any[]>((resolve) => setTimeout(() => {
console.warn("Local query timed out");
resolve([]);
}, timeoutMs))
]));
// 2. Arxiv
if (settings?.useArxiv) {
promises.push(Promise.race([
(async () => {
try {
const arxivStr: any = await mcp.callToolDynamic('search_arxiv', { query, max_results: 5 }, undefined, true);
if (arxivStr?.isError) throw new Error(arxivStr.content?.[0]?.text || "Unknown error in arxiv search");
let arxivResult = arxivStr;
try {
if (arxivStr?.content?.[0]?.text) arxivResult = JSON.parse(arxivStr.content[0].text);
else if (typeof arxivStr === 'string') arxivResult = JSON.parse(arxivStr);
} catch(e) {}
const returnArr = Array.isArray(arxivResult) ? arxivResult : (arxivResult?.results ? arxivResult.results : [arxivResult]);
return returnArr.map((r: any, i: number) => {
let itemPath = r.pdf_url || r.entry_id || r.id || r.url;
if (itemPath && !itemPath.startsWith('http') && !itemPath.startsWith('/')) {
itemPath = `https://arxiv.org/abs/${itemPath}`;
}
return {
id: `arxiv_${i}_${Date.now()}`,
title: r.title || 'Arxiv Paper',
type: 'Academic Paper',
date: r.published || r.updated || new Date().toISOString().split('T')[0],
excerpt: r.summary || r.abstract || (typeof r === 'object' ? JSON.stringify(r).substring(0, 100) : String(r)),
provider: 'Arxiv',
path: itemPath
};
});
} catch (e) {
console.error("Arxiv fail:", e);
return [];
}
})(),
new Promise<any[]>((resolve) => setTimeout(() => {
console.warn("Arxiv query timed out");
resolve([]);
}, timeoutMs))
]));
}
// 3. Libgen
if (settings?.useLibgen) {
promises.push(Promise.race([
(async () => {
try {
const libgenStr: any = await mcp.callToolDynamic('search_libgen', { query, max_results: 5 }, undefined, true);
if (libgenStr?.isError) throw new Error(libgenStr.content?.[0]?.text || "Unknown error in libgen search");
let libgenResult = libgenStr;
try {
if (libgenStr?.content?.[0]?.text) libgenResult = JSON.parse(libgenStr.content[0].text);
else if (typeof libgenStr === 'string') libgenResult = JSON.parse(libgenStr);
} catch(e) {}
const returnArr = Array.isArray(libgenResult) ? libgenResult : (libgenResult?.results ? libgenResult.results : [libgenResult]);
return returnArr.map((r: any, i: number) => {
let itemPath = r.mirror_1 || r.mirror_2 || r.url;
if (itemPath && !itemPath.startsWith('http') && !itemPath.startsWith('/')) {
itemPath = `http://${itemPath}`;
}
return {
id: `libgen_${i}_${Date.now()}`,
title: r.title || 'Libgen Book',
type: 'Book / Paper',
date: r.year || new Date().toISOString().split('T')[0],
excerpt: `Author: ${r.author || 'Unknown'}. Ext: ${r.extension || 'pdf'}. Size: ${r.size || 'Unknown'}`,
provider: 'Library Genesis',
path: itemPath
};
});
} catch (e) {
console.error("Libgen fail:", e);
return [];
}
})(),
new Promise<any[]>((resolve) => setTimeout(() => {
console.warn("Libgen query timed out");
resolve([]);
}, timeoutMs))
]));
}
// 4. Google Scholar
if (settings?.useScholar) {
promises.push(Promise.race([
(async () => {
try {
const scholarStr: any = await mcp.callToolDynamic('search_scholar', { query, max_results: 5 }, undefined, true);
if (scholarStr?.isError) throw new Error(scholarStr.content?.[0]?.text || "Unknown error in scholar search");
let scholarResult = scholarStr;
try {
if (scholarStr?.content?.[0]?.text) scholarResult = JSON.parse(scholarStr.content[0].text);
else if (typeof scholarStr === 'string') scholarResult = JSON.parse(scholarStr);
} catch(e) {}
const returnArr = Array.isArray(scholarResult) ? scholarResult : (scholarResult?.results ? scholarResult.results : [scholarResult]);
return returnArr.map((r: any, i: number) => {
let itemPath = r.eprint_url || r.url || r.link || r.pdf_url;
if (itemPath && !itemPath.startsWith('http') && !itemPath.startsWith('/')) {
itemPath = `https://${itemPath}`;
}
return {
id: `scholar_${i}_${Date.now()}`,
title: r.title || 'Scholar Paper',
type: 'Academic Paper',
date: r.year || new Date().toISOString().split('T')[0],
excerpt: r.snippet || r.abstract || (typeof r === 'object' ? JSON.stringify(r).substring(0, 100) : String(r)),
provider: 'Google Scholar',
path: itemPath
};
});
} catch (e) {
console.error("Scholar fail:", e);
return [];
}
})(),
new Promise<any[]>((resolve) => setTimeout(() => {
console.warn("Scholar query timed out");
resolve([]);
}, timeoutMs))
]));
}
// 5. Annas Archive (resolve_academic_paper)
if (settings?.useAnnasArchive) {
promises.push(Promise.race([
(async () => {
try {
const annasStr: any = await mcp.callToolDynamic('resolve_academic_paper', { query }, undefined, true);
if (annasStr?.isError) throw new Error(annasStr.content?.[0]?.text || "Unknown error in annas archive resolution");
let annasResult = annasStr;
try {
if (annasStr?.content?.[0]?.text) annasResult = JSON.parse(annasStr.content[0].text);
else if (typeof annasStr === 'string') annasResult = JSON.parse(annasStr);
} catch(e) {}
if (Array.isArray(annasResult)) {
return annasResult.map((r, i) => {
let itemPath = r.url || r.link || r.pdf_url || (r.doi ? `https://doi.org/${r.doi}` : undefined);
if (itemPath && !itemPath.startsWith('http') && !itemPath.startsWith('/')) {
itemPath = `https://${itemPath}`;
}
return {
id: `annas_${i}_${Date.now()}`,
title: r.title || 'Academic Paper',
type: 'Paper Resolution',
date: r.date || new Date().toISOString().split('T')[0],
excerpt: r.doi ? `DOI: ${r.doi}` : (typeof r === 'object' ? JSON.stringify(r).substring(0,100) : String(r)),
provider: 'Annas Archive',
path: itemPath
};
});
} else if (annasResult && typeof annasResult === 'object') {
let itemPath = annasResult.url || annasResult.link || annasResult.pdf_url || (annasResult.doi ? `https://doi.org/${annasResult.doi}` : undefined);
if (itemPath && !itemPath.startsWith('http') && !itemPath.startsWith('/')) {
itemPath = `https://${itemPath}`;
}
return [{
id: `annas_0_${Date.now()}`,
title: annasResult.title || (annasResult.status === 'failed' ? 'Resolution Failed' : 'Academic Paper'),
type: 'Paper Resolution',
date: annasResult.date || new Date().toISOString().split('T')[0],
excerpt: annasResult.doi ? `DOI: ${annasResult.doi}` : (annasResult.message || JSON.stringify(annasResult).substring(0, 100)),
provider: 'Annas Archive',
path: itemPath
}];
}
return [];
} catch (e) {
console.error("Annas archive fail:", e);
return [];
}
})(),
new Promise<any[]>((resolve) => setTimeout(() => {
console.warn("Annas Archive query timed out");
resolve([]);
}, timeoutMs))
]));
}
vscode.window.showInformationMessage('Querying knowledge providers concurrently...');
const results = await Promise.allSettled(promises);
let combinedResults: any[] = [];
for (const res of results) {
if (res.status === 'fulfilled' && res.value) {
combinedResults = combinedResults.concat(res.value);
}
}
this._panel.webview.postMessage({ command: 'queryResult', result: combinedResults });
} catch (e: any) {
vscode.window.showErrorMessage(`Query failed: ${e.message}`);
}
}
private async _handleSyncGoogleKeep() {
try {
vscode.window.showInformationMessage('Syncing Google Keep...');
await ServiceRegistry.getInstance().get(McpServerManager).callToolDynamic('sync_google_keep', {}, undefined, true);
vscode.window.showInformationMessage('Sync complete!');
} catch (e: any) {
vscode.window.showErrorMessage(`Sync failed: ${e.message}`);
}
}
private async _handleOptimizeVault(vaultPath?: string) {
try {
let actualPath = vaultPath;
if (!actualPath) {
const config = vscode.workspace.getConfiguration('aurelio');
let confVaultPath = config.get<string>('knowledge.vaultPath');
if (confVaultPath) {
actualPath = confVaultPath;
} else if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
actualPath = path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, '.aurelio', 'knowledge');
if (!fs.existsSync(actualPath)) {
fs.mkdirSync(actualPath, { recursive: true });
}
} else {
throw new Error("No workspace folder open. Cannot resolve Zettelkasten vault path.");
}
}
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: "Optimizing Zettelkasten Vault",
cancellable: false
}, async (progress) => {
progress.report({ message: `Optimizing vault at ${actualPath}...` });
const result = await ServiceRegistry.getInstance().get(McpServerManager).callToolDynamic('optimize_zettelkasten_vault', { vault_path: actualPath }, undefined, true);
vscode.window.showInformationMessage('Optimization complete!');
this._panel.webview.postMessage({ command: 'ingestResult', success: true, result, type: 'optimize' });
});
} catch (e: any) {
vscode.window.showErrorMessage(`Optimization failed: ${e.message}`);
this._panel.webview.postMessage({ command: 'ingestResult', success: false, error: e.message, type: 'optimize' });
}
}
private async _handleSyncKnowledge(settings?: any) {
try {
vscode.window.showInformationMessage('Synchronizing global knowledge fleet...');
const mcp = ServiceRegistry.getInstance().get(McpServerManager);
try {
await mcp.callToolDynamic('sync_google_keep', {}, undefined, true);
} catch (e) {
console.warn('Sync keep failed:', e);
}
if (settings?.obsidianVaultPath) {
try {
await mcp.callToolDynamic('ingest_obsidian_vault', { vault_path: settings.obsidianVaultPath }, undefined, true);
} catch (e) {
console.warn('Ingest obsidian vault failed:', e);
}
}
if (settings?.dataFolderPath) {
try {
await mcp.callToolDynamic('ingest_personal_data', { folder_path: settings.dataFolderPath }, undefined, true);
} catch (e) {
console.warn('Ingest personal data failed:', e);
}
}
vscode.window.showInformationMessage('Global knowledge sync complete.');
} catch (e: any) {
vscode.window.showErrorMessage(`Sync failed: ${e.message}`);
}
}
private async _handleOpenKnowledgeItem(pathStr: string) {
try {
if (!pathStr) return;
if (pathStr.startsWith('http')) {
if (pathStr.toLowerCase().endsWith('.pdf') || pathStr.toLowerCase().includes('/pdf/')) {
PdfViewerPanel.createOrShow(this._extensionUri, pathStr);
} else {
vscode.env.openExternal(vscode.Uri.parse(pathStr));
}
} else if (pathStr.startsWith('/') || pathStr.match(/^[a-zA-Z]:\\/)) {
if (pathStr.toLowerCase().endsWith('.pdf') || pathStr.toLowerCase().endsWith('.epub')) {
PdfViewerPanel.createOrShow(this._extensionUri, pathStr);
} else {
const doc = await vscode.workspace.openTextDocument(pathStr);
await vscode.window.showTextDocument(doc);
}
} else {
vscode.window.showWarningMessage(`No URL or local file available to open for: ${pathStr}`);
}
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to open item: ${e.message}`);
}
}
private async _handleFetchHilLogs() {
const config = vscode.workspace.getConfiguration('aurelio.hub');
const baseUrl = config.get<string>('hilBaseUrl') || 'https://hil.portugalfuturista.org';
const cfId = config.get<string>('cfClientId');
const cfSecret = config.get<string>('cfClientSecret');
const headers: any = {};
if (cfId && cfSecret) {
headers['CF-Access-Client-Id'] = cfId;
headers['CF-Access-Client-Secret'] = cfSecret;
}
try {
// Adjust endpoint if needed, assuming /api/v1/logs/ for now
const res = await axios.get(`${baseUrl}/api/v1/logs/`, { headers });
this._panel.webview.postMessage({ command: 'hilLogsData', logs: res.data });
} catch (e: any) {
console.error('HIL logs fetch failed:', e);
this._panel.webview.postMessage({ command: 'hilLogsData', error: e.message });
}
}
private async _handleFetchFinanceData() {
// Aggregating mock finance data for Control Center
// Keys must match what renderFinanceData() expects: liquidity, allocated, burn, events[]
const mockData = {
liquidity: '€42,850.00',
allocated: '€12,400.00',
burn: '€3,200/mo',
events: [
{ type: 'income', desc: 'Invoice #PF-2026-04 Paid', amount: '+ €4,500.00', timestamp: '2h ago' },
{ type: 'expense', desc: 'AWS Infrastructure', amount: '- €450.00', timestamp: '5h ago' },
{ type: 'expense', desc: 'Proto Labs Prototype', amount: '- €1,200.00', timestamp: '1d ago' },
{ type: 'income', desc: 'Stripe Subscription Payout', amount: '+ €1,800.00', timestamp: '2d ago' },
{ type: 'allocation', desc: 'BOM Sourcing Allocation', amount: '€850.00', timestamp: '3d ago' }
]
};
this._panel.webview.postMessage({ command: 'financeData', data: mockData });
}
private async _handleFetchFleetData() {
// Keys must match what renderFleetData() expects: total, online, errors, health
const mockData = {
total: 42,
online: 38,
errors: 2,
health: '94%'
};
this._panel.webview.postMessage({ command: 'fleetData', data: mockData });
}
private async _handleRefreshForecasting() {
vscode.window.showInformationMessage('Aurelio AI is simulating future scenarios...');
// Mock a long simulation
setTimeout(() => {
const mockForecasting = {
nextMonthProjected: '€14,200',
growthRate: '+14% YoY',
recommendations: [
'Upgrade ESP32-S3 fleet to latest FSM build to reduce 2% offline rate',
'Increase AWS Lambda memory for TelemetryProcessor to handle increased traffic',
'Purchase C-numbers C12345 in bulk for upcoming 100-unit batch'
],
scenarios: [
{ name: 'Optimistic', value: '€22,000' },
{ name: 'Baseline', value: '€18,500' },
{ name: 'Conservative', value: '€15,200' }
]
};
this._panel.webview.postMessage({ command: 'forecastingData', data: mockForecasting });
vscode.window.showInformationMessage('Scenario simulation complete.');
}, 1500);
}
// ═══════════════════════════════════════════════════════════════
// ─── Escola Backend Handlers ───
// ═══════════════════════════════════════════════════════════════
private _getOrpheuRoot(): string {
// Check setting first
const config = vscode.workspace.getConfiguration('aurelio');
const configuredPath = config.get<string>('knowledge.orpheuRoot');
if (configuredPath && fs.existsSync(configuredPath)) return configuredPath;
// Scan workspace folders for 'olhos-de-orpheu'
const folders = vscode.workspace.workspaceFolders || [];
for (const folder of folders) {
if (folder.uri.fsPath.includes('olhos-de-orpheu')) {
return folder.uri.fsPath;
}
// Check sibling directory
const siblingPath = path.join(path.dirname(folder.uri.fsPath), 'olhos-de-orpheu');
if (fs.existsSync(siblingPath)) return siblingPath;
}
// Fallback: use brain directory as a last resort
return this._brainManager.getBrainDir();
}
private _getEscolaDir(): string {
const brainDir = this._brainManager.getBrainDir();
const escolaDir = path.join(brainDir, 'escola');
if (!fs.existsSync(escolaDir)) {
fs.mkdirSync(escolaDir, { recursive: true });
}
return escolaDir;
}
private _getEscolaSessionPath(sessionId: string): string {
return path.join(this._getEscolaDir(), `${sessionId}.json`);
}
private async _handleEscolaAddSource(sourceType: string) {
try {
let sourceName = '';
let sourcePath = '';
let indexed = false;
if (sourceType === 'repo') {
const uris = await vscode.window.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
openLabel: 'Select Repository'
});
if (!uris || uris.length === 0) return;
sourcePath = uris[0].fsPath;
sourceName = path.basename(sourcePath);
} else if (sourceType === 'vault') {
const uris = await vscode.window.showOpenDialog({
canSelectFolders: true,
canSelectFiles: false,
canSelectMany: false,
openLabel: 'Select Obsidian Vault'
});
if (!uris || uris.length === 0) return;
sourcePath = uris[0].fsPath;
sourceName = path.basename(sourcePath);
} else if (sourceType === 'file' || sourceType === 'pdf') {
const uris = await vscode.window.showOpenDialog({
canSelectFolders: false,
canSelectFiles: true,
canSelectMany: false,
openLabel: 'Select File',
filters: { 'Documents': ['pdf', 'epub', 'md', 'txt'] }
});
if (!uris || uris.length === 0) return;
sourcePath = uris[0].fsPath;
sourceName = path.basename(sourcePath);
} else if (sourceType === 'url') {
const url = await vscode.window.showInputBox({
prompt: 'Enter URL to use as knowledge source',
placeHolder: 'https://example.com/document.pdf'
});
if (!url) return;
sourcePath = url;
sourceName = new URL(url).hostname + new URL(url).pathname.split('/').pop();
}
// Attempt to ingest the source into the knowledge base
await vscode.window.withProgress({
location: vscode.ProgressLocation.Notification,
title: `Escola: Indexing ${sourceName}`,
cancellable: false
}, async (progress) => {
const mcp = ServiceRegistry.getInstance().get(McpServerManager);
try {
if (sourcePath.toLowerCase().endsWith('.pdf')) {
progress.report({ message: 'Parsing PDF content...' });
await mcp.callToolDynamic('parse_pdf_document', { file_path: sourcePath, pages: 'all' }, undefined, true);
indexed = true;
} else if (sourceType === 'vault') {
progress.report({ message: 'Ingesting Obsidian vault...' });
await mcp.callToolDynamic('ingest_obsidian_vault', { vault_path: sourcePath }, undefined, true);
indexed = true;
} else if (sourceType === 'repo' || sourceType === 'file') {
progress.report({ message: 'Ingesting data folder...' });
const targetPath = sourceType === 'repo' ? sourcePath : path.dirname(sourcePath);
await mcp.callToolDynamic('ingest_personal_data', { folder_path: targetPath }, undefined, true);
indexed = true;
} else if (sourceType === 'url' && sourcePath.toLowerCase().endsWith('.pdf')) {
progress.report({ message: 'Downloading and parsing PDF...' });
// Download PDF to temp location, then parse
const tempDir = path.join(this._getEscolaDir(), 'downloads');
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir, { recursive: true });
const tempFile = path.join(tempDir, `source_${Date.now()}.pdf`);
const response = await axios.get(sourcePath, { responseType: 'arraybuffer' });
fs.writeFileSync(tempFile, Buffer.from(response.data));
await mcp.callToolDynamic('parse_pdf_document', { file_path: tempFile, pages: 'all' }, undefined, true);
sourcePath = tempFile; // Update path to local copy
indexed = true;
}
} catch (e: any) {
console.warn(`Escola: Indexing partial failure for ${sourceName}:`, e.message);
// Still add the source but mark as not fully indexed
}
});
const source = {
id: `src_${Date.now().toString(36)}`,
type: sourceType,
name: sourceName,
path: sourcePath,
indexed
};
this._panel.webview.postMessage({
command: 'escolaSourceAdded',
source
});
if (indexed) {
vscode.window.showInformationMessage(`Escola: ${sourceName} indexed successfully.`);
} else {
vscode.window.showWarningMessage(`Escola: ${sourceName} added but indexing may be incomplete.`);
}
} catch (e: any) {
vscode.window.showErrorMessage(`Escola: Failed to add source — ${e.message}`);
}
}
private async _handleEscolaChat(
sessionId: string,
message: string,
personaId: string,
sourceIds: string[],
personaHint: string
) {
try {
const mcp = ServiceRegistry.getInstance().get(McpServerManager);
const actions: Array<{tool: string; toolId: string; label: string; status: string; input?: string; output?: string}> = [];
// Helper: send progress update to webview
const sendProgress = (currentActions: typeof actions, reasoning?: string) => {
this._panel.webview.postMessage({
command: 'escolaChatProgress',
sessionId,
actions: currentActions,
reasoning: reasoning || undefined,
reasoningDone: false,
});
};
// ─── Step 1: Knowledge Retrieval ───
const retrievalAction = {
tool: 'query_personal_knowledge',
toolId: `retrieval_${Date.now()}`,
label: 'Pesquisar nas fontes indexadas',
status: 'pending' as string,
input: message,
output: undefined as string | undefined,
};
actions.push(retrievalAction);
sendProgress(actions, `Searching indexed sources for: "${message.substring(0, 80)}"…`);
let context = '';
let citations: string[] = [];
try {
const knowledgeResult: any = await mcp.callToolDynamic(
'query_personal_knowledge',
{ query: message, hybrid: true, n_results: 8 },
undefined, true
);
let parsed = knowledgeResult;
try {
if (knowledgeResult?.content?.[0]?.text) {
parsed = JSON.parse(knowledgeResult.content[0].text);
} else if (typeof knowledgeResult === 'string') {
parsed = JSON.parse(knowledgeResult);
}
} catch (e) { /* keep raw */ }
const results = Array.isArray(parsed) ? parsed : (parsed?.results || []);
context = results.map((r: any, i: number) => {
const title = r.title || r.name || `Source ${i + 1}`;
const body = r.excerpt || r.summary || r.content || JSON.stringify(r).substring(0, 500);
citations.push(`${title}`);
return `[Source ${i + 1}: ${title}]\n${body}`;
}).join('\n\n---\n\n');
retrievalAction.status = 'done';
retrievalAction.output = `Found ${results.length} relevant source(s)`;
} catch (e: any) {
retrievalAction.status = 'error';
retrievalAction.output = e.message;
console.warn('Escola: Knowledge retrieval failed, proceeding without context:', e);
}
sendProgress(actions, `Retrieved ${citations.length} source(s). Building persona prompt…`);
// ─── Step 2: Model Synthesis ───
const synthesisAction = {
tool: 'ask_model',
toolId: `synthesis_${Date.now()}`,
label: 'Sintetizar resposta com o modelo',
status: 'pending' as string,
output: undefined as string | undefined,
};
actions.push(synthesisAction);
sendProgress(actions, 'Synthesizing response from source material…');
const systemPrompt = `You are a teaching assistant in the "Escola de Sensações" learning system.\n\nADAPTIVE PERSONA: ${personaHint}\n\nIMPORTANT RULES:\n1. ONLY answer based on the provided source material below. If the sources don't contain relevant information, say so explicitly.\n2. Always cite which source(s) your answer draws from using [Source N] notation.\n3. Adapt your language complexity to match the persona level.\n4. Be pedagogically effective — use examples, analogies, and progressive disclosure.\n\n--- SOURCE MATERIAL ---\n${context || '(No indexed sources available for this query.)'}\n--- END SOURCES ---`;
const fullPrompt = `${systemPrompt}\n\nStudent question: ${message}`;
let responseText = '';
try {
const result: any = await mcp.callToolDynamic(
'ask_model',
{ prompt: fullPrompt, context: context },
undefined, true
);
if (result?.content?.[0]?.text) {
responseText = result.content[0].text;
} else if (typeof result === 'string') {
responseText = result;
} else {
responseText = JSON.stringify(result);
}
synthesisAction.status = 'done';
synthesisAction.output = `Generated ${responseText.length} character response`;
} catch (e: any) {
synthesisAction.status = 'error';
synthesisAction.output = e.message;
responseText = `⚠️ Synthesis error: ${e.message}\n\nThe knowledge retrieval succeeded but the model synthesis failed. Here is the raw context retrieved from your sources:\n\n${context.substring(0, 1000)}`;
}
// ─── Step 3: Send final structured response ───
const reasoning = `Searched ${citations.length} indexed source(s) for "${message.substring(0, 60)}". ` +
(citations.length > 0
? `Top sources: ${citations.slice(0, 3).join(', ')}. `
: 'No relevant sources found. ') +
`Synthesized response using ${personaId} persona.`;
this._panel.webview.postMessage({
command: 'escolaChatResponse',
sessionId,
message: {
role: 'assistant',
content: responseText,
timestamp: Date.now(),
citations: citations.slice(0, 5),
reasoning: reasoning,
reasoningDone: true,
actions: actions,
}
});
// ─── Step 4: Persist session ───
this._persistEscolaSession(sessionId, message, responseText, citations);
} catch (e: any) {
vscode.window.showErrorMessage(`Escola chat failed: ${e.message}`);
this._panel.webview.postMessage({
command: 'escolaChatResponse',
sessionId,
message: {
role: 'assistant',
content: `❌ Error: ${e.message}`,
timestamp: Date.now(),
citations: [],
actions: [{
tool: 'error',
toolId: `error_${Date.now()}`,
label: 'Erro no processamento',
status: 'error',
output: e.message,
}],
}
});
}
}
private _persistEscolaSession(
sessionId: string,
userMessage: string,
assistantResponse: string,
citations: string[]
) {
try {
const sessionPath = this._getEscolaSessionPath(sessionId);
let sessionData: any = { id: sessionId, messages: [], updatedAt: Date.now() };
if (fs.existsSync(sessionPath)) {
try {
sessionData = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
} catch (e) { /* start fresh */ }
}
sessionData.messages.push(
{ role: 'user', content: userMessage, timestamp: Date.now() },
{ role: 'assistant', content: assistantResponse, timestamp: Date.now(), citations }
);
sessionData.updatedAt = Date.now();
fs.writeFileSync(sessionPath, JSON.stringify(sessionData, null, 2), 'utf8');
} catch (e: any) {
console.error('Escola: Failed to persist session:', e.message);
}
}
private async _handleCreateEscolaSession(session: any) {
try {
const sessionPath = this._getEscolaSessionPath(session.id);
fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2), 'utf8');
} catch (e: any) {
console.error('Escola: Failed to create session:', e.message);
}
}
private async _handleListEscolaSessions() {
try {
const escolaDir = this._getEscolaDir();
const files = fs.readdirSync(escolaDir).filter(f => f.endsWith('.json'));
const sessions = files.map(f => {
try {
return JSON.parse(fs.readFileSync(path.join(escolaDir, f), 'utf8'));
} catch (e) { return null; }
}).filter(Boolean);
this._panel.webview.postMessage({
command: 'escolaSessionsList',
sessions
});
} catch (e: any) {
console.error('Escola: Failed to list sessions:', e.message);
}
}
private async _handleEscolaAction(action: string, sessionId?: string) {
switch (action) {
case 'grill-me': {
// Auto-generate a quiz/interrogation based on session context
if (!sessionId) {
vscode.window.showWarningMessage('Escola: Inicia uma sessão primeiro para ser interrogado.');
return;
}
await this._handleEscolaChat(
sessionId,
'Interroga-me! Faz-me 5 perguntas desafiantes sobre o material que estudámos nesta sessão. Usa diferentes tipos de pergunta: escolha múltipla, verdadeiro/falso, resposta curta e análise crítica.',
'licenciatura',
[],
'Quiz Master'
);
break;
}
case 'study-guide': {
// Generate a structured study guide from the session
if (!sessionId) {
vscode.window.showWarningMessage('Escola: Inicia uma sessão primeiro para gerar um guia de estudo.');
return;
}
await this._handleEscolaChat(
sessionId,
'Gera um guia de estudo estruturado com base em toda a nossa conversa até agora. Inclui: 1) Conceitos-chave, 2) Definições importantes, 3) Relações entre conceitos, 4) Pontos a rever, 5) Sugestões de estudo adicional.',
'licenciatura',
[],
'Study Guide Generator'
);
break;
}
case 'save': {
if (!sessionId) return;
const sessionPath = this._getEscolaSessionPath(sessionId);
if (fs.existsSync(sessionPath)) {
const data = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
// Export to markdown in the knowledge vault
const config = vscode.workspace.getConfiguration('aurelio');
const vaultPath = config.get<string>('knowledge.vaultPath') || this._getEscolaDir();
const exportPath = path.join(vaultPath, `escola_${sessionId}.md`);
let md = `# Escola Session: ${data.title || sessionId}\n\n`;
md += `**Persona:** ${data.personaId || 'unknown'}\n`;
md += `**Date:** ${new Date(data.createdAt || Date.now()).toLocaleString('pt-PT')}\n\n---\n\n`;
for (const msg of (data.messages || [])) {
md += `### ${msg.role === 'user' ? '🎓 Student' : '📚 Escola'}\n\n`;
md += `${msg.content}\n\n`;
if (msg.citations?.length) {
md += `> Citations: ${msg.citations.join(', ')}\n\n`;
}
}
fs.writeFileSync(exportPath, md, 'utf8');
vscode.window.showInformationMessage(`Session saved to ${exportPath}`);
}
break;
}
case 'export': {
if (!sessionId) return;
const sessionPath = this._getEscolaSessionPath(sessionId);
if (fs.existsSync(sessionPath)) {
const doc = await vscode.workspace.openTextDocument(sessionPath);
await vscode.window.showTextDocument(doc);
}
break;
}
case 'cite': {
vscode.window.showInformationMessage('Escola: Citations copied to clipboard (coming soon).');
break;
}
case 'share': {
vscode.window.showInformationMessage('Escola: Session sharing via Proxmox sync (coming soon).');
break;
}
}
}
public static createOrShow(extensionUri: vscode.Uri, skills: any[] = [], brainManager: BrainManager, heteronimoManager: HeteronimoManager, chatPanel?: ChatPanel) {
const column = vscode.window.activeTextEditor
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (ControlCenterPanel.currentPanel) {
ControlCenterPanel.currentPanel._panel.reveal(column);
return;
}
const panel = vscode.window.createWebviewPanel(
'aurelioControlCenter',
'Aurelio Control Center',
column || vscode.ViewColumn.One,
{
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [
vscode.Uri.joinPath(extensionUri, 'dist', 'webviews'),
vscode.Uri.joinPath(extensionUri, 'media'),
vscode.Uri.joinPath(extensionUri, 'resources')
]
}
);
ControlCenterPanel.currentPanel = new ControlCenterPanel(panel, extensionUri, skills, brainManager, heteronimoManager, chatPanel);
}
public updateFinancials(data: any) {
this._panel.webview.postMessage({ command: 'updateFinancials', data });
}
public dispose() {
ControlCenterPanel.currentPanel = undefined;
this._panel.dispose();
// Unregister the ChatPanel external sink
this._chatSinkDisposable?.dispose();
if (this._approvalRequestedListener) {
ApprovalGate.events.removeListener('requestApproval', this._approvalRequestedListener);
}
if (this._approvalResolvedListener) {
ApprovalGate.events.removeListener('resolveApproval', this._approvalResolvedListener);
}
while (this._disposables.length) {
const x = this._disposables.pop();
if (x) {
x.dispose();
}
}
}
private _updateTimeout: NodeJS.Timeout | undefined;
private async _update() {
if (this._updateTimeout) {
clearTimeout(this._updateTimeout);
}
// Debounce update to avoid flickering while servers are connecting
this._updateTimeout = setTimeout(async () => {
try {
this._panel.webview.html = await this._getHtmlForWebview();
// Initial state population will happen when webviewReady is received
} catch (error: any) {
console.error('Error updating Control Center:', error);
this._panel.webview.html = `
<div style="padding: 20px; text-align: center; color: var(--vscode-errorForeground); background: var(--vscode-editor-background); height: 100vh;">
<h2>Critical Error</h2>
<p>${error.message}</p>
<button onclick="window.location.reload()">Retry</button>
</div>
`;
}
}, 50);
}
private _updateWorkflows() {
const configResolver = ServiceRegistry.getInstance().get(McpServerManager).configResolver;
if (!configResolver) return;
const workflows = configResolver.loadWorkflows();
this._panel.webview.postMessage({ command: 'workflowsData', workflows });
}
private _updateChronicles() {
const chronicles = this._brainManager.chronicle.listEntries();
this._panel.webview.postMessage({ command: 'chroniclesData', chronicles });
}
private _updateSessions() {
const sessions = this._brainManager.listConversations().map(c => ({
...c,
artifacts: this._brainManager.listArtifactsExtended(c.id)
}));
this._panel.webview.postMessage({ command: 'sessionsData', sessions });
}
private _updateOrchestrationStatus() {
const coordinator = ServiceRegistry.getInstance().get(Coordinator);
const coordinatorStatus = coordinator.getStatus();
// Merge evolution orchestrator status if available
let evolutionStatus: Record<string, unknown> = {};
try {
const orchestrator = ServiceRegistry.getInstance().get(EvolutionOrchestrator);
evolutionStatus = { ...orchestrator.getStatus() };
} catch { /* orchestrator not registered yet */ }
this._panel.webview.postMessage({
command: 'orchestrationStatus',
status: { ...coordinatorStatus, ...evolutionStatus }
});
// Also send current loop stats if loop is active
try {
const wfm = ServiceRegistry.getInstance().get(WorkflowManager);
const loop = wfm.getActiveLoop();
if (loop) {
this._panel.webview.postMessage({ command: 'evolutionStats', stats: loop.getStats() });
this._panel.webview.postMessage({ command: 'evolutionFunnel', funnel: loop.getFunnel() });
}
} catch { /* no active loop */ }
}
/**
* Wire real-time evolution telemetry from the EvolutionLoop to the webview.
* Subscribes to statsUpdate, funnelUpdate, and convergenceEvent emissions.
*/
private _wireEvolutionTelemetry() {
try {
const wfm = ServiceRegistry.getInstance().get(WorkflowManager);
const loop = wfm.getActiveLoop();
if (!loop) return;
// Remove any previous listeners from prior webviewReady calls
loop.removeAllListeners('statsUpdate');
loop.removeAllListeners('funnelUpdate');
loop.removeAllListeners('convergenceEvent');
loop.on('statsUpdate', (stats: any) => {
this._panel.webview.postMessage({ command: 'evolutionStats', stats });
});
loop.on('funnelUpdate', (funnel: any) => {
this._panel.webview.postMessage({ command: 'evolutionFunnel', funnel });
});
loop.on('convergenceEvent', (event: any) => {
this._panel.webview.postMessage({ command: 'convergenceEvent', event });
});
console.log('[ControlCenter] Evolution telemetry wired to webview');
} catch {
// WorkflowManager or loop not available — silent
}
}
private _updateSettings() {
const config = vscode.workspace.getConfiguration('aurelio');
const knowledgeSettings = config.get('knowledgeSettings') || {};
// Collect all top-level aurelio settings to pass to the webview
const allAurelioSettings = JSON.parse(JSON.stringify(config));
this._panel.webview.postMessage({
command: 'updateData',
data: {
knowledgeSettings,
aurelioSettings: allAurelioSettings
}
});
}
private async _updateHeteronyms() {
try {
const allHeteronimos = this._heteronimoManager.getHeteronimos();
const personas = allHeteronimos.map(config => ({
id: config.slug,
label: config.name || config.slug,
shortLabel: config.name?.split(' ').pop() || config.slug,
description: `Realm: ${config.preferredRealm || 'Global'} | Capabilities: ${(config.capabilities || []).join(', ')}`,
icon: 'persona',
color: '#6366f1',
systemPrompt: config.roleDefinition || `You are ${config.name}.`
}));
if (personas.length > 0) {
this._panel.webview.postMessage({ command: 'escolaPersonasUpdate', personas });
}
} catch (e: any) {
console.error('[ControlCenter] Failed to update heteronyms:', e.message);
}
}
private async _pushVertexStatus() {
try {
const health = await this._vertexAuth.healthCheck();
this._panel.webview.postMessage({ command: 'vertexHealth', health });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'vertexHealth', health: { status: 'unavailable', authenticated: false, projectId: null, location: '', error: e.message, checkedAt: new Date().toISOString() } });
}
}
private async _pushJulesStatus() {
try {
const status = await this._julesClient.getStatus();
this._panel.webview.postMessage({ command: 'julesStatus', status });
} catch (e: any) {
this._panel.webview.postMessage({ command: 'julesStatus', status: { status: 'offline', available: false, sshConnected: false, activeTask: null, totalTasksCompleted: 0, error: e.message, checkedAt: new Date().toISOString() } });
}
}
private _pushEnabledPanels() {
const config = vscode.workspace.getConfiguration('aurelio.controlCenter');
const panels = config.get<Record<string, boolean>>('enabledPanels', {});
this._panel.webview.postMessage({ command: 'enabledPanelsUpdate', panels });
}
private _updateMcpStatus() {
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
const hubs = mcpManager.getHubs();
const configServers = mcpManager.getServersConfig();
const statuses = hubs.map(h => ({
name: h.name,
status: h.state,
error: h.error,
tools: (h.availableTools || []).map((t: any) => ({
...t,
isDisabled: (h.config?.disabledTools || []).includes(t.name)
})),
isConnected: h.state === 'ConnectedMcpConnection',
isConnecting: h.state === 'ConnectingMcpConnection',
isDisabled: !!(configServers[h.name] as any)?._disabled
}));
// Include configured servers that aren't currently managed hubs (e.g. disabled)
const hubNames = new Set(hubs.map(h => h.name));
for (const [name, config] of Object.entries(configServers)) {
if (!hubNames.has(name)) {
statuses.push({
name: name,
status: 'DisconnectedMcpConnection',
error: undefined,
tools: [],
isConnected: false,
isConnecting: false,
isDisabled: !!(config as any)._disabled
});
}
}
this._panel.webview.postMessage({ command: 'mcpStatusUpdate', statuses });
}
private async _getHtmlForWebview() {
const nonce = getNonce();
// Svelte 5 bundle — single self-contained file with all deps inlined.
// Migrated from React (controlCenter.js) to Svelte for 50% smaller bundle
// and no virtual DOM overhead in the VS Code webview sandbox.
const scriptUri = this._panel.webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist', 'webviews', 'controlCenterSvelte.js'));
const styleUri = this._panel.webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist', 'webviews', 'controlCenterSvelte.css'));
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${this._panel.webview.cspSource} 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval' ${this._panel.webview.cspSource}; connect-src https: wss:; font-src ${this._panel.webview.cspSource} https: data:; img-src ${this._panel.webview.cspSource} https: data:;">
<title>Aurelio Control Center</title>
<link href="${styleUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="${scriptUri}"></script>
</body>
</html>`;
}
private async _handleExtractKnowledgeItem(item: any) {
try {
let actualPath = '';
const config = vscode.workspace.getConfiguration('aurelio');
let confVaultPath = config.get<string>('knowledge.vaultPath');
if (confVaultPath) {
actualPath = confVaultPath;
} else if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
actualPath = path.join(vscode.workspace.workspaceFolders[0].uri.fsPath, '.aurelio', 'knowledge');
if (!fs.existsSync(actualPath)) {
fs.mkdirSync(actualPath, { recursive: true });
}
} else {
throw new Error("No workspace folder open. Cannot extract knowledge item.");
}
const title = item.title || 'Untitled';
const safeTitle = title.replace(/[^a-z0-9]/gi, '_').toLowerCase();
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const kiDirName = `ki_${safeTitle}_${timestamp}`;
const kiDirPath = path.join(actualPath, kiDirName);
const artifactsDirPath = path.join(kiDirPath, 'artifacts');
if (!fs.existsSync(kiDirPath)) {
fs.mkdirSync(kiDirPath, { recursive: true });
}
if (!fs.existsSync(artifactsDirPath)) {
fs.mkdirSync(artifactsDirPath, { recursive: true });
}
const fileName = 'content.md';
const filePath = path.join(artifactsDirPath, fileName);
let content = `# ${title}\n\n`;
content += `**Source Provider:** ${item.provider || 'Unknown'}\n`;
content += `**Date Extracted:** ${new Date().toLocaleString()}\n`;
if (item.authors) content += `**Authors:** ${item.authors}\n`;
if (item.path) content += `**Original Path:** ${item.path}\n`;
if (item.document_id) content += `**Document ID:** ${item.document_id}\n`;
content += `\n---\n\n`;
content += `## Content\n\n`;
content += item.content || item.excerpt || 'No content provided.';
fs.writeFileSync(filePath, content, 'utf8');
const metadataPath = path.join(kiDirPath, 'metadata.json');
const references = [];
if (item.path) references.push(item.path);
if (item.document_id) references.push(item.document_id);
const metadata = {
title: title,
summary: item.summary || `Extracted knowledge item: ${title}`,
references: references,
artifacts: [`artifacts/${fileName}`]
};
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8');
vscode.window.showInformationMessage(`Extracted knowledge item to ${kiDirPath}`);
} catch (e: any) {
vscode.window.showErrorMessage(`Failed to extract knowledge item: ${e.message}`);
}
}
// ─── Electronics Dashboard Handlers ────────────────────────────────
private async _handleElecSearch(message: any): Promise<void> {
try {
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) {
this._panel.webview.postMessage({
command: 'electronicsSearchResult',
results: [],
error: 'MCP manager not available. Check electrical-sourcing-mcp configuration.'
});
return;
}
const query = message.query || '';
const searchMode = message.searchMode || 'expert'; // 'expert' | 'basic'
// Expert mode: fire parallel queries — distributor search + JLCPCB local library
const promises: Promise<any>[] = [
mcpManager.callTool('electrical-sourcing-mcp', 'search_components', {
query,
in_stock_only: message.inStockOnly ?? false,
category: message.category || undefined,
limit: message.limit ?? 20
})
];
// Add parallel JLCPCB local library search for expert mode
if (searchMode === 'expert') {
promises.push(
mcpManager.callTool('electrical-sourcing-mcp', 'search_local_library', {
query,
category: message.category || undefined,
limit: 10
}).catch(() => null)
);
}
const [mainResult, jlcpcbResult] = await Promise.all(promises);
// Parse results
const mainParsed = typeof mainResult === 'string' ? JSON.parse(mainResult) : mainResult;
const jlcpcbParsed = jlcpcbResult ? (typeof jlcpcbResult === 'string' ? JSON.parse(jlcpcbResult) : jlcpcbResult) : null;
// Merge and deduplicate: main results first, then JLCPCB-only results tagged as basic
let allResults = Array.isArray(mainParsed) ? mainParsed : (mainParsed?.results ?? []);
if (jlcpcbParsed) {
const jlcpcbArr = Array.isArray(jlcpcbParsed) ? jlcpcbParsed : (jlcpcbParsed?.results ?? []);
const existingMpns = new Set(allResults.map((r: any) => (r.mpn || r.MPN || '').toLowerCase()));
for (const item of jlcpcbArr) {
const mpn = (item.mpn || item.MPN || '').toLowerCase();
if (mpn && !existingMpns.has(mpn)) {
allResults.push({ ...item, source: 'jlcpcb', basic: true });
}
}
}
this._panel.webview.postMessage({
command: 'electronicsSearchResult',
results: allResults
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsSearchResult',
results: [],
error: e.message
});
}
}
private async _handleElecDetail(message: any): Promise<void> {
try {
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
// Fetch component details and datasheet in parallel
const detailPromise = mcpManager.callTool('electrical-sourcing-mcp', 'get_component_details', {
mpn: message.mpn || undefined,
c_number: message.cNumber || undefined
});
// Auto-fetch datasheet if C-number is available
const datasheetPromise = message.cNumber
? mcpManager.callTool('electrical-sourcing-mcp', 'get_datasheet', {
c_number: message.cNumber
}).catch(() => null)
: Promise.resolve(null);
const [result, datasheetResult] = await Promise.all([detailPromise, datasheetPromise]);
const detailParsed = typeof result === 'string' ? JSON.parse(result) : result;
const datasheetParsed = datasheetResult ? (typeof datasheetResult === 'string' ? JSON.parse(datasheetResult) : datasheetResult) : null;
// Merge datasheet content into detail
if (datasheetParsed) {
detailParsed.datasheet_content = datasheetParsed;
}
this._panel.webview.postMessage({
command: 'electronicsDetailResult',
detail: detailParsed
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsDetailResult',
detail: null,
error: e.message
});
}
}
private async _handleElecAlternates(message: any): Promise<void> {
try {
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
// First get direct alternates
const alternates = await mcpManager.callTool('electrical-sourcing-mcp', 'find_alternates', {
mpn: message.mpn || undefined,
c_number: message.cNumber || undefined,
priority: message.priority || 'stock'
});
// Then get deeper analysis if requested
let analysis = null;
if (message.analyze) {
analysis = await mcpManager.callTool('electrical-sourcing-mcp', 'analyze_component_alternatives', {
mpn: message.mpn || undefined,
c_number: message.cNumber || undefined,
priority_metrics: message.priorityMetrics || ['stock', 'price', 'package']
});
}
this._panel.webview.postMessage({
command: 'electronicsAlternatesResult',
alternates: typeof alternates === 'string' ? JSON.parse(alternates) : alternates,
analysis: analysis ? (typeof analysis === 'string' ? JSON.parse(analysis) : analysis) : null
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsAlternatesResult',
alternates: [],
error: e.message
});
}
}
private async _handleElecBom(message: any): Promise<void> {
try {
// ── Remote-first: offload BOM optimization to Proxmox ──
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable) {
const result = await remoteClient.optimizeBom(
message.components || [],
message.optimizeFor || 'cost',
);
this._panel.webview.postMessage({
command: 'electronicsBomResult',
bom: result,
source: 'remote',
});
return;
}
// ── Fallback: local MCP execution ──
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-sourcing-mcp', 'generate_optimized_bom', {
components: message.components || [],
optimize_for: message.optimizeFor || 'cost'
});
this._panel.webview.postMessage({
command: 'electronicsBomResult',
bom: typeof result === 'string' ? JSON.parse(result) : result,
source: 'local',
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsBomResult',
bom: null,
error: e.message
});
}
}
private async _handleElecSpice(message: any): Promise<void> {
try {
// ── Remote-first: offload SPICE simulation to Proxmox ──
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable) {
const result = await remoteClient.runSpice(
message.netlist || '',
message.analysisType || 'tran',
message.parameters || {},
);
this._panel.webview.postMessage({
command: 'electronicsSpiceResult',
simulation: result,
source: 'remote',
});
return;
}
// ── Fallback: local MCP execution ──
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-eda-mcp', 'run_spice_simulation', {
netlist: message.netlist || '',
analysis_type: message.analysisType || 'tran',
parameters: message.parameters || {}
});
this._panel.webview.postMessage({
command: 'electronicsSpiceResult',
simulation: typeof result === 'string' ? JSON.parse(result) : result,
source: 'local',
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsSpiceResult',
simulation: null,
error: e.message
});
}
}
private async _handleElecErc(message: any): Promise<void> {
try {
// ── Remote-first: offload ERC to Proxmox ──
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable && message.netlistContent) {
const result = await remoteClient.runErc(
message.netlistContent,
message.fileType || 'spice',
);
this._panel.webview.postMessage({
command: 'electronicsErcResult',
erc: result,
source: 'remote',
});
return;
}
// ── Fallback: local MCP execution (file-path based) ──
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-eda-mcp', 'run_erc', {
file_path: message.filePath || ''
});
this._panel.webview.postMessage({
command: 'electronicsErcResult',
erc: typeof result === 'string' ? JSON.parse(result) : result,
source: 'local',
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsErcResult',
erc: null,
error: e.message
});
}
}
private async _handleElecExtractBom(message: any): Promise<void> {
try {
// ── Remote-first: offload BOM extraction to Proxmox ──
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable && message.netlistContent) {
const result = await remoteClient.extractBom(
message.netlistContent,
message.format || 'json',
);
this._panel.webview.postMessage({
command: 'electronicsExtractBomResult',
bom: result,
source: 'remote',
});
return;
}
// ── Fallback: local MCP execution (file-path based) ──
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-eda-mcp', 'extract_bom', {
file_path: message.filePath || '',
format: message.format || 'json'
});
this._panel.webview.postMessage({
command: 'electronicsExtractBomResult',
bom: typeof result === 'string' ? JSON.parse(result) : result,
source: 'local',
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsExtractBomResult',
bom: null,
error: e.message
});
}
}
private async _handleElecPowerRails(message: any): Promise<void> {
try {
// ── Remote-first: offload power rail analysis to Proxmox ──
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable && message.netlistContent) {
const result = await remoteClient.analyzePowerRails(
message.netlistContent,
);
this._panel.webview.postMessage({
command: 'electronicsAnalyzePowerResult',
powerRails: result,
source: 'remote',
});
return;
}
// ── Fallback: local MCP execution (file-path based) ──
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-eda-mcp', 'analyze_power_rails', {
file_path: message.filePath || ''
});
this._panel.webview.postMessage({
command: 'electronicsAnalyzePowerResult',
powerRails: typeof result === 'string' ? JSON.parse(result) : result
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsAnalyzePowerResult',
powerRails: null,
error: e.message
});
}
}
private async _handleElecScanProjects(_message: any): Promise<void> {
try {
const projects: any[] = [];
const edaExtensions = ['.kicad_sch', '.kicad_pcb', '.SchDoc', '.PcbDoc', '.brd', '.sch', '.dsn'];
const workspaceFolders = vscode.workspace.workspaceFolders || [];
const path = require('path');
for (const folder of workspaceFolders) {
try {
const glob = `**/*{${edaExtensions.join(',')}}`;
const files = await vscode.workspace.findFiles(
new vscode.RelativePattern(folder, glob),
'{**/node_modules/**,**/build/**,**/dist/**,**/.git/**}',
100
);
// Group files by project directory to avoid duplicates
const dirMap = new Map<string, any[]>();
for (const file of files) {
const dir = path.dirname(file.fsPath);
if (!dirMap.has(dir)) { dirMap.set(dir, []); }
dirMap.get(dir)!.push(file);
}
for (const [projDir, projFiles] of dirMap) {
// Find the primary schematic file (prefer .kicad_sch over .kicad_pcb, etc.)
const sorted = projFiles.sort((a: vscode.Uri, b: vscode.Uri) => {
const prio = (f: string) => f.endsWith('.kicad_sch') ? 0 : f.endsWith('.SchDoc') ? 1 : f.endsWith('.sch') ? 2 : 10;
return prio(a.fsPath) - prio(b.fsPath);
});
const primary = sorted[0];
const filename = path.basename(primary.fsPath);
const ext = path.extname(filename);
let edaType = 'unknown';
if (ext.startsWith('.kicad')) { edaType = 'kicad'; }
else if (ext === '.SchDoc' || ext === '.PcbDoc') { edaType = 'altium'; }
else if (ext === '.brd' || ext === '.sch') { edaType = 'eagle'; }
else if (ext === '.dsn') { edaType = 'orcad'; }
// Count project files by type
const hasSchematic = projFiles.some((f: vscode.Uri) => /\.(kicad_sch|SchDoc|sch|dsn)$/i.test(f.fsPath));
const hasPcb = projFiles.some((f: vscode.Uri) => /\.(kicad_pcb|PcbDoc|brd)$/i.test(f.fsPath));
// Compute change rate via git log
let changeRate = null;
try {
const cp = require('child_process');
const fwGlob = '*.c *.cpp *.h *.py *.rs *.ino';
const hwGlob = '*.kicad_sch *.kicad_pcb *.SchDoc *.PcbDoc *.brd *.sch *.dsn';
const fwCount = cp.execSync(
`git log --oneline --since='30 days ago' -- ${fwGlob} 2>/dev/null | wc -l`,
{ cwd: projDir, encoding: 'utf-8', timeout: 5000 }
).trim();
const hwCount = cp.execSync(
`git log --oneline --since='30 days ago' -- ${hwGlob} 2>/dev/null | wc -l`,
{ cwd: projDir, encoding: 'utf-8', timeout: 5000 }
).trim();
// Get last modified date for the schematic
const lastModified = cp.execSync(
`git log -1 --format='%ci' -- '${filename}' 2>/dev/null`,
{ cwd: projDir, encoding: 'utf-8', timeout: 3000 }
).trim();
const fwN = parseInt(fwCount) || 0;
const hwN = parseInt(hwCount) || 0;
const rateOf = (n: number) => n > 20 ? 'high' : n > 5 ? 'medium' : 'low';
changeRate = {
firmware: { commits: fwN, period: '30d', rate: rateOf(fwN) },
hardware: { commits: hwN, period: '30d', rate: rateOf(hwN) },
lastModified: lastModified || null
};
} catch { /* not a git repo or git not available */ }
projects.push({
name: path.basename(projDir),
filename,
path: primary.fsPath,
projectDir: projDir,
type: edaType,
workspace: folder.name,
fileCount: projFiles.length,
hasSchematic,
hasPcb,
changeRate
});
}
} catch { /* skip folder */ }
}
// Sort by most recently modified first
projects.sort((a, b) => {
const aDate = a.changeRate?.lastModified ? new Date(a.changeRate.lastModified).getTime() : 0;
const bDate = b.changeRate?.lastModified ? new Date(b.changeRate.lastModified).getTime() : 0;
return bDate - aDate;
});
this._panel.webview.postMessage({
command: 'electronicsProjectsResult',
projects,
lastScan: Date.now()
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsProjectsResult',
projects: [],
error: e.message
});
}
}
private async _handleElecGetDatasheet(message: any): Promise<void> {
try {
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-sourcing-mcp', 'get_datasheet', {
c_number: message.cNumber || ''
});
this._panel.webview.postMessage({
command: 'electronicsDatasheetResult',
datasheet: typeof result === 'string' ? JSON.parse(result) : result,
mpn: message.mpn || ''
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsDatasheetResult',
datasheet: null,
error: e.message
});
}
}
private async _handleElecValidateReplacement(message: any): Promise<void> {
try {
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
this._panel.webview.postMessage({ command: 'electronicsValidationStatus', status: 'running', mpn: message.mpn });
// Generate SPICE test case for the replacement component
const spiceTest = await mcpManager.callTool('electrical-eda-mcp', 'create_spice_test_case', {
circuit_config: {
name: `Validation: ${message.mpn} replacing ${message.originalMpn || 'unknown'}`,
components: message.circuit?.components || [],
analysis: message.circuit?.analysis || { type: 'tran', step: '1u', stop: '10m' },
probes: message.circuit?.probes || []
}
});
const spiceParsed = typeof spiceTest === 'string' ? JSON.parse(spiceTest) : spiceTest;
// Run the generated netlist if available — prefer remote execution
let simResult = null;
if (spiceParsed?.netlist) {
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable) {
simResult = await remoteClient.runSpice(
spiceParsed.netlist,
message.circuit?.analysis?.type || 'tran',
);
} else {
simResult = await mcpManager.callTool('electrical-eda-mcp', 'run_spice_simulation', {
netlist: spiceParsed.netlist,
analysis_type: message.circuit?.analysis?.type || 'tran'
});
}
}
this._panel.webview.postMessage({
command: 'electronicsValidationResult',
mpn: message.mpn,
testCase: spiceParsed,
simulation: simResult ? (typeof simResult === 'string' ? JSON.parse(simResult) : simResult) : null
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsValidationResult',
mpn: message.mpn,
error: e.message
});
}
}
private async _handleElecConvertToSpice(message: any): Promise<void> {
try {
// ── Remote-first: offload conversion to Proxmox ──
const remoteClient = getRemoteEdaClient();
const remoteAvailable = await remoteClient.isAvailable();
if (remoteAvailable && message.content) {
const result = await remoteClient.convertToSpice(
message.content,
message.sourceFormat || 'kicad',
);
this._panel.webview.postMessage({
command: 'electronicsConvertSpiceResult',
netlist: result,
filePath: message.filePath,
source: 'remote',
});
return;
}
// ── Fallback: local MCP execution (file-path based) ──
const mcpManager = ServiceRegistry.getInstance().get(McpServerManager);
if (!mcpManager) { return; }
const result = await mcpManager.callTool('electrical-eda-mcp', 'convert_schematic_to_spice', {
file_path: message.filePath || ''
});
this._panel.webview.postMessage({
command: 'electronicsConvertSpiceResult',
netlist: typeof result === 'string' ? JSON.parse(result) : result,
filePath: message.filePath,
source: 'local',
});
} catch (e: any) {
this._panel.webview.postMessage({
command: 'electronicsConvertSpiceResult',
netlist: null,
error: e.message
});
}
}
}
function getNonce() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}