2646 lines
130 KiB
TypeScript
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;
|
|
}
|