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",
|
"name": "gcp-productos",
|
||||||
"displayName": "GCP Product OS",
|
"displayName": "GCP Product OS",
|
||||||
"description": "Product-centric IDE for launching and operating SaaS products on Google Cloud.",
|
"description": "Product-centric IDE for launching and operating SaaS products on Google Cloud. Use @productos in chat!",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"publisher": "productos",
|
"publisher": "productos",
|
||||||
"engines": { "vscode": "^1.90.0" },
|
"engines": { "vscode": "^1.90.0" },
|
||||||
"categories": ["Other"],
|
"categories": ["AI", "Chat", "Other"],
|
||||||
"activationEvents": ["onStartupFinished"],
|
"activationEvents": ["onStartupFinished"],
|
||||||
"main": "./dist/extension.js",
|
"main": "./dist/extension.js",
|
||||||
"contributes": {
|
"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": {
|
"viewsContainers": {
|
||||||
"activitybar": [
|
"activitybar": [
|
||||||
{
|
{
|
||||||
@@ -20,12 +47,6 @@
|
|||||||
},
|
},
|
||||||
"views": {
|
"views": {
|
||||||
"productos": [
|
"productos": [
|
||||||
{
|
|
||||||
"id": "productos.chat",
|
|
||||||
"type": "webview",
|
|
||||||
"name": "Chat",
|
|
||||||
"icon": "$(comment-discussion)"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "productos.tools",
|
"id": "productos.tools",
|
||||||
"name": "Tools",
|
"name": "Tools",
|
||||||
@@ -44,18 +65,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": [
|
"commands": [
|
||||||
{ "command": "productos.chat", "title": "Product OS: Open Chat", "icon": "$(comment-discussion)" },
|
|
||||||
{ "command": "productos.configure", "title": "Product OS: Configure Backend", "icon": "$(gear)" },
|
{ "command": "productos.configure", "title": "Product OS: Configure Backend", "icon": "$(gear)" },
|
||||||
{ "command": "productos.refresh", "title": "Product OS: Refresh", "icon": "$(refresh)" },
|
{ "command": "productos.refresh", "title": "Product OS: Refresh", "icon": "$(refresh)" },
|
||||||
{ "command": "productos.tools.list", "title": "Product OS: List Tools" },
|
{ "command": "productos.tools.list", "title": "Product OS: List Tools" },
|
||||||
{ "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool", "icon": "$(play)" },
|
{ "command": "productos.tools.invoke", "title": "Product OS: Invoke Tool", "icon": "$(play)" },
|
||||||
{ "command": "productos.tools.invokeFromTree", "title": "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.open", "title": "Product OS: Open Run" },
|
||||||
{ "command": "productos.runs.openFromTree", "title": "View Run Details", "icon": "$(eye)" },
|
{ "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" }
|
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
"view/title": [
|
"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 { RunsTreeProvider, RunItem } from "./runsTreeView";
|
||||||
import { createStatusBar, updateConnectionStatus, dispose as disposeStatusBar } from "./statusBar";
|
import { createStatusBar, updateConnectionStatus, dispose as disposeStatusBar } from "./statusBar";
|
||||||
import { InvokePanel } from "./invokePanel";
|
import { InvokePanel } from "./invokePanel";
|
||||||
import { ChatPanel } from "./chatPanel";
|
|
||||||
import { ChatViewProvider } from "./chatViewProvider";
|
|
||||||
import { showJson, openRun, showRunDocument } from "./ui";
|
import { showJson, openRun, showRunDocument } from "./ui";
|
||||||
|
import { registerChatParticipant } from "./chatParticipant";
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
console.log("Product OS extension activated");
|
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
|
// Create tree providers
|
||||||
const toolsProvider = new ToolsTreeProvider();
|
const toolsProvider = new ToolsTreeProvider();
|
||||||
const runsProvider = new RunsTreeProvider();
|
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
|
// Register tree views
|
||||||
vscode.window.registerTreeDataProvider("productos.tools", toolsProvider);
|
vscode.window.registerTreeDataProvider("productos.tools", toolsProvider);
|
||||||
vscode.window.registerTreeDataProvider("productos.runs", runsProvider);
|
vscode.window.registerTreeDataProvider("productos.runs", runsProvider);
|
||||||
|
|||||||
Reference in New Issue
Block a user