Add VS Code Chat Participant API integration

- Register @productos as native chat participant
- Users can type @productos in VS Code chat
- Connects to Control Plane backend
- Removes dependency on Continue
- Native Copilot-like experience
This commit is contained in:
2026-01-19 20:39:30 -08:00
parent b6d7148ded
commit cb8ff46020
3 changed files with 263 additions and 30 deletions

View File

@@ -1,14 +1,41 @@
{
"name": "gcp-productos",
"displayName": "GCP Product OS",
"description": "Product-centric IDE for launching and operating SaaS products on Google Cloud.",
"version": "0.1.0",
"description": "Product-centric IDE for launching and operating SaaS products on Google Cloud. Use @productos in chat!",
"version": "0.2.0",
"publisher": "productos",
"engines": { "vscode": "^1.90.0" },
"categories": ["Other"],
"categories": ["AI", "Chat", "Other"],
"activationEvents": ["onStartupFinished"],
"main": "./dist/extension.js",
"contributes": {
"chatParticipants": [
{
"id": "productos.chat",
"fullName": "Product OS",
"name": "productos",
"description": "Deploy, analyze, and automate your SaaS product on Google Cloud",
"isSticky": true,
"commands": [
{
"name": "deploy",
"description": "Deploy a service to Cloud Run"
},
{
"name": "analytics",
"description": "Get funnel and conversion analytics"
},
{
"name": "marketing",
"description": "Generate marketing content"
},
{
"name": "status",
"description": "Check service status"
}
]
}
],
"viewsContainers": {
"activitybar": [
{
@@ -20,12 +47,6 @@
},
"views": {
"productos": [
{
"id": "productos.chat",
"type": "webview",
"name": "Chat",
"icon": "$(comment-discussion)"
},
{
"id": "productos.tools",
"name": "Tools",
@@ -44,18 +65,13 @@
}
],
"commands": [
{ "command": "productos.chat", "title": "Product OS: Open Chat", "icon": "$(comment-discussion)" },
{ "command": "productos.configure", "title": "Product OS: Configure Backend", "icon": "$(gear)" },
{ "command": "productos.refresh", "title": "Product OS: Refresh", "icon": "$(refresh)" },
{ "command": "productos.tools.list", "title": "Product OS: List Tools" },
{ "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool", "icon": "$(play)" },
{ "command": "productos.tools.invokeFromTree", "title": "Invoke Tool", "icon": "$(play)" },
{ "command": "productos.runs.open", "title": "Product OS: Open Run" },
{ "command": "productos.runs.openFromTree", "title": "View Run Details", "icon": "$(eye)" },
{ "command": "productos.openPanel", "title": "Product OS: Open Panel" }
],
"keybindings": [
{ "command": "productos.chat", "key": "ctrl+shift+p", "mac": "cmd+shift+p", "when": "editorTextFocus" }
{ "command": "productos.runs.openFromTree", "title": "View Run Details", "icon": "$(eye)" }
],
"menus": {
"view/title": [

View File

@@ -0,0 +1,223 @@
import * as vscode from "vscode";
import { getBackendUrl } from "./api";
/**
* Product OS Chat Participant
*
* Registers @productos in the native VS Code chat panel.
* Users can type "@productos deploy to staging" and get responses
* in the same UI as GitHub Copilot.
*/
// Chat response interface from Control Plane
interface ChatResponse {
message: string;
toolCalls?: { name: string; arguments: any }[];
runs?: any[];
finishReason: string;
}
/**
* Register the Product OS chat participant
*/
export function registerChatParticipant(context: vscode.ExtensionContext) {
// Create the chat participant
const participant = vscode.chat.createChatParticipant(
"productos.chat",
chatHandler
);
// Set participant properties
participant.iconPath = vscode.Uri.joinPath(context.extensionUri, "media", "icon.svg");
// Add follow-up provider for suggestions
participant.followupProvider = {
provideFollowups(
result: vscode.ChatResult,
context: vscode.ChatContext,
token: vscode.CancellationToken
): vscode.ProviderResult<vscode.ChatFollowup[]> {
// Suggest follow-up actions based on what was just done
return [
{ prompt: "Show me funnel analytics", label: "📊 Analytics" },
{ prompt: "Deploy to staging", label: "🚀 Deploy" },
{ prompt: "Generate marketing posts", label: "📣 Marketing" },
{ prompt: "What drives conversions?", label: "📈 Drivers" }
];
}
};
context.subscriptions.push(participant);
console.log("[Product OS] Chat participant @productos registered");
}
/**
* Handle chat requests
*/
async function chatHandler(
request: vscode.ChatRequest,
context: vscode.ChatContext,
response: vscode.ChatResponseStream,
token: vscode.CancellationToken
): Promise<vscode.ChatResult> {
const userPrompt = request.prompt;
console.log("[Product OS] Chat request:", userPrompt);
// Show progress
response.progress("Connecting to Product OS...");
try {
// Build message history from context
const messages = buildMessageHistory(context, userPrompt);
// Get code context if available
const codeContext = await getCodeContext();
// Call the Control Plane
const result = await callControlPlane(messages, codeContext);
// Handle tool calls
if (result.toolCalls && result.toolCalls.length > 0) {
for (const toolCall of result.toolCalls) {
response.markdown(`\n\n**🔧 Executing:** \`${toolCall.name}\`\n`);
}
}
// Handle runs (tool execution results)
if (result.runs && result.runs.length > 0) {
for (const run of result.runs) {
const status = run.status === "succeeded" ? "✅" : "❌";
response.markdown(`\n${status} **${run.tool}**\n`);
if (run.output) {
response.markdown("\n```json\n" + JSON.stringify(run.output, null, 2) + "\n```\n");
}
if (run.error) {
response.markdown(`\n**Error:** ${run.error.message}\n`);
}
}
}
// Stream the main response
if (result.message) {
response.markdown(result.message);
}
return { metadata: { command: "chat" } };
} catch (error: any) {
console.error("[Product OS] Chat error:", error);
response.markdown(`\n\n❌ **Error:** ${error.message}\n\n`);
response.markdown("Make sure the Control Plane is running at " + getBackendUrl());
return {
metadata: { command: "error" },
errorDetails: { message: error.message }
};
}
}
/**
* Build message history from chat context
*/
function buildMessageHistory(
context: vscode.ChatContext,
currentPrompt: string
): { role: string; content: string }[] {
const messages: { role: string; content: string }[] = [];
// Add previous messages from context
for (const turn of context.history) {
if (turn instanceof vscode.ChatRequestTurn) {
messages.push({ role: "user", content: turn.prompt });
} else if (turn instanceof vscode.ChatResponseTurn) {
// Extract text from response
let text = "";
for (const part of turn.response) {
if (part instanceof vscode.ChatResponseMarkdownPart) {
text += part.value.value;
}
}
if (text) {
messages.push({ role: "assistant", content: text });
}
}
}
// Add current prompt
messages.push({ role: "user", content: currentPrompt });
return messages;
}
/**
* Get context from the active editor
*/
async function getCodeContext(): Promise<any | undefined> {
const editor = vscode.window.activeTextEditor;
if (!editor) return undefined;
const selection = editor.selection;
const selectedText = editor.document.getText(selection);
if (selectedText) {
return {
selection: {
path: vscode.workspace.asRelativePath(editor.document.uri),
text: selectedText,
startLine: selection.start.line + 1
}
};
}
// No selection - include some context from the current file
const document = editor.document;
const cursorLine = selection.active.line;
const startLine = Math.max(0, cursorLine - 20);
const endLine = Math.min(document.lineCount - 1, cursorLine + 20);
const range = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text.length);
const surroundingCode = document.getText(range);
if (surroundingCode.trim()) {
return {
files: [{
path: vscode.workspace.asRelativePath(editor.document.uri),
content: surroundingCode
}]
};
}
return undefined;
}
/**
* Call the Control Plane chat endpoint
*/
async function callControlPlane(
messages: { role: string; content: string }[],
context?: any
): Promise<ChatResponse> {
const backendUrl = getBackendUrl();
const response = await fetch(`${backendUrl}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages,
context,
autoExecuteTools: true
})
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Control Plane error: ${response.status} - ${text}`);
}
return response.json();
}

View File

@@ -4,30 +4,24 @@ import { ToolsTreeProvider, ToolItem } from "./toolsTreeView";
import { RunsTreeProvider, RunItem } from "./runsTreeView";
import { createStatusBar, updateConnectionStatus, dispose as disposeStatusBar } from "./statusBar";
import { InvokePanel } from "./invokePanel";
import { ChatPanel } from "./chatPanel";
import { ChatViewProvider } from "./chatViewProvider";
import { showJson, openRun, showRunDocument } from "./ui";
import { registerChatParticipant } from "./chatParticipant";
export function activate(context: vscode.ExtensionContext) {
console.log("Product OS extension activated");
// Register @productos in the native VS Code chat
// This gives us the Copilot-like chat experience for FREE
try {
registerChatParticipant(context);
} catch (e) {
console.log("[Product OS] Chat Participant API not available (requires VS Code 1.90+)");
}
// Create tree providers
const toolsProvider = new ToolsTreeProvider();
const runsProvider = new RunsTreeProvider();
// Register sidebar Chat view provider
const chatViewProvider = new ChatViewProvider(context.extensionUri);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, chatViewProvider)
);
// Register Chat panel command (opens in editor area)
context.subscriptions.push(
vscode.commands.registerCommand("productos.chat", () => {
ChatPanel.createOrShow(context.extensionUri);
})
);
// Register tree views
vscode.window.registerTreeDataProvider("productos.tools", toolsProvider);
vscode.window.registerTreeDataProvider("productos.runs", runsProvider);