diff --git a/extensions/ai-agent/README.md b/extensions/ai-agent/README.md index e9be2e27e747f..ac10a180fba96 100644 --- a/extensions/ai-agent/README.md +++ b/extensions/ai-agent/README.md @@ -7,7 +7,10 @@ A minimal built-in extension that contributes an AI activity bar icon and a side - Commands: - `AI: Open Chat Sidebar` (`aiAgent.open`) focuses the AI view container - `AI: Send Message` (`aiAgent.sendMessage`) posts a stubbed echo reply to the chat + - `AI: Select Model` (`aiAgent.selectModel`) switches between demo Grok model variants Notes -- The chat is a simple webview view with no networking; responses are stubbed. +- The chat is a simple webview view with no networking; responses are stubbed. When a selected model id starts with `grok`, canned responses are returned. +- Selected model is persisted in `extensions/ai-agent/config/ai-agent.json` under `currentModel`. +- Environment: A placeholder `GROK_API_KEY` may be configured in your environment for future work, but it is not used by this demo code. - Styling uses flat colors and `--ai-accent: #0aa`. diff --git a/extensions/ai-agent/config/ai-agent.json b/extensions/ai-agent/config/ai-agent.json new file mode 100644 index 0000000000000..b02bac3651f00 --- /dev/null +++ b/extensions/ai-agent/config/ai-agent.json @@ -0,0 +1,3 @@ +{ + "currentModel": "grok-2" +} diff --git a/extensions/ai-agent/package.json b/extensions/ai-agent/package.json index 678660137ebf6..b93ec55cde517 100644 --- a/extensions/ai-agent/package.json +++ b/extensions/ai-agent/package.json @@ -12,7 +12,8 @@ "activationEvents": [ "onView:aiAgent.chat", "onCommand:aiAgent.open", - "onCommand:aiAgent.sendMessage" + "onCommand:aiAgent.sendMessage", + "onCommand:aiAgent.selectModel" ], "main": "./out/extension", "capabilities": { @@ -47,6 +48,11 @@ "command": "aiAgent.sendMessage", "title": "AI: Send Message", "category": "AI" + }, + { + "command": "aiAgent.selectModel", + "title": "AI: Select Model", + "category": "AI" } ] }, diff --git a/extensions/ai-agent/src/extension.ts b/extensions/ai-agent/src/extension.ts index 6f23d9dbcd27f..4f876db29bacb 100644 --- a/extensions/ai-agent/src/extension.ts +++ b/extensions/ai-agent/src/extension.ts @@ -1,10 +1,14 @@ import * as vscode from 'vscode'; +import { MODEL_LIST, MODEL_REGISTRY, DEFAULT_MODEL_ID } from './models'; class AIChatViewProvider implements vscode.WebviewViewProvider { public static readonly viewId = 'aiAgent.chat'; private _view?: vscode.WebviewView; + private _currentModelId: string; - constructor(private readonly _context: vscode.ExtensionContext) {} + constructor(private readonly _context: vscode.ExtensionContext, initialModelId: string) { + this._currentModelId = initialModelId || DEFAULT_MODEL_ID; + } resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable { this._view = webviewView; @@ -22,20 +26,38 @@ class AIChatViewProvider implements vscode.WebviewViewProvider { if (msg?.type === 'send' && typeof msg.text === 'string') { const text: string = msg.text.trim(); if (!text) { return; } - // Stubbed response only - const response = `Echo: ${text}`; - webview.postMessage({ type: 'response', text: response }); + if (this._currentModelId.startsWith('grok')) { + const canned: Record = { + 'grok-2': 'Grok 2 (demo): This is a canned response for screenshots.', + 'grok-mini': 'Grok Mini (demo): Quick stubbed reply for demo.', + }; + const response = canned[this._currentModelId] ?? 'Grok (demo): Stubbed response.'; + webview.postMessage({ type: 'response', text: response }); + } else { + const response = `Echo: ${text}`; + webview.postMessage({ type: 'response', text: response }); + } } }); } + public setModel(id: string) { + this._currentModelId = id; + this._view?.webview.postMessage({ type: 'modelChanged', label: this._currentModelLabel }); + } + public post(text: string) { this._view?.webview.postMessage({ type: 'response', text }); } + private get _currentModelLabel() { + return MODEL_REGISTRY[this._currentModelId]?.label ?? this._currentModelId; + } + private _getHtmlForWebview(webview: vscode.Webview): string { const nonce = getNonce(); const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._context.extensionUri, 'media', 'styles.css')); + const initialModelLabel = this._currentModelLabel.replace(/"/g, '\\"'); return /* html */ ` @@ -46,7 +68,7 @@ class AIChatViewProvider implements vscode.WebviewViewProvider { AI Chat -
AI Chat
+
AI Chat —
@@ -59,6 +81,8 @@ class AIChatViewProvider implements vscode.WebviewViewProvider { const messages = document.getElementById('messages'); const input = document.getElementById('input'); const send = document.getElementById('send'); + const modelLabelEl = document.getElementById('modelLabel'); + modelLabelEl.textContent = "${initialModelLabel}"; function addMessage(text, role) { const el = document.createElement('div'); @@ -89,6 +113,9 @@ class AIChatViewProvider implements vscode.WebviewViewProvider { if (msg?.type === 'response') { addMessage(msg.text, 'assistant'); } + if (msg?.type === 'modelChanged') { + modelLabelEl.textContent = msg.label || ''; + } }); addMessage('Hi! This is a stubbed demo chat.','assistant'); @@ -98,8 +125,9 @@ class AIChatViewProvider implements vscode.WebviewViewProvider { } } -export function activate(context: vscode.ExtensionContext) { - const provider = new AIChatViewProvider(context); +export async function activate(context: vscode.ExtensionContext) { + const currentModelId = await loadCurrentModel(context); + const provider = new AIChatViewProvider(context, currentModelId); context.subscriptions.push( vscode.window.registerWebviewViewProvider(AIChatViewProvider.viewId, provider), vscode.commands.registerCommand('aiAgent.open', async () => { @@ -110,12 +138,46 @@ export function activate(context: vscode.ExtensionContext) { if (value) { provider.post(`Echo: ${value}`); } + }), + vscode.commands.registerCommand('aiAgent.selectModel', async () => { + const pick = await vscode.window.showQuickPick( + MODEL_LIST.map(m => ({ label: m.label, description: `${m.contextTokens.toLocaleString()} tokens`, picked: m.id === currentModelId, id: m.id } as (vscode.QuickPickItem & { id: string }))), + { title: 'Select AI Model' } + ); + if (pick && 'id' in pick) { + await saveCurrentModel(context, pick.id); + provider.setModel(pick.id); + } }) ); } export function deactivate() {} +async function loadCurrentModel(context: vscode.ExtensionContext): Promise { + const configUri = vscode.Uri.joinPath(context.extensionUri, 'config', 'ai-agent.json'); + try { + const buf = await vscode.workspace.fs.readFile(configUri); + const json = JSON.parse(new TextDecoder().decode(buf)); + const id = typeof json?.currentModel === 'string' ? json.currentModel : undefined; + if (id && MODEL_REGISTRY[id]) { + return id; + } + } catch { + // ignore + } + await saveCurrentModel(context, DEFAULT_MODEL_ID); + return DEFAULT_MODEL_ID; +} + +async function saveCurrentModel(context: vscode.ExtensionContext, id: string): Promise { + const configDir = vscode.Uri.joinPath(context.extensionUri, 'config'); + const configUri = vscode.Uri.joinPath(configDir, 'ai-agent.json'); + try { await vscode.workspace.fs.createDirectory(configDir); } catch {} + const content = JSON.stringify({ currentModel: id }, null, 2); + await vscode.workspace.fs.writeFile(configUri, new TextEncoder().encode(content)); +} + function getNonce() { let text = ''; const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; diff --git a/extensions/ai-agent/src/models.ts b/extensions/ai-agent/src/models.ts new file mode 100644 index 0000000000000..4eb9c36661902 --- /dev/null +++ b/extensions/ai-agent/src/models.ts @@ -0,0 +1,14 @@ +export interface ModelInfo { + id: string; + label: string; + contextTokens: number; +} + +export const MODEL_REGISTRY: Record = { + 'grok-2': { id: 'grok-2', label: 'Grok 2 (demo)', contextTokens: 32768 }, + 'grok-mini': { id: 'grok-mini', label: 'Grok Mini (demo)', contextTokens: 8192 }, +}; + +export const MODEL_LIST: ModelInfo[] = Object.values(MODEL_REGISTRY); + +export const DEFAULT_MODEL_ID = 'grok-2';