diff --git a/src/api/ApiClient.ts b/src/api/ApiClient.ts index c024b44..498434d 100644 --- a/src/api/ApiClient.ts +++ b/src/api/ApiClient.ts @@ -10,6 +10,12 @@ export interface AgentRun { web_url?: string; github_pull_requests?: any[]; source_type?: string; + repository?: { + id: number; + name: string; + full_name: string; + owner: string; + }; } export interface AgentRunsResponse { @@ -62,19 +68,26 @@ export class ApiClient { ); } - async getAgentRuns(page: number = 1, perPage: number = 10): Promise { + async getAgentRuns(page: number = 1, perPage: number = 10, repositoryName?: string): Promise { const orgId = this.authManager.getOrgId(); if (!orgId) { throw new Error('No organization ID found. Please login again.'); } try { + const params: any = { + page, + per_page: perPage, + source_type: 'API' // Filter to API source type like the CLI does + }; + + // Add repository filter if provided + if (repositoryName) { + params.repository = repositoryName; + } + const response = await this.client.get(`/v1/organizations/${orgId}/agent/runs`, { - params: { - page, - per_page: perPage, - source_type: 'API' // Filter to API source type like the CLI does - } + params }); return response.data; diff --git a/src/extension.ts b/src/extension.ts index d6f6777..fc88bf2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { AgentRunsProvider } from './providers/AgentRunsProvider'; import { AuthManager } from './auth/AuthManager'; import { ApiClient } from './api/ApiClient'; +import { PrUtils } from './utils/PrUtils'; export function activate(context: vscode.ExtensionContext) { console.log('Codegen IDE extension is now active!'); @@ -62,7 +63,11 @@ export function activate(context: vscode.ExtensionContext) { if (prompt) { try { - const agentRun = await apiClient.createAgentRun(prompt); + // Get current repository for context + const currentRepo = agentRunsProvider.getCurrentRepository(); + const repoId = currentRepo ? undefined : undefined; // TODO: Map repository name to ID + + const agentRun = await apiClient.createAgentRun(prompt, undefined, repoId); vscode.window.showInformationMessage( `Agent run created successfully! ID: ${agentRun.id}`, 'View in Browser' @@ -86,6 +91,29 @@ export function activate(context: vscode.ExtensionContext) { if (agentRun.web_url) { vscode.env.openExternal(vscode.Uri.parse(agentRun.web_url)); } + }), + + vscode.commands.registerCommand('codegen.openAgentRunOrPr', async (agentRun) => { + // First, try to open the PR if it exists + const prInfo = PrUtils.extractPrInfo(agentRun); + + if (prInfo) { + // Get current repository for local path context + const currentRepo = agentRunsProvider.getCurrentRepository(); + + if (currentRepo && currentRepo.fullName === prInfo.repository) { + // We're in the same repository, try to open PR diff view + await PrUtils.openPrDiffView(prInfo, currentRepo.localPath); + } else { + // Different repository or no local repo, open PR in browser + await PrUtils.openPrDiff(prInfo); + } + } else { + // No PR, fall back to opening the agent run in browser + if (agentRun.web_url) { + vscode.env.openExternal(vscode.Uri.parse(agentRun.web_url)); + } + } }) ]; diff --git a/src/providers/AgentRunsProvider.ts b/src/providers/AgentRunsProvider.ts index 3c519fe..535516a 100644 --- a/src/providers/AgentRunsProvider.ts +++ b/src/providers/AgentRunsProvider.ts @@ -1,6 +1,8 @@ import * as vscode from 'vscode'; import { ApiClient, AgentRun } from '../api/ApiClient'; import { AuthManager } from '../auth/AuthManager'; +import { GitUtils, GitRepository } from '../utils/GitUtils'; +import { PrUtils, PullRequest } from '../utils/PrUtils'; export class AgentRunItem extends vscode.TreeItem { constructor( @@ -14,10 +16,10 @@ export class AgentRunItem extends vscode.TreeItem { this.iconPath = this.getIcon(); this.contextValue = 'agentRun'; - // Make it clickable to open in browser + // Make it clickable to open PR diff or agent run this.command = { - command: 'codegen.openAgentRun', - title: 'Open Agent Run', + command: 'codegen.openAgentRunOrPr', + title: 'Open Agent Run or PR', arguments: [agentRun] }; } @@ -49,10 +51,10 @@ export class AgentRunItem extends vscode.TreeItem { timeAgo = `${diffMinutes}m ago`; } - const prCount = this.agentRun.github_pull_requests?.length || 0; - const prText = prCount > 0 ? ` • ${prCount} PR${prCount > 1 ? 's' : ''}` : ''; + const prText = PrUtils.getPrStatusDescription(this.agentRun); + const repoText = this.agentRun.repository ? ` • ${this.agentRun.repository.name}` : ''; - return `${status} • ${timeAgo}${prText}`; + return `${status} • ${timeAgo} • ${prText}${repoText}`; } private getIcon(): vscode.ThemeIcon { @@ -82,16 +84,36 @@ export class AgentRunsProvider implements vscode.TreeDataProvider readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; private agentRuns: AgentRun[] = []; + private currentRepository: GitRepository | null = null; constructor( private apiClient: ApiClient, private authManager: AuthManager - ) {} + ) { + // Listen for workspace changes to update repository context + vscode.workspace.onDidChangeWorkspaceFolders(() => { + this.updateRepositoryContext(); + }); + + // Initialize repository context + this.updateRepositoryContext(); + } refresh(): void { + this.updateRepositoryContext(); this._onDidChangeTreeData.fire(); } + private async updateRepositoryContext(): Promise { + try { + this.currentRepository = await GitUtils.getCurrentRepository(); + console.log('Current repository:', this.currentRepository?.fullName || 'None'); + } catch (error) { + console.error('Failed to get current repository:', error); + this.currentRepository = null; + } + } + getTreeItem(element: AgentRunItem): vscode.TreeItem { return element; } @@ -104,9 +126,24 @@ export class AgentRunsProvider implements vscode.TreeDataProvider if (!element) { // Root level - return agent runs try { - const response = await this.apiClient.getAgentRuns(1, 20); // Get first 20 runs + // Filter by current repository if available + const repositoryName = this.currentRepository?.fullName; + const response = await this.apiClient.getAgentRuns(1, 20, repositoryName); this.agentRuns = response.items; + // If we have a repository context but no results, show a helpful message + if (this.agentRuns.length === 0 && repositoryName) { + // Return a placeholder item to show the user what's happening + const placeholderItem = new vscode.TreeItem( + `No agents found for ${repositoryName}`, + vscode.TreeItemCollapsibleState.None + ); + placeholderItem.description = 'Try creating a new agent or check a different repository'; + placeholderItem.iconPath = new vscode.ThemeIcon('info'); + placeholderItem.contextValue = 'placeholder'; + return [placeholderItem]; + } + return this.agentRuns.map(agentRun => new AgentRunItem(agentRun, vscode.TreeItemCollapsibleState.None) ); @@ -119,4 +156,8 @@ export class AgentRunsProvider implements vscode.TreeDataProvider return []; } + + getCurrentRepository(): GitRepository | null { + return this.currentRepository; + } } diff --git a/src/utils/GitUtils.ts b/src/utils/GitUtils.ts new file mode 100644 index 0000000..524ae80 --- /dev/null +++ b/src/utils/GitUtils.ts @@ -0,0 +1,125 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +export interface GitRepository { + name: string; + owner: string; + fullName: string; + localPath: string; +} + +export class GitUtils { + /** + * Get the current git repository information from the active workspace + */ + static async getCurrentRepository(): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return null; + } + + // Try to get the repository from the first workspace folder + const workspaceFolder = workspaceFolders[0]; + const gitExtension = vscode.extensions.getExtension('vscode.git'); + + if (!gitExtension) { + console.warn('Git extension not found'); + return null; + } + + if (!gitExtension.isActive) { + await gitExtension.activate(); + } + + const git = gitExtension.exports.getAPI(1); + if (!git) { + console.warn('Git API not available'); + return null; + } + + // Find the repository for the current workspace + const repository = git.repositories.find((repo: any) => + repo.rootUri.fsPath === workspaceFolder.uri.fsPath + ); + + if (!repository) { + console.warn('No git repository found in workspace'); + return null; + } + + try { + // Get remote origin URL + const remotes = repository.state.remotes; + const origin = remotes.find((remote: any) => remote.name === 'origin'); + + if (!origin || !origin.fetchUrl) { + console.warn('No origin remote found'); + return null; + } + + const repoInfo = this.parseGitUrl(origin.fetchUrl); + if (!repoInfo) { + console.warn('Could not parse git URL:', origin.fetchUrl); + return null; + } + + return { + ...repoInfo, + localPath: repository.rootUri.fsPath + }; + } catch (error) { + console.error('Error getting repository info:', error); + return null; + } + } + + /** + * Parse a git URL to extract owner and repository name + */ + private static parseGitUrl(url: string): { name: string; owner: string; fullName: string } | null { + // Handle different Git URL formats + const patterns = [ + // SSH format: git@github.com:owner/repo.git + /^git@([^:]+):([^\/]+)\/(.+?)(?:\.git)?$/, + // HTTPS format: https://github.com/owner/repo.git + /^https?:\/\/([^\/]+)\/([^\/]+)\/(.+?)(?:\.git)?$/, + // HTTP format: http://github.com/owner/repo.git + /^https?:\/\/([^\/]+)\/([^\/]+)\/(.+?)(?:\.git)?$/ + ]; + + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) { + const [, , owner, name] = match; + return { + name: name.replace(/\.git$/, ''), + owner, + fullName: `${owner}/${name.replace(/\.git$/, '')}` + }; + } + } + + return null; + } + + /** + * Check if the current workspace has a git repository + */ + static async hasGitRepository(): Promise { + const repo = await this.getCurrentRepository(); + return repo !== null; + } + + /** + * Get repository ID from the Codegen API based on the repository name + * This would need to be implemented based on the API structure + */ + static getRepositoryId(repoFullName: string): number | null { + // This is a placeholder - in a real implementation, you'd need to: + // 1. Call the Codegen API to get a list of repositories + // 2. Find the matching repository by name + // 3. Return its ID + // For now, we'll return null and filter by repository name in the API call + return null; + } +} diff --git a/src/utils/PrUtils.ts b/src/utils/PrUtils.ts new file mode 100644 index 0000000..5db1b84 --- /dev/null +++ b/src/utils/PrUtils.ts @@ -0,0 +1,132 @@ +import * as vscode from 'vscode'; +import { AgentRun } from '../api/ApiClient'; + +export interface PullRequest { + number: number; + title: string; + url: string; + head_sha: string; + base_sha: string; + repository: string; +} + +export class PrUtils { + /** + * Extract PR information from an agent run + */ + static extractPrInfo(agentRun: AgentRun): PullRequest | null { + if (!agentRun.github_pull_requests || agentRun.github_pull_requests.length === 0) { + return null; + } + + // Get the first PR (most recent) + const pr = agentRun.github_pull_requests[0]; + + if (!pr.number || !pr.html_url) { + return null; + } + + return { + number: pr.number, + title: pr.title || `PR #${pr.number}`, + url: pr.html_url, + head_sha: pr.head?.sha || '', + base_sha: pr.base?.sha || '', + repository: pr.base?.repo?.full_name || '' + }; + } + + /** + * Open a PR diff in VSCode using the GitHub Pull Requests extension + */ + static async openPrDiff(pr: PullRequest): Promise { + try { + // First, try to use the GitHub Pull Requests extension + const githubExtension = vscode.extensions.getExtension('GitHub.vscode-pull-request-github'); + + if (githubExtension) { + if (!githubExtension.isActive) { + await githubExtension.activate(); + } + + // Try to open the PR using the GitHub extension command + try { + await vscode.commands.executeCommand('pr.openPullRequestInGitHub', pr.url); + return; + } catch (error) { + console.warn('Failed to open PR with GitHub extension:', error); + } + } + + // Fallback: Open the PR URL in the browser + await vscode.env.openExternal(vscode.Uri.parse(pr.url)); + + } catch (error) { + console.error('Failed to open PR:', error); + vscode.window.showErrorMessage(`Failed to open PR: ${error}`); + } + } + + /** + * Open PR diff view using VSCode's built-in diff viewer + * This requires the repository to be cloned locally + */ + static async openPrDiffView(pr: PullRequest, localRepoPath: string): Promise { + try { + // Check if we have the GitHub extension for better PR integration + const githubExtension = vscode.extensions.getExtension('GitHub.vscode-pull-request-github'); + + if (githubExtension && githubExtension.isActive) { + // Try to use the GitHub extension's PR view + try { + await vscode.commands.executeCommand('pr.openPullRequestInGitHub', pr.url); + return; + } catch (error) { + console.warn('GitHub extension command failed:', error); + } + } + + // Fallback: Show information and open in browser + const action = await vscode.window.showInformationMessage( + `PR #${pr.number}: ${pr.title}`, + 'View in Browser', + 'Copy URL' + ); + + if (action === 'View in Browser') { + await vscode.env.openExternal(vscode.Uri.parse(pr.url)); + } else if (action === 'Copy URL') { + await vscode.env.clipboard.writeText(pr.url); + vscode.window.showInformationMessage('PR URL copied to clipboard'); + } + + } catch (error) { + console.error('Failed to open PR diff view:', error); + vscode.window.showErrorMessage(`Failed to open PR diff: ${error}`); + } + } + + /** + * Check if the GitHub Pull Requests extension is available + */ + static isGitHubExtensionAvailable(): boolean { + const githubExtension = vscode.extensions.getExtension('GitHub.vscode-pull-request-github'); + return githubExtension !== undefined; + } + + /** + * Get a user-friendly description of PR status + */ + static getPrStatusDescription(agentRun: AgentRun): string { + const prCount = agentRun.github_pull_requests?.length || 0; + + if (prCount === 0) { + return 'No PRs'; + } else if (prCount === 1) { + const pr = agentRun.github_pull_requests![0]; + return `PR #${pr.number}`; + } else { + return `${prCount} PRs`; + } + } +}