diff --git a/platform/client-ide/extensions/gcp-productos/package.json b/platform/client-ide/extensions/gcp-productos/package.json index 3d458ce..cbd37cc 100644 --- a/platform/client-ide/extensions/gcp-productos/package.json +++ b/platform/client-ide/extensions/gcp-productos/package.json @@ -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": [ diff --git a/platform/client-ide/extensions/gcp-productos/src/chatParticipant.ts b/platform/client-ide/extensions/gcp-productos/src/chatParticipant.ts new file mode 100644 index 0000000..838976e --- /dev/null +++ b/platform/client-ide/extensions/gcp-productos/src/chatParticipant.ts @@ -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 { + // 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 { + + 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 { + 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 { + 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(); +} diff --git a/platform/client-ide/extensions/gcp-productos/src/extension.ts b/platform/client-ide/extensions/gcp-productos/src/extension.ts index d8f804a..7433f29 100644 --- a/platform/client-ide/extensions/gcp-productos/src/extension.ts +++ b/platform/client-ide/extensions/gcp-productos/src/extension.ts @@ -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);