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:
@@ -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": [
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user