Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion extensions/ai-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
3 changes: 3 additions & 0 deletions extensions/ai-agent/config/ai-agent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"currentModel": "grok-2"
}
8 changes: 7 additions & 1 deletion extensions/ai-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"activationEvents": [
"onView:aiAgent.chat",
"onCommand:aiAgent.open",
"onCommand:aiAgent.sendMessage"
"onCommand:aiAgent.sendMessage",
"onCommand:aiAgent.selectModel"
],
"main": "./out/extension",
"capabilities": {
Expand Down Expand Up @@ -47,6 +48,11 @@
"command": "aiAgent.sendMessage",
"title": "AI: Send Message",
"category": "AI"
},
{
"command": "aiAgent.selectModel",
"title": "AI: Select Model",
"category": "AI"
}
]
},
Expand Down
76 changes: 69 additions & 7 deletions extensions/ai-agent/src/extension.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
this._view = webviewView;
Expand All @@ -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<string, string> = {
'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 */ `<!DOCTYPE html>
<html lang="en">
<head>
Expand All @@ -46,7 +68,7 @@ class AIChatViewProvider implements vscode.WebviewViewProvider {
<title>AI Chat</title>
</head>
<body>
<header class="ai-header">AI Chat</header>
<header class="ai-header">AI Chat — <span id="modelLabel"></span></header>
<main class="ai-main">
<div id="messages" class="ai-messages" aria-live="polite"></div>
<div class="ai-input">
Expand All @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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 () => {
Expand All @@ -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<string> {
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<void> {
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';
Expand Down
14 changes: 14 additions & 0 deletions extensions/ai-agent/src/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface ModelInfo {
id: string;
label: string;
contextTokens: number;
}

export const MODEL_REGISTRY: Record<string, ModelInfo> = {
'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';