import * as vscode from 'vscode'; import { ACTIONS, ACTION_LABELS, MIN_SELECTION_LENGTH, resolvePromptPersona, type ActionId, } from '@lib/actions'; import { runCodeAssist } from './codeAssist'; import { hasApiKey, readSettings, resolveConfig, settingsCatalog } from './config'; import { generateSuggestion } from './llm'; import { withLexAIProgress } from './statusBar'; const VIEW_ID = 'lexai.sidebar'; export function registerSidebarView(context: vscode.ExtensionContext): void { const provider = new LexAISidebarProvider(context); context.subscriptions.push( vscode.window.registerWebviewViewProvider(VIEW_ID, provider, { webviewOptions: { retainContextWhenHidden: true }, }), vscode.commands.registerCommand('lexai.openSidebar', async () => { await vscode.commands.executeCommand(`${VIEW_ID}.focus`); }), ); } class LexAISidebarProvider implements vscode.WebviewViewProvider { private view?: vscode.WebviewView; constructor(private readonly context: vscode.ExtensionContext) {} resolveWebviewView(webviewView: vscode.WebviewView): void { this.view = webviewView; const logoUri = webviewView.webview.asWebviewUri( vscode.Uri.joinPath(this.context.extensionUri, 'media', 'icon.svg'), ); webviewView.webview.options = { enableScripts: true, localResourceRoots: [vscode.Uri.joinPath(this.context.extensionUri, 'media')], }; webviewView.webview.html = getHtml(webviewView.webview, logoUri.toString()); webviewView.webview.onDidReceiveMessage(async (msg) => { switch (msg.type) { case 'ready': await this.pushState(); break; case 'run': if (msg.action === 'codeAssist') { await this.runCodeAssistFromSidebar(String(msg.text ?? '')); break; } await this.run(msg.action as ActionId, String(msg.text ?? ''), msg.options ?? {}); break; case 'insertSelection': await this.insertSelection(); break; case 'openSettings': await vscode.commands.executeCommand('lexai.openSettings'); break; case 'copy': await vscode.env.clipboard.writeText(String(msg.text ?? '')); await webviewView.webview.postMessage({ type: 'copied' }); break; } }); } private async pushState(extra?: { status?: string; error?: string; result?: string }): Promise { if (!this.view) return; const settings = readSettings(); const keyed = await hasApiKey(this.context); await this.view.webview.postMessage({ type: 'state', catalog: settingsCatalog(), settings: { writingStyle: settings.writingStyle, promptPattern: settings.promptPattern, promptPersona: settings.promptPersona, promptFormat: settings.promptFormat, }, actions: [ { id: 'codeAssist', label: 'Code Assist' }, ...ACTIONS.map((id) => ({ id, label: ACTION_LABELS[id] })), ], hasApiKey: keyed, minLength: MIN_SELECTION_LENGTH, status: extra?.status, error: extra?.error, result: extra?.result, }); } private async insertSelection(): Promise { const editor = vscode.window.activeTextEditor; const text = editor && !editor.selection.isEmpty ? editor.document.getText(editor.selection) : ''; if (!this.view) return; if (!text.trim()) { await this.view.webview.postMessage({ type: 'status', error: 'No editor selection to insert.', }); return; } await this.view.webview.postMessage({ type: 'setInput', text }); } private async runCodeAssistFromSidebar(instruction: string): Promise { if (!this.view) return; const goal = instruction.trim(); if (goal.length < 3) { await this.pushState({ error: 'Describe what you want LexAI to do with the editor selection.', }); return; } const editor = vscode.window.activeTextEditor; if (!editor || editor.selection.isEmpty) { await this.pushState({ error: 'Select code in the editor first (Code Assist uses that selection + related files).', }); return; } await runCodeAssist(this.context, goal); await this.pushState({ status: 'Code Assist result is in the editor suggestion zone.', }); } private async run( action: ActionId, text: string, options: { writingStyle?: string; promptPattern?: string; promptPersona?: string; promptFormat?: string; }, ): Promise { if (!this.view) return; const trimmed = text.trim(); if (trimmed.length < MIN_SELECTION_LENGTH) { await this.pushState({ error: `Enter at least ${MIN_SELECTION_LENGTH} characters.`, }); return; } const resolved = await resolveConfig(this.context); if (resolved.error || !resolved.config) { await this.pushState({ error: resolved.error ?? 'Not configured' }); return; } const settings = readSettings(); const writingStyle = (options.writingStyle || settings.writingStyle) as typeof settings.writingStyle; const promptPattern = options.promptPattern || settings.promptPattern; const promptPersona = options.promptPersona || settings.promptPersona; const promptFormat = options.promptFormat || settings.promptFormat; const label = ACTION_LABELS[action]; const response = await withLexAIProgress(`LexAI: ${label}…`, () => generateSuggestion({ action, text, config: resolved.config!, writingStyle, promptParams: { pattern: promptPattern, persona: resolvePromptPersona(promptPersona, settings.customPersona), format: promptFormat, }, promptModel: settings.promptModel || undefined, }), ); if (response.error || !response.result) { await this.pushState({ error: response.error ?? 'Empty response.' }); return; } await this.pushState({ result: response.result, status: `${label} done.` }); } } function getHtml(webview: vscode.Webview, logoUri: string): string { const csp = [ `default-src 'none'`, `img-src ${webview.cspSource} data:`, `style-src ${webview.cspSource} 'unsafe-inline'`, `script-src ${webview.cspSource} 'unsafe-inline'`, ].join('; '); return ` LexAI
LexAI

LexAI

Writing help in the sidebar · Code Assist on editor selections with workspace context.

Input

Action

Output

Read-only · copy when ready
`; }