diff --git a/package.json b/package.json index 2df9b609a9..b4c1abd891 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "chatContextProvider", "chatParticipantAdditions", "chatParticipantPrivate", - "chatSessionsProvider@3", + "chatSessionsProvider", "codiconDecoration", "codeActionRanges", "commentingRangeHint", @@ -88,28 +88,6 @@ "displayName": "GitHub Issues" } ], - "chatSessions": [ - { - "type": "copilot-swe-agent", - "name": "copilot", - "displayName": "GitHub Copilot coding agent", - "description": "Delegate tasks to the GitHub Copilot coding agent. The agent works asynchronously to implement changes, iterates via chat, and can create or update pull requests as needed.", - "when": "config.chat.agentSessionsViewLocation && config.chat.agentSessionsViewLocation != 'disabled' && config.github.copilot.chat.advanced.copilotCodingAgentV0.enabled", - "capabilities": { - "supportsFileAttachments": true - } - } - ], - "remoteCodingAgents": [ - { - "id": "githubCodingAgent", - "command": "githubpr.remoteAgent", - "displayName": "GitHub Copilot coding agent", - "description": "Copilot coding agent is a remote, autonomous software development agent. Developers delegate tasks to the agent, which iterates on pull requests based on feedback and reviews.", - "followUpRegex": "open-pull-request-webview.*((%7B.*?%7D)|(\\{.*?\\}))", - "when": "config.githubPullRequests.codingAgent.enabled && config.githubPullRequests.codingAgent.uiIntegration && copilotCodingAgentAssignable && config.github.copilot.chat.advanced.copilotCodingAgentV0.enabled" - } - ], "chatParticipants": [ { "id": "githubpr", @@ -658,11 +636,6 @@ ], "description": "%githubIssues.createIssueTriggers.description%" }, - "githubPullRequests.codingAgent.codeLens": { - "type": "boolean", - "default": true, - "description": "%githubPullRequests.codingAgent.codeLens.description%" - }, "githubIssues.createInsertFormat": { "type": "string", "enum": [ @@ -971,11 +944,6 @@ ] }, "commands": [ - { - "command": "githubpr.remoteAgent", - "title": "%command.githubpr.remoteAgent.title%", - "enablement": "config.githubPullRequests.codingAgent.enabled" - }, { "command": "github.api.preloadPullRequest", "title": "Preload Pull Request", @@ -1888,38 +1856,10 @@ "category": "%command.notifications.category%", "icon": "$(gear)" }, - { - "command": "pr.refreshChatSessions", - "title": "%command.pr.refreshChatSessions.title%", - "icon": "$(refresh)", - "category": "%command.pull.request.category%" - }, - { - "command": "pr.preferredCodingAgentGitHubRemote", - "title": "%command.pr.preferredCodingAgentGitHubRemote.title%", - "icon": "$(gear)", - "enablement": "github:hasMultipleGitHubRemotes", - "category": "%command.pull.request.category%" - }, - { - "command": "pr.resetCodingAgentPreferences", - "title": "%command.pr.resetCodingAgentPreferences.title%", - "category": "%command.pull.request.category%" - }, { "command": "pr.checkoutChatSessionPullRequest", "title": "%command.pr.checkoutChatSessionPullRequest.title%", "category": "%command.pull.request.category%" - }, - { - "command": "pr.closeChatSessionPullRequest", - "title": "%command.pr.closeChatSessionPullRequest.title%", - "category": "%command.pull.request.category%" - }, - { - "command": "pr.cancelCodingAgent", - "title": "%command.pr.cancelCodingAgent.title%", - "category": "%command.pull.request.category%" } ], "viewsWelcome": [ @@ -2027,31 +1967,6 @@ "view": "notifications:github", "when": "ReposManagerStateContext == RepositoriesLoaded && github:notificationCount == -1", "contents": "%welcome.github.notifications.contents%" - }, - { - "view": "workbench.view.chat.sessions.copilot-swe-agent", - "when": "workspaceFolderCount == 0", - "contents": "%welcome.pr.github.noFolder.contents%" - }, - { - "view": "workbench.view.chat.sessions.copilot-swe-agent", - "when": "git.state == initialized && gitOpenRepositoryCount == 0 && workspaceFolderCount > 0 && git.parentRepositoryCount == 0", - "contents": "%welcome.pr.github.noRepo.contents%" - }, - { - "view": "workbench.view.chat.sessions.copilot-swe-agent", - "when": "git.state == initialized && workspaceFolderCount > 0 && (git.parentRepositoryCount > 0 || gitOpenRepositoryCount > 0) && !github:hasGitHubRemotes", - "contents": "%welcome.chat.sessions.copilot-swe-agent.noGitHub.contents%" - }, - { - "view": "workbench.view.chat.sessions.copilot-swe-agent", - "when": "ReposManagerStateContext == NeedsAuthentication && github:hasGitHubRemotes", - "contents": "%welcome.chat.sessions.copilot-swe-agent.login.contents%" - }, - { - "view": "workbench.view.chat.sessions.copilot-swe-agent", - "when": "ReposManagerStateContext != NeedsAuthentication && github:hasGitHubRemotes", - "contents": "%welcome.chat.sessions.copilot-swe-agent.startSession.contents%" } ], "keybindings": [ @@ -2097,10 +2012,6 @@ "command": "github.api.preloadPullRequest", "when": "false" }, - { - "command": "githubpr.remoteAgent", - "when": "false" - }, { "command": "pr.configureRemotes", "when": "gitHubOpenRepositoryCount != 0" @@ -2740,18 +2651,6 @@ { "command": "review.copyPrLink", "when": "github:inReviewMode" - }, - { - "command": "pr.preferredCodingAgentGitHubRemote", - "when": "false" - }, - { - "command": "pr.closeChatSessionPullRequest", - "when": "false" - }, - { - "command": "pr.cancelCodingAgent", - "when": "false" } ], "view/title": [ @@ -2904,16 +2803,6 @@ "command": "notifications.refresh", "when": "gitHubOpenRepositoryCount != 0 && github:initialized && view == notifications:github", "group": "navigation@1" - }, - { - "command": "pr.refreshChatSessions", - "when": "view == workbench.view.chat.sessions.copilot-swe-agent", - "group": "navigation@1" - }, - { - "command": "pr.preferredCodingAgentGitHubRemote", - "when": "github:hasMultipleGitHubRemotes && view == workbench.view.chat.sessions.copilot-swe-agent", - "group": "overflow@1" } ], "view/item/context": [ @@ -3708,22 +3597,12 @@ "chat/chatSessions": [ { "command": "pr.openChanges", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "when": "chatSessionType == copilot-cloud-agent", "group": "inline" }, { "command": "pr.checkoutChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", - "group": "context" - }, - { - "command": "pr.closeChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent", - "group": "context" - }, - { - "command": "pr.cancelCodingAgent", - "when": "chatSessionType == copilot-swe-agent", + "when": "chatSessionType == copilot-cloud-agent", "group": "context" } ], @@ -4545,7 +4424,6 @@ "fast-deep-equal": "^3.1.3", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", - "jszip": "^3.10.1", "lru-cache": "6.0.0", "markdown-it": "^14.1.0", "marked": "^4.0.10", diff --git a/package.nls.json b/package.nls.json index e83d1bd84e..76394136c1 100644 --- a/package.nls.json +++ b/package.nls.json @@ -113,7 +113,6 @@ "githubIssues.ignoreMilestones.description": "An array of milestone titles to never show issues from.", "githubIssues.createIssueTriggers.description": "Strings that will cause the 'Create issue from comment' code action to show.", "githubIssues.createIssueTriggers.items": "String that enables the 'Create issue from comment' code action. Should not contain whitespace.", - "githubPullRequests.codingAgent.codeLens.description": "Show the 'Delegate to agent' CodeLens actions above TODO comments for delegating to coding agent.", "githubIssues.createInsertFormat.description": "Controls whether an issue number (ex. #1234) or a full url (ex. https://github.com/owner/name/issues/1234) is inserted when the Create Issue code action is run.", "githubIssues.issueCompletions.enabled.description": "Controls whether completion suggestions are shown for issues.", "githubIssues.userCompletions.enabled.description": "Controls whether completion suggestions are shown for users.", @@ -201,7 +200,6 @@ "view.github.active.pull.request.name": "Review Pull Request", "view.github.active.pull.request.welcome.name": "Active Pull Request", "command.pull.request.category": "GitHub Pull Requests", - "command.githubpr.remoteAgent.title": "Remote agent integration", "command.pr.create.title": "Create Pull Request", "command.pr.pick.title": "Checkout Pull Request", "command.pr.openChanges.title": "Open Changes", @@ -241,7 +239,6 @@ "command.review.createSuggestionFromChange.title": "Convert to Pull Request Suggestion", "command.review.copyPrLink.title": "Copy Pull Request Link", "command.pr.refreshList.title": "Refresh Pull Requests List", - "command.pr.refreshChatSessions.title": "Refresh Chat Sessions", "command.pr.setFileListLayoutAsTree.title": "View as Tree", "command.pr.setFileListLayoutAsFlat.title": "View as List", "command.pr.toggleHideViewedFiles.title": "Toggle Hide Viewed Files", @@ -355,10 +352,6 @@ "command.notifications.configureNotificationsViewlet.title": "Configure...", "command.notification.chatSummarizeNotification.title": "Summarize With Copilot", "command.pr.checkoutChatSessionPullRequest.title": "Checkout Pull Request", - "command.pr.closeChatSessionPullRequest.title": "Close Pull Request", - "command.pr.preferredCodingAgentGitHubRemote.title": "Set Preferred GitHub Remote", - "command.pr.resetCodingAgentPreferences.title": "Reset Coding Agent Workspace Preferences", - "command.pr.cancelCodingAgent.title": "Cancel Coding Agent", "welcome.github.login.contents": { "message": "You have not yet signed in with GitHub\n[Sign in](command:pr.signin)", "comment": [ @@ -421,21 +414,6 @@ "welcome.issues.github.noFolder.contents": "You have not yet opened a folder.", "welcome.issues.github.noRepo.contents": "No git repositories found", "welcome.github.activePullRequest.contents": "Loading...", - "welcome.chat.sessions.copilot-swe-agent.login.contents": { - "message": "Sign in to get started with Copilot coding agent\n[Sign in](command:pr.signin)", - "comment": [ - "Do not translate what's inside of (...). It is link syntax.", - "{Locked='](command:pr.signin)'}" - ] - }, - "welcome.chat.sessions.copilot-swe-agent.startSession.contents": { - "message": "No Copilot coding agent sessions\n[Start a coding session](command:workbench.action.chat.openNewSessionEditor.copilot-swe-agent)", - "comment": [ - "Do not translate what's inside of (...). It is link syntax.", - "{Locked='](command:workbench.action.chat.open?%7B%22query%22%3A%22%23copilotCodingAgent%20%22%2C%22isPartialQuery%22%3Atrue%7D)'}" - ] - }, - "welcome.chat.sessions.copilot-swe-agent.noGitHub.contents": "Clone or open a GitHub repository to get started", "languageModelTools.github-pull-request_issue_fetch.displayName": "Get a GitHub Issue or pull request", "languageModelTools.github-pull-request_issue_summarize.displayName": "Summarize a GitHub Issue or pull request", "languageModelTools.github-pull-request_notification_fetch.displayName": "Get a GitHub Notification", diff --git a/src/commands.ts b/src/commands.ts index 950b1221b1..0c16fcc689 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -18,8 +18,7 @@ import { SessionLinkInfo } from './common/timelineEvent'; import { asTempStorageURI, fromPRUri, fromReviewUri, Schemes, toPRUri } from './common/uri'; import { formatError } from './common/utils'; import { EXTENSION_ID } from './constants'; -import { ICopilotRemoteAgentCommandArgs } from './github/common'; -import { ChatSessionWithPR, CrossChatSessionWithPR } from './github/copilotApi'; +import { CrossChatSessionWithPR } from './github/copilotApi'; import { CopilotRemoteAgentManager, SessionIdForPr } from './github/copilotRemoteAgent'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; import { GitHubRepository } from './github/githubRepository'; @@ -119,11 +118,6 @@ export async function closeAllPrAndReviewEditors() { } } -function isChatSessionWithPR(value: any): value is ChatSessionWithPR { - const asChatSessionWithPR = value as Partial; - return !!asChatSessionWithPR.pullRequest; -} - function isCrossChatSessionWithPR(value: any): value is CrossChatSessionWithPR { const asCrossChatSessionWithPR = value as Partial; return !!asCrossChatSessionWithPR.pullRequestDetails; @@ -646,7 +640,7 @@ export function registerCommands( })); context.subscriptions.push( - vscode.commands.registerCommand('pr.openChanges', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | OverviewContext | ChatSessionWithPR | { path: string } | undefined) => { + vscode.commands.registerCommand('pr.openChanges', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | OverviewContext | CrossChatSessionWithPR | { path: string } | undefined) => { if (pr === undefined) { // This is unexpected, but has happened a few times. Logger.error('Unexpectedly received undefined when picking a PR.', logId); @@ -659,8 +653,6 @@ export function registerCommands( pullRequestModel = pr.pullRequestModel; } else if (pr instanceof PullRequestModel) { pullRequestModel = pr; - } else if (isChatSessionWithPR(pr)) { - pullRequestModel = pr.pullRequest; } else if (isCrossChatSessionWithPR(pr)) { const resolved = await resolvePr({ owner: pr.pullRequestDetails.repository.owner.login, @@ -903,7 +895,7 @@ export function registerCommands( }), ); - async function openDescriptionCommand(argument: RepositoryChangesNode | PRNode | IssueModel | ChatSessionWithPR | undefined) { + async function openDescriptionCommand(argument: RepositoryChangesNode | PRNode | IssueModel | CrossChatSessionWithPR | undefined) { let issueModel: IssueModel | undefined; if (!argument) { const activePullRequests: PullRequestModel[] = reposManager.folderManagers @@ -920,8 +912,13 @@ export function registerCommands( issueModel = argument.pullRequestModel; } else if (argument instanceof PRNode) { issueModel = argument.pullRequestModel; - } else if (isChatSessionWithPR(argument)) { - issueModel = argument.pullRequest; + } else if (isCrossChatSessionWithPR(argument)) { + issueModel = (await resolvePr({ + owner: argument.pullRequestDetails.repository.owner.login, + repo: argument.pullRequestDetails.repository.name, + number: argument.pullRequestDetails.number, + preventDefaultContextMenuItems: true, + }))?.pr; } else { issueModel = argument; } @@ -951,8 +948,8 @@ export function registerCommands( await openDescription(telemetry, issueModel, descriptionNode, folderManager, revealDescription, !(argument instanceof RepositoryChangesNode)); } - async function checkoutChatSessionPullRequest(argument: ChatSessionWithPR | CrossChatSessionWithPR) { - const pr = isChatSessionWithPR(argument) ? argument.pullRequest : await resolvePr({ + async function checkoutChatSessionPullRequest(argument: CrossChatSessionWithPR) { + const pr = await resolvePr({ owner: argument.pullRequestDetails.repository.owner.login, repo: argument.pullRequestDetails.repository.name, number: argument.pullRequestDetails.number, @@ -973,37 +970,6 @@ export function registerCommands( return reviewsManager.switchToPr(folderManager, pr, folderManager.repository, false); } - async function closeChatSessionPullRequest(argument: ChatSessionWithPR | CrossChatSessionWithPR) { - const pr = isChatSessionWithPR(argument) ? argument.pullRequest : await resolvePr({ - owner: argument.pullRequestDetails.repository.owner.login, - repo: argument.pullRequestDetails.repository.name, - number: argument.pullRequestDetails.number, - preventDefaultContextMenuItems: true, - }).then(resolved => resolved?.pr); - if (!pr) { - Logger.warn(`No pull request found in chat session`, logId); - return; - } - await pr.close(); - copilotRemoteAgentManager.refreshChatSessions(); - } - - async function cancelCodingAgent(argument: ChatSessionWithPR | CrossChatSessionWithPR) { - const pr = isChatSessionWithPR(argument) ? argument.pullRequest : await resolvePr({ - owner: argument.pullRequestDetails.repository.owner.login, - repo: argument.pullRequestDetails.repository.name, - number: argument.pullRequestDetails.number, - preventDefaultContextMenuItems: true, - }).then(resolved => resolved?.pr); - if (!pr) { - Logger.warn(`No pull request found in chat session`, logId); - return; - } - - copilotRemoteAgentManager.cancelMostRecentChatSession(pr); - // TODO: show a progress icon until the cancelation is finished - } - context.subscriptions.push( vscode.commands.registerCommand( 'pr.checkoutChatSessionPullRequest', @@ -1011,20 +977,6 @@ export function registerCommands( ) ); - context.subscriptions.push( - vscode.commands.registerCommand( - 'pr.closeChatSessionPullRequest', - closeChatSessionPullRequest - ) - ); - - context.subscriptions.push( - vscode.commands.registerCommand( - 'pr.cancelCodingAgent', - cancelCodingAgent - ) - ); - context.subscriptions.push( vscode.commands.registerCommand( 'pr.openDescription', @@ -1799,9 +1751,6 @@ ${contents} handler.applySuggestion(comment); } })); - context.subscriptions.push( - vscode.commands.registerCommand('githubpr.remoteAgent', async (args: ICopilotRemoteAgentCommandArgs) => await copilotRemoteAgentManager.commandImpl(args)) - ); context.subscriptions.push( vscode.commands.registerCommand('pr.applySuggestionWithCopilot', async (comment: GHPRComment) => { /* __GDPR__ @@ -1998,22 +1947,4 @@ ${contents} } }) ); - - context.subscriptions.push( - vscode.commands.registerCommand('pr.refreshChatSessions', async () => { - copilotRemoteAgentManager.refreshChatSessions(); - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('pr.preferredCodingAgentGitHubRemote', async () => { - await copilotRemoteAgentManager.promptAndUpdatePreferredGitHubRemote(); - }) - ); - - context.subscriptions.push( - vscode.commands.registerCommand('pr.resetCodingAgentPreferences', async () => { - await copilotRemoteAgentManager.resetCodingAgentPreferences(); - }) - ); } diff --git a/src/common/settingKeys.ts b/src/common/settingKeys.ts index 2cc2b5021c..ddc7a88ba7 100644 --- a/src/common/settingKeys.ts +++ b/src/common/settingKeys.ts @@ -101,4 +101,3 @@ export const CODING_AGENT = `${PR_SETTINGS_NAMESPACE}.codingAgent`; export const CODING_AGENT_ENABLED = 'enabled'; export const CODING_AGENT_AUTO_COMMIT_AND_PUSH = 'autoCommitAndPush'; export const CODING_AGENT_PROMPT_FOR_CONFIRMATION = 'promptForConfirmation'; -export const SHOW_CODE_LENS = 'codeLens'; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 495cb74de5..f99a2b30ff 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,6 @@ import { LiveShare } from 'vsls/vscode.js'; import { PostCommitCommandsProvider, Repository } from './api/api'; import { GitApiImpl } from './api/api1'; import { registerCommands } from './commands'; -import { COPILOT_SWE_AGENT } from './common/copilot'; import { commands, contexts } from './common/executeCommands'; import { isSubmodule } from './common/gitUtils'; import Logger from './common/logger'; @@ -289,7 +288,7 @@ async function init( // Resume any pending checkout request stored before workspace reopened. await resumePendingCheckout(reviewsManager, context, reposManager); - initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel); + initChat(context, credentialStore, reposManager); context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git))); // Make sure any compare changes tabs, which come from the create flow, are closed. @@ -300,11 +299,11 @@ async function init( telemetry.sendTelemetryEvent('startup'); } -function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry, prsTreeModel: PrsTreeModel) { +function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager) { const createParticipant = () => { const chatParticipantState = new ChatParticipantState(); context.subscriptions.push(new ChatParticipant(context, chatParticipantState)); - registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry, prsTreeModel); + registerTools(context, credentialStore, reposManager, chatParticipantState); }; const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(EXPERIMENTAL_CHAT, false); @@ -461,40 +460,10 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll Logger.debug('Creating tree view.', 'Activation'); - const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, apiImpl, prsTreeModel); + const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, prsTreeModel); context.subscriptions.push(copilotRemoteAgentManager); - if (vscode.chat?.registerChatSessionItemProvider) { - const chatParticipant = vscode.chat.createChatParticipant(COPILOT_SWE_AGENT, async (request, context, stream, token) => - await copilotRemoteAgentManager.chatParticipantImpl(request, context, stream, token) - ); - context.subscriptions.push(chatParticipant); - - const provider = new class implements vscode.ChatSessionContentProvider, vscode.ChatSessionItemProvider { - label = vscode.l10n.t('GitHub Copilot Coding Agent'); - async provideChatSessionItems(token: vscode.CancellationToken) { - return await copilotRemoteAgentManager.provideChatSessions(token); - } - async provideChatSessionContent(resource: vscode.Uri, token: vscode.CancellationToken) { - return await copilotRemoteAgentManager.provideChatSessionContent(resource, token); - } - onDidChangeChatSessionItems = copilotRemoteAgentManager.onDidChangeChatSessions; - onDidCommitChatSessionItem = copilotRemoteAgentManager.onDidCommitChatSession; - }(); - - context.subscriptions.push(vscode.chat?.registerChatSessionItemProvider( - COPILOT_SWE_AGENT, - provider - )); - - context.subscriptions.push(vscode.chat?.registerChatSessionContentProvider( - COPILOT_SWE_AGENT, - provider, - chatParticipant, - { supportsInterruptions: true } - )); - } - const prTree = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotRemoteAgentManager); + const prTree = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager); context.subscriptions.push(prTree); context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refreshAll(true))); Logger.appendLine('Looking for git repository', ACTIVATION); diff --git a/src/github/common.ts b/src/github/common.ts index 6d9b424d56..23a253493c 100644 --- a/src/github/common.ts +++ b/src/github/common.ts @@ -5,14 +5,10 @@ import * as OctokitRest from '@octokit/rest'; import { Endpoints } from '@octokit/types'; import { DocumentNode } from 'graphql'; -import { ChatSessionStatus, Uri } from 'vscode'; -import { SessionInfo, SessionSetupStep } from './copilotApi'; import { FolderRepositoryManager } from './folderRepositoryManager'; import { GitHubRepository } from './githubRepository'; import { Repository } from '../api/api'; -import { CopilotPRStatus } from '../common/copilot'; import { GitHubRemote } from '../common/remote'; -import { EventType, TimelineEvent } from '../common/timelineEvent'; export namespace OctokitCommon { export type IssuesAssignParams = OctokitRest.RestEndpointMethodTypes['issues']['addAssignees']['parameters']; @@ -100,56 +96,6 @@ export function mergeQuerySchemaWithShared(sharedSchema: DocumentNode, schema: D }; } -type RemoteAgentSuccessResult = { link: string; state: 'success'; number: number; webviewUri: Uri; llmDetails: string; sessionId: string }; -type RemoteAgentErrorResult = { error: string; innerError?: string; state: 'error' }; -export type RemoteAgentResult = RemoteAgentSuccessResult | RemoteAgentErrorResult; - -export interface IAPISessionLogs { - readonly info: SessionInfo; - readonly logs: string; - readonly setupSteps: SessionSetupStep[] | undefined; -} - -export interface ICopilotRemoteAgentCommandArgs { - userPrompt: string; - summary?: string; - source?: 'prompt' | (string & {}); - followup?: string; - _version?: number; // TODO(jospicer): Remove once stabilized/engine version enforced -} - -export interface ICopilotRemoteAgentCommandResponse { - uri: string; - title: string; - description: string; - author: string; - linkTag: string; -} - -export interface ToolCall { - function: { - arguments: string; - name: 'bash' | 'reply_to_comment' | (string & {}); - }; - id: string; - type: string; - index: number; -} - -export interface AssistantDelta { - content?: string; - role: 'assistant' | (string & {}); - tool_calls?: ToolCall[]; -} - -export interface Choice { - finish_reason?: 'tool_calls' | (string & {}); - delta: { - content?: string; - role: 'assistant' | (string & {}); - tool_calls?: ToolCall[]; - }; -} export interface RepoInfo { owner: string; @@ -160,33 +106,3 @@ export interface RepoInfo { ghRepository: GitHubRepository; fm: FolderRepositoryManager; } - -export function copilotEventToSessionStatus(event: TimelineEvent | undefined): ChatSessionStatus { - if (!event) { - return ChatSessionStatus.InProgress; - } - - switch (event.event) { - case EventType.CopilotStarted: - return ChatSessionStatus.InProgress; - case EventType.CopilotFinished: - return ChatSessionStatus.Completed; - case EventType.CopilotFinishedError: - return ChatSessionStatus.Failed; - default: - return ChatSessionStatus.InProgress; - } -} - -export function copilotPRStatusToSessionStatus(event: CopilotPRStatus): ChatSessionStatus { - switch (event) { - case CopilotPRStatus.Started: - return ChatSessionStatus.InProgress; - case CopilotPRStatus.Completed: - return ChatSessionStatus.Completed; - case CopilotPRStatus.Failed: - return ChatSessionStatus.Failed; - default: - return ChatSessionStatus.InProgress; - } -} diff --git a/src/github/copilotApi.ts b/src/github/copilotApi.ts index 499ba2a253..7b172e3cae 100644 --- a/src/github/copilotApi.ts +++ b/src/github/copilotApi.ts @@ -4,55 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import fetch from 'cross-fetch'; -import JSZip from 'jszip'; import * as vscode from 'vscode'; -import { CredentialStore, GitHub } from './credentials'; -import { PRType } from './interface'; +import { CredentialStore } from './credentials'; import { LoggingOctokit } from './loggingOctokit'; -import { PullRequestModel } from './pullRequestModel'; -import { RepositoriesManager } from './repositoriesManager'; import { hasEnterpriseUri } from './utils'; import { AuthProvider } from '../common/authentication'; -import { COPILOT_SWE_AGENT } from '../common/copilot'; import Logger from '../common/logger'; import { ITelemetry } from '../common/telemetry'; -const LEARN_MORE_URL = 'https://aka.ms/coding-agent-docs'; -const PREMIUM_REQUESTS_URL = 'https://docs.github.com/en/copilot/concepts/copilot-billing/understanding-and-managing-requests-in-copilot#what-are-premium-requests'; -// https://github.com/github/sweagentd/blob/59e7d9210ca3ebba029918387e525eea73cb1f4a/internal/problemstatement/problemstatement.go#L36-L53 -export const MAX_PROBLEM_STATEMENT_LENGTH = 30_000 - 50; // 50 character buffer -// https://github.com/github/sweagentd/blob/0ad8f81a9c64754cb8a83d10777de4638bba1a6e/docs/adr/0001-create-job-api.md#post-jobsownerrepo---create-job-task -const JOBS_API_VERSION = 'v1'; - -export interface RemoteAgentJobPayload { - problem_statement: string; - event_type: string; - pull_request?: { - title?: string; - body_placeholder?: string; - body_suffix?: string; - base_ref?: string; - head_ref?: string; - }; - run_name?: string; -} - -export interface RemoteAgentJobResponse { - job_id: string; - session_id: string; - actor: { - id: number; - login: string; - }; - created_at: string; - updated_at: string; -} - -export interface ChatSessionWithPR extends vscode.ChatSessionItem { - pullRequest: PullRequestModel; -} - - /** * This is temporary for the migration of CCA only. * Once fully migrated we can rename to ChatSessionWithPR and remove the old one. @@ -76,7 +35,6 @@ export class CopilotApi { constructor( private octokit: LoggingOctokit, private token: string, - private credentialStore: CredentialStore, private telemetry: ITelemetry ) { } @@ -92,152 +50,6 @@ export class CopilotApi { return this.makeApiCallFullUrl(`${this.baseUrl}${api}`, init); } - private get userAgent(): string { - const extensionVersion = vscode.extensions.getExtension('GitHub.vscode-pull-request-github')?.packageJSON.version ?? 'unknown'; - return `vscode-pull-request-github/${extensionVersion}`; - } - - async postRemoteAgentJob( - owner: string, - name: string, - payload: RemoteAgentJobPayload, - isTruncated: boolean, - ): Promise { - const repoSlug = `${owner}/${name}`; - const apiUrl = `/agents/swe/${JOBS_API_VERSION}/jobs/${repoSlug}`; - let status: number | undefined; - - const problemStatementLength = payload.problem_statement.length.toString(); - const payloadJson = JSON.stringify(payload); - const payloadLength = payloadJson.length.toString(); - Logger.trace(`postRemoteAgentJob: Posting job to ${apiUrl} with payload: ${JSON.stringify(payload)}`, CopilotApi.ID); - try { - const response = await this.makeApiCall(apiUrl, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': this.userAgent, - }, - body: payloadJson - }); - - status = response.status; - if (!response.ok) { - throw new Error(await this.formatRemoteAgentJobError(status, repoSlug, response)); - } - - const data = await response.json(); - this.validateRemoteAgentJobResponse(data); - /* - __GDPR__ - "remoteAgent.postRemoteAgentJob" : { - "status" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "payloadLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "problemStatementLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isTruncated": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryEvent('remoteAgent.postRemoteAgentJob', { - status: status.toString(), - payloadLength, - problemStatementLength, - isTruncated: isTruncated.toString(), - }); - return data; - } catch (error) { - /* __GDPR__ - "remoteAgent.postRemoteAgentJob" : { - "status" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "payloadLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "problemStatementLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isTruncated": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryErrorEvent('remoteAgent.postRemoteAgentJob', { - status: status?.toString() || '999', - payloadLength, - problemStatementLength, - isTruncated: isTruncated.toString(), - }); - throw error; - } - } - - // https://github.com/github/sweagentd/blob/371ea6db280b9aecf790ccc20660e39a7ecb8d1c/internal/api/jobapi/handler.go#L110-L120 - private async formatRemoteAgentJobError(status: number, repoSlug: string, response: Response): Promise { - Logger.error(`Error in remote agent job: ${await response.text()}`, CopilotApi.ID); - switch (status) { - case 400: - return vscode.l10n.t('Bad request'); - case 401: - return vscode.l10n.t('Unauthorized'); - case 402: - return vscode.l10n.t('[Premium request]({0}) quota exceeded', PREMIUM_REQUESTS_URL); - case 403: - return vscode.l10n.t('[GitHub coding agent]({0}) is not enabled for repository \'{1}\'', LEARN_MORE_URL, repoSlug); - case 404: - return vscode.l10n.t('Repository \'{0}\' not found', repoSlug); - case 409: - return vscode.l10n.t('A coding agent pull request already exists'); - case 500: - return vscode.l10n.t('Server error. Please see logs for details.'); - default: - return vscode.l10n.t('Error: {0}. Please see logs for details', status); - } - } - - private validateRemoteAgentJobResponse(data: any): asserts data is RemoteAgentJobResponse { - if (!data || typeof data !== 'object') { - throw new Error('Invalid response from coding agent'); - } - if (typeof data.job_id !== 'string') { - throw new Error('Invalid job_id in response'); - } - if (typeof data.session_id !== 'string') { - throw new Error('Invalid session_id in response'); - } - if (!data.actor || typeof data.actor !== 'object') { - throw new Error('Invalid actor in response'); - } - if (typeof data.actor.id !== 'number') { - throw new Error('Invalid actor.id in response'); - } - if (typeof data.actor.login !== 'string') { - throw new Error('Invalid actor.login in response'); - } - if (typeof data.created_at !== 'string') { - throw new Error('Invalid created_at in response'); - } - if (typeof data.updated_at !== 'string') { - throw new Error('Invalid updated_at in response'); - } - } - - public async getLogsFromZipUrl(logsUrl: string): Promise { - const logsZip = await this.makeApiCallFullUrl(logsUrl, { - headers: { - Authorization: `Bearer ${this.token}`, - Accept: 'application/json', - }, - }); - if (!logsZip.ok) { - throw new Error(`Failed to fetch logs zip: ${logsZip.statusText}`); - } - const logsText = await logsZip.arrayBuffer(); - const copilotSteps: string[] = []; - const zip = await JSZip.loadAsync(logsText); - for (const fileName of Object.keys(zip.files)) { - const file = zip.files[fileName]; - if (!file.dir && fileName.endsWith('Processing Request.txt')) { - const content = await file.async('string'); - copilotSteps.push(...content.split('\n')); - } - } - return copilotSteps; - } - public async getAllSessions(pullRequestId: number | undefined): Promise { const response = await this.makeApiCall( pullRequestId @@ -256,111 +68,6 @@ export class CopilotApi { return sessions.sessions; } - public async getAllCodingAgentPRs(repositoriesManager: RepositoriesManager): Promise { - const hub = this.getHub(); - const username = (await hub?.currentUser)?.login; - if (!username) { - Logger.error('Failed to get GitHub username from auth provider', CopilotApi.ID); - return []; - } - const query = `is:open author:${COPILOT_SWE_AGENT}[bot] assignee:${username} is:pr repo:\${owner}/\${repository}`; - const allItems = await Promise.all( - repositoriesManager.folderManagers.map(async fm => { - const result = await fm.getPullRequests(PRType.Query, undefined, query); - return result.items; - }) - ); - return allItems.flat(); - } - - public async getSessionInfo(sessionId: string): Promise { - const response = await this.makeApiCall(`/agents/sessions/${sessionId}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${this.token}`, - 'Accept': 'application/json' - } - }); - if (!response.ok) { - await this.handleApiError(response, 'getSessionInfo'); - } - - return (await response.json()) as SessionInfo; - } - - public async getLogsFromSession(sessionId: string): Promise { - const logsResponse = await this.makeApiCall(`/agents/sessions/${sessionId}/logs`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Content-Type': 'application/json', - }, - }); - if (!logsResponse.ok) { - await this.handleApiError(logsResponse, 'getLogsFromSession'); - } - return await logsResponse.text(); - } - - public async getJobByJobId(owner: string, repo: string, jobId: string): Promise { - try { - const response = await this.makeApiCall(`/agents/swe/v1/jobs/${owner}/${repo}/${jobId}`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': this.userAgent, - } - }); - if (!response.ok) { - Logger.warn(`Failed to fetch job info for job ${jobId}: ${response.statusText}`, CopilotApi.ID); - return; - } - const data = await response.json() as JobInfo; - return data; - } catch (error) { - Logger.warn(`Error fetching job info for job ${jobId}: ${error}`, CopilotApi.ID); - return; - } - } - - public async getJobBySessionId(owner: string, repo: string, sessionId: string): Promise { - try { - const response = await this.makeApiCall(`/agents/swe/${JOBS_API_VERSION}/jobs/${owner}/${repo}/session/${sessionId}`, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${this.token}`, - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': this.userAgent, - } - }); - if (!response.ok) { - Logger.warn(`Failed to fetch job info for session ${sessionId}: ${response.statusText}`, CopilotApi.ID); - return; - } - const data = await response.json() as JobInfo; - return data; - } catch (error) { - Logger.warn(`Error fetching job info for session ${sessionId}: ${error}`, CopilotApi.ID); - return; - } - } - - private getHub(): GitHub | undefined { - let authProvider: AuthProvider | undefined; - if (this.credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { - authProvider = AuthProvider.githubEnterprise; - } else if (this.credentialStore.isAuthenticated(AuthProvider.github)) { - authProvider = AuthProvider.github; - } else { - return; - } - - return this.credentialStore.getHub(authProvider); - } - private async handleApiError(response: Response, action: string): Promise { let errorBody: string | undefined = undefined; try { @@ -408,39 +115,6 @@ export interface SessionInfo { error: string | null; } -export interface SessionSetupStep { - name: string; - status: 'completed' | 'in_progress' | 'queued'; -} - -export interface JobInfo { - job_id: string; - session_id: string; - problem_statement: string; - content_filter_mode?: string; - status: string; - result?: string; - actor: { - id: number; - login: string; - }; - created_at: string; - updated_at: string; - pull_request: { - id: number; - number: number; - }; - workflow_run?: { - id: number; - }; - error?: { - message: string; - }; - event_type?: string; - event_url?: string; - event_identifiers?: string[]; -} - export async function getCopilotApi(credentialStore: CredentialStore, telemetry: ITelemetry, authProvider?: AuthProvider): Promise { if (!authProvider) { if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { @@ -458,5 +132,5 @@ export async function getCopilotApi(credentialStore: CredentialStore, telemetry: } const { token } = await github.octokit.api.auth() as { token: string }; - return new CopilotApi(github.octokit, token, credentialStore, telemetry); + return new CopilotApi(github.octokit, token, telemetry); } \ No newline at end of file diff --git a/src/github/copilotRemoteAgent.ts b/src/github/copilotRemoteAgent.ts index 8b2f53540b..bc2c7ce9e4 100644 --- a/src/github/copilotRemoteAgent.ts +++ b/src/github/copilotRemoteAgent.ts @@ -3,57 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as pathLib from 'path'; -import { URI } from '@vscode/prompt-tsx/dist/base/util/vs/common/uri'; -import * as marked from 'marked'; -import vscode, { ChatPromptReference, ChatSessionItem } from 'vscode'; -import { copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common'; -import { ChatSessionWithPR, CopilotApi, getCopilotApi, JobInfo, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi'; -import { CodingAgentPRAndStatus, CopilotPRWatcher } from './copilotPrWatcher'; -import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from '../../common/sessionParsing'; -import { GitApiImpl } from '../api/api1'; -import { COPILOT_ACCOUNTS } from '../common/comment'; +import * as vscode from 'vscode'; +import { RepoInfo } from './common'; +import { CopilotApi, getCopilotApi } from './copilotApi'; +import { CopilotPRWatcher } from './copilotPrWatcher'; + +import { CredentialStore } from './credentials'; +import { FolderRepositoryManager } from './folderRepositoryManager'; +import { GitHubRepository } from './githubRepository'; +import { RepositoriesManager } from './repositoriesManager'; import { CopilotRemoteAgentConfig } from '../common/config'; -import { COPILOT_CLOUD_AGENT, COPILOT_LOGINS, COPILOT_SWE_AGENT } from '../common/copilot'; -import { commands } from '../common/executeCommands'; +import { COPILOT_CLOUD_AGENT, COPILOT_LOGINS } from '../common/copilot'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; import { GitHubRemote } from '../common/remote'; -import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; -import { toOpenPullRequestWebviewUri } from '../common/uri'; -import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder'; -import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager'; -import { extractTitle, formatBodyPlaceholder, truncatePrompt } from './copilotRemoteAgentUtils'; -import { CredentialStore } from './credentials'; -import { FolderRepositoryManager, ReposManagerState } from './folderRepositoryManager'; -import { GitHubRepository } from './githubRepository'; -import { issueMarkdown, PlainTextRenderer } from './markdownUtils'; -import { PullRequestModel } from './pullRequestModel'; -import { chooseItem } from './quickPicks'; -import { RepositoriesManager } from './repositoriesManager'; -import { getRepositoryForFile } from './utils'; import { PrsTreeModel } from '../view/prsTreeModel'; -const LEARN_MORE = vscode.l10n.t('Learn about coding agent'); -// Without Pending Changes -const CONTINUE = vscode.l10n.t('Continue'); -// With Pending Changes -const PUSH_CHANGES = vscode.l10n.t('Include changes'); -const CONTINUE_WITHOUT_PUSHING = vscode.l10n.t('Ignore changes'); -const CONTINUE_AND_DO_NOT_ASK_AGAIN = vscode.l10n.t('Continue and don\'t ask again'); - -const CONTINUE_TRUNCATION = vscode.l10n.t('Continue with truncation'); -const DELEGATE_MODAL_DETAILS = vscode.l10n.t('The agent will work asynchronously to create a pull request with your requested changes.'); -const DISABLE_CODELENS = vscode.l10n.t('Disable Code Lens'); - -const COPILOT = '@copilot'; - -const body_suffix = vscode.l10n.t('Created from VS Code via the [GitHub Pull Request](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github) extension.'); - const PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY = 'PREFERRED_GITHUB_CODING_AGENT_REMOTE'; - export namespace SessionIdForPr { const prefix = 'pull-session-by-index'; @@ -76,193 +44,8 @@ export namespace SessionIdForPr { } } -type ConfirmationResult = { step: string; accepted: boolean; metadata?: CreatePromptMetadata /* | SomeOtherMetadata */ }; - -interface CreatePromptMetadata { - prompt: string; - history?: string; - references?: ChatPromptReference[]; -} - export class CopilotRemoteAgentManager extends Disposable { - async chatParticipantImpl(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { - const startSession = async (source: string, prompt: string, history?: string, references?: readonly vscode.ChatPromptReference[]) => { - /* __GDPR__ - "copilot.remoteagent.editor.invoke" : { - "promptLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "historyLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "referencesCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryEvent('copilot.remoteagent.editor.invoke', { - promptLength: prompt.length.toString() ?? '0', - historyLength: history?.length.toString() ?? '0', - referencesCount: references?.length.toString() ?? '0', - source, - }); - const result = await this.invokeRemoteAgent( - prompt, - [ - this.extractFileReferences(references), - history - ].join('\n\n').trim(), - token, - false, - stream, - ); - if (result.state !== 'success') { - Logger.error(`Failed to provide new chat session item: ${result.error}${result.innerError ? `\nInner Error: ${result.innerError}` : ''}`, CopilotRemoteAgentManager.ID); - stream.warning(result.error); - return; - } - return result.number; - }; - - const handleConfirmationData = async () => { - const results: ConfirmationResult[] = []; - results.push(...(request.acceptedConfirmationData?.map(data => ({ step: data.step, accepted: true, metadata: data?.metadata })) ?? [])); - results.push(...((request.rejectedConfirmationData ?? []).filter(data => !results.some(r => r.step === data.step)).map(data => ({ step: data.step, accepted: false, metadata: data?.metadata })))); - for (const data of results) { - switch (data.step) { - case 'create': - if (!data.accepted) { - stream.markdown(vscode.l10n.t('Coding agent request cancelled.')); - return {}; - } - const { prompt, history, references } = data.metadata as CreatePromptMetadata; - const number = await startSession('chat', prompt, history, references); - if (!number) { - return {}; - } - const pullRequest = await this.findPullRequestById(number, true); - if (!pullRequest) { - stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', number)); - return {}; - } - const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); - const plaintextBody = marked.parse(pullRequest.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); - const card = new vscode.ChatResponsePullRequestPart(uri, pullRequest.title, plaintextBody, pullRequest.author.specialDisplayName ?? pullRequest.author.login, `#${pullRequest.number}`); - stream.push(card); - stream.markdown(vscode.l10n.t('GitHub Copilot coding agent has begun working on your request. Follow its progress in the associated chat and pull request.')); - vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + number }), { viewColumn: vscode.ViewColumn.Active }); - break; - default: - stream.warning(`Unknown confirmation step: ${data.step}\n\n`); - break; - } - } - return {}; - }; - - if (request.acceptedConfirmationData || request.rejectedConfirmationData) { - return await handleConfirmationData(); - } - - if (context.chatSessionContext?.isUntitled) { - /* Generate new coding agent session from an 'untitled' session */ - const number = await startSession( - 'untitledChatSession', - request.prompt, - undefined, - request.references - ); - if (!number) { - return {}; - } - // Tell UI to the new chat session - const modified: vscode.ChatSessionItem = { - resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + number }), - label: `Pull Request ${number}`, - }; - this._onDidCommitChatSession.fire({ original: context.chatSessionContext.chatSessionItem, modified }); - } else if (context.chatSessionContext) { - /* Follow up to an existing coding agent session */ - try { - if (token.isCancellationRequested) { - return {}; - } - - // Validate user input - const userPrompt = request.prompt; - if (!userPrompt || userPrompt.trim().length === 0) { - stream.markdown(vscode.l10n.t('Please provide a message for the coding agent.')); - return {}; - } - - stream.progress(vscode.l10n.t('Preparing')); - - const pullRequest = await this.findPullRequestById(parseInt(context.chatSessionContext.chatSessionItem.resource.path.slice(1), 10), true); - if (!pullRequest) { - stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', context.chatSessionContext.chatSessionItem.resource.toString())); - return {}; - } - - stream.progress(vscode.l10n.t('Delegating request to coding agent')); - - // Add follow-up comment to the PR - const result = await this.addFollowUpToExistingPR(pullRequest.number, userPrompt); - if (!result) { - stream.markdown(vscode.l10n.t('Failed to add follow-up comment to the pull request.')); - return {}; - } - - // Show initial success message - stream.markdown(result); - stream.markdown('\n\n'); - - stream.progress(vscode.l10n.t('Attaching to session')); - - // Wait for new session and stream its progress - const newSession = await this.waitForNewSession(pullRequest, stream, token, true); - if (!newSession) { - return {}; - } - - // Stream the new session logs - stream.markdown(vscode.l10n.t('Coding agent has begun work on your request')); - stream.markdown('\n\n'); - - await this.streamSessionLogs(stream, pullRequest, newSession.id, token); - - return {}; - } catch (error) { - Logger.error(`Error in request handler: ${error}`, CopilotRemoteAgentManager.ID); - stream.markdown(vscode.l10n.t('An error occurred while processing your request.')); - return { errorDetails: { message: error.message } }; - } - } else { - /* @copilot invoked from a 'normal' chat or 'cloud button' */ - stream.confirmation( - vscode.l10n.t('Delegate to agent'), - DELEGATE_MODAL_DETAILS, - { - step: 'create', - metadata: { - prompt: request.prompt, - history: undefined, - references: request.references, - } - }, - ['Delegate', 'Cancel'] - ); - } - } - public static ID = 'CopilotRemoteAgentManager'; - - private readonly _onDidChangeStates = this._register(new vscode.EventEmitter()); - readonly onDidChangeStates = this._onDidChangeStates.event; - private readonly _onDidChangeNotifications = this._register(new vscode.EventEmitter()); - readonly onDidChangeNotifications = this._onDidChangeNotifications.event; - private readonly _onDidCreatePullRequest = this._register(new vscode.EventEmitter()); - readonly onDidCreatePullRequest = this._onDidCreatePullRequest.event; - private readonly _onDidChangeChatSessions = this._register(new vscode.EventEmitter()); - readonly onDidChangeChatSessions = this._onDidChangeChatSessions.event; - private readonly _onDidCommitChatSession = this._register(new vscode.EventEmitter<{ original: ChatSessionItem; modified: ChatSessionItem }>()); - readonly onDidCommitChatSession = this._onDidCommitChatSession.event; - - private readonly gitOperationsManager: GitOperationsManager; private _isAssignable: boolean | undefined; constructor( @@ -270,47 +53,12 @@ export class CopilotRemoteAgentManager extends Disposable { public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext, - private gitAPI: GitApiImpl, private readonly prsTreeModel: PrsTreeModel, ) { super(); - this.gitOperationsManager = new GitOperationsManager(CopilotRemoteAgentManager.ID); - this._register(this.credentialStore.onDidChangeSessions((e: vscode.AuthenticationSessionsChangeEvent) => { - if (e.provider.id === 'github') { - this._copilotApiPromise = undefined; // Invalidate cached session - } - })); this._register(new CopilotPRWatcher(this.repositoriesManager, this.prsTreeModel)); - this._register(this.prsTreeModel.onDidChangeCopilotStates(() => { - this._onDidChangeStates.fire(); - this._onDidChangeChatSessions.fire(); - })); - this._register(this.prsTreeModel.onDidChangeCopilotNotifications(items => this._onDidChangeNotifications.fire(items))); - - this._register(this.repositoriesManager.onDidChangeFolderRepositories((event) => { - if (event.added) { - this._register(event.added.onDidChangeAssignableUsers(() => { - this.updateAssignabilityContext(); - })); - } - this.updateAssignabilityContext(); - })); - this.repositoriesManager.folderManagers.forEach(manager => { - this._register(manager.onDidChangeAssignableUsers(() => { - this.updateAssignabilityContext(); - })); - }); - this._register(vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration(CODING_AGENT)) { - this.updateAssignabilityContext(); - } - })); - - // Set initial context - this.updateAssignabilityContext(); } - private _copilotApiPromise: Promise | undefined; private get copilotApi(): Promise { if (!this._copilotApiPromise) { @@ -323,34 +71,6 @@ export class CopilotRemoteAgentManager extends Disposable { return await getCopilotApi(this.credentialStore, this.telemetry); } - public get enabled(): boolean { - return CopilotRemoteAgentConfig.getEnabled(); - } - - public get autoCommitAndPushEnabled(): boolean { - return CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled(); - } - - private _repoManagerInitializationPromise: Promise | undefined; - private async waitRepoManagerInitialization() { - if (this.repositoriesManager.state === ReposManagerState.RepositoriesLoaded || this.repositoriesManager.state === ReposManagerState.NeedsAuthentication) { - return; - } - - if (!this._repoManagerInitializationPromise) { - this._repoManagerInitializationPromise = new Promise((resolve) => { - const disposable = this.repositoriesManager.onDidChangeState(() => { - if (this.repositoriesManager.state === ReposManagerState.RepositoriesLoaded || this.repositoriesManager.state === ReposManagerState.NeedsAuthentication) { - disposable.dispose(); - resolve(); - } - }); - }); - } - - return this._repoManagerInitializationPromise; - } - async isAssignable(): Promise { const setCachedResult = (b: boolean) => { this._isAssignable = b; @@ -407,17 +127,6 @@ export class CopilotRemoteAgentManager extends Disposable { return await this.isAssignable(); } - private async updateAssignabilityContext(): Promise { - try { - this._isAssignable = undefined; // Invalidate cache - const available = await this.isAvailable(); - commands.setContext('copilotCodingAgentAssignable', available); - } catch (error) { - // Presume false - commands.setContext('copilotCodingAgentAssignable', false); - } - } - private firstFolderManager(): FolderRepositoryManager | undefined { if (!this.repositoriesManager.folderManagers.length) { return; @@ -425,60 +134,6 @@ export class CopilotRemoteAgentManager extends Disposable { return this.repositoriesManager.folderManagers[0]; } - private chooseFolderManager(): Promise { - return chooseItem( - this.repositoriesManager.folderManagers, - itemValue => ({ label: pathLib.basename(itemValue.repository.rootUri.fsPath) }), - ); - } - - public async resetCodingAgentPreferences() { - await this.context.workspaceState.update(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY, undefined); - } - - public async promptAndUpdatePreferredGitHubRemote(skipIfValueAlreadyCached = false): Promise { - if (skipIfValueAlreadyCached) { - const cachedValue = await this.context.workspaceState.get(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY); - if (cachedValue) { - return; - } - } - - const fm = this.firstFolderManager(); - if (!fm) { - return; - } - - const ghRemotes = await fm.getAllGitHubRemotes(); - Logger.trace(`There are ${ghRemotes.length} GitHub remotes available to select from`, CopilotRemoteAgentManager.ID); - - if (!ghRemotes || ghRemotes.length <= 1) { - // Unexpected if we reach here, command should be hidden. - Logger.trace('No need to select a coding agent GitHub remote, skipping prompt', CopilotRemoteAgentManager.ID); - return; - } - - // Sort so that origin is first if it exists - ghRemotes.sort((a, b) => (a.remoteName === 'origin' ? -1 : b.remoteName === 'origin' ? 1 : 0)); - - const result = await chooseItem( - ghRemotes, - itemValue => ({ label: itemValue.remoteName, description: `(${itemValue.owner}/${itemValue.repositoryName})` }), - { - title: vscode.l10n.t('Coding agent will create pull requests against the selected remote.'), - } - ); - - if (!result) { - Logger.warn('No coding agent GitHub remote selected.', CopilotRemoteAgentManager.ID); - Logger.warn(`Keeping previous value of: ${this.context.workspaceState.get(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY)}`, CopilotRemoteAgentManager.ID); - return; - } - - Logger.appendLine(`Updated '${result.remoteName}' as preferred coding agent remote`, CopilotRemoteAgentManager.ID); - await this.context.workspaceState.update(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY, result.remoteName); - } - async repoInfo(fm?: FolderRepositoryManager): Promise { fm = fm || this.firstFolderManager(); const repository = fm?.repository; @@ -513,1173 +168,4 @@ export class CopilotRemoteAgentManager extends Disposable { return { owner, repo, baseRef, remote, repository, ghRepository, fm }; } - async addFollowUpToExistingPR(pullRequestNumber: number, userPrompt: string, summary?: string): Promise { - try { - const pr = await this.findPullRequestById(pullRequestNumber, true); - if (!pr) { - Logger.error(`Could not find pull request #${pullRequestNumber}`, CopilotRemoteAgentManager.ID); - return; - } - // Add a comment tagging @copilot with the user's prompt - const commentBody = `${COPILOT} ${userPrompt} \n\n --- \n\n ${summary ?? ''}`; - const commentResult = await pr.createIssueComment(commentBody); - if (!commentResult) { - Logger.error(`Failed to add comment to PR #${pullRequestNumber}`, CopilotRemoteAgentManager.ID); - return; - } - Logger.appendLine(`Added comment ${commentResult.htmlUrl}`, CopilotRemoteAgentManager.ID); - // allow-any-unicode-next-line - return vscode.l10n.t('🚀 Follow-up comment added to [#{0}]({1})', pullRequestNumber, commentResult.htmlUrl); - } catch (err) { - Logger.error(`Failed to add follow-up comment to PR #${pullRequestNumber}: ${err}`, CopilotRemoteAgentManager.ID); - return; - } - } - - async tryPromptForAuthAndRepo(): Promise { - const authResult = await this.credentialStore.tryPromptForCopilotAuth(); - if (!authResult) { - return undefined; - } - // Wait for repos to update - const fm = await this.chooseFolderManager(); - await fm?.updateRepositories(); - return fm; - } - - async commandImpl(args?: ICopilotRemoteAgentCommandArgs): Promise { - if (!args) { - return; - } - const { userPrompt, summary, source, followup, _version } = args; - const fm = await this.tryPromptForAuthAndRepo(); - if (!fm) { - return; - } - - /* __GDPR__ - "remoteAgent.command.args" : { - "source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "isFollowup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "userPromptLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "summaryLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryEvent('remoteAgent.command.args', { - source: source?.toString() || 'unknown', - isFollowup: !!followup ? 'true' : 'false', - userPromptLength: userPrompt.length.toString(), - summaryLength: summary ? summary.length.toString() : '0', - version: _version?.toString() || 'unknown' - }); - - if (!userPrompt || userPrompt.trim().length === 0) { - return; - } - - const repoInfo = await this.repoInfo(fm); - if (!repoInfo) { - /* __GDPR__ - "remoteAgent.command.result" : { - "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'noRepositoryInfo' }); - return; - } - const { repository, owner, repo } = repoInfo; - - const repoName = `${owner}/${repo}`; - const hasChanges = repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0; - const learnMoreCb = async () => { - /* __GDPR__ - "remoteAgent.command.result" : { - "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'learnMore' }); - vscode.env.openExternal(vscode.Uri.parse('https://aka.ms/coding-agent-docs')); - }; - - let autoPushAndCommit = false; - const message = vscode.l10n.t('Copilot coding agent will continue your work in \'{0}\'.', repoName); - const detail = DELEGATE_MODAL_DETAILS; - if (source !== 'prompt' && hasChanges && CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled()) { - - const buttons = [PUSH_CHANGES, CONTINUE_WITHOUT_PUSHING, LEARN_MORE]; - if (source === 'todo') { - buttons.push(DISABLE_CODELENS); - } - - const modalResult = await vscode.window.showInformationMessage( - message, - { - modal: true, - detail, - }, - ...buttons, - ); - - if (!modalResult) { - /* __GDPR__ - "remoteAgent.command.result" : { - "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'cancel' }); - return; - } - - if (modalResult === LEARN_MORE) { - learnMoreCb(); - return; - } - - if (modalResult === DISABLE_CODELENS) { - vscode.commands.executeCommand('workbench.action.openSettings', 'githubPullRequests.codingAgent.codeLens'); - return; - } - - if (modalResult === PUSH_CHANGES) { - autoPushAndCommit = true; - } - } else if (CopilotRemoteAgentConfig.getPromptForConfirmation()) { - // No pending changes modal - const modalResult = await vscode.window.showInformationMessage( - source !== 'prompt' ? message : vscode.l10n.t('Copilot coding agent will implement the specification outlined in this prompt file'), - { - modal: true, - detail: source !== 'prompt' ? detail : undefined - }, - CONTINUE, - CONTINUE_AND_DO_NOT_ASK_AGAIN, - LEARN_MORE, - ); - if (!modalResult) { - return; - } - - if (modalResult === CONTINUE_AND_DO_NOT_ASK_AGAIN) { - await CopilotRemoteAgentConfig.disablePromptForConfirmation(); - } - - if (modalResult === LEARN_MORE) { - learnMoreCb(); - return; - } - } - - const result = await this.invokeRemoteAgent( - userPrompt, - summary, - undefined, - autoPushAndCommit, - undefined, - fm - ); - - if (result.state !== 'success') { - /* __GDPR__ - "remoteAgent.command.result" : { - "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'invocationFailure' }); - vscode.window.showErrorMessage(result.error); - return; - } - - const { webviewUri, link, number } = result; - - /* __GDPR__ - "remoteAgent.command.success" : { - "source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "hasFollowup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryEvent('remoteAgent.command.success', { - source: source || 'unknown', - hasFollowup: (!!followup).toString(), - outcome: 'success' - }); - - const viewLocationSetting = vscode.workspace.getConfiguration('chat').get('agentSessionsViewLocation'); - const pr = await (async () => { - const capi = await this.copilotApi; - if (!capi) { - return; - } - const sessions = await capi.getAllCodingAgentPRs(this.repositoriesManager); - return sessions.find(session => session.number === number); - })(); - - if (!viewLocationSetting || viewLocationSetting === 'disabled') { - vscode.commands.executeCommand('vscode.open', webviewUri); - } else { - await this.provideChatSessions(new vscode.CancellationTokenSource().token); - if (pr) { - vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pr.number })); - } - } - - if (pr && (_version && _version === 2)) { /* version 2 means caller knows how to render this */ - const plaintextBody = marked.parse(pr.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); - - return { - uri: webviewUri.toString(), - title: pr.title, - description: plaintextBody, - author: COPILOT_ACCOUNTS[pr.author.login].name, - linkTag: `#${pr.number}` - }; - } - - // allow-any-unicode-next-line - return vscode.l10n.t('🚀 Coding agent will continue work in [#{0}]({1}). Track progress [here]({2}).', number, link, webviewUri.toString()); - } - - async invokeRemoteAgent(prompt: string, problemContext?: string, token?: vscode.CancellationToken, autoPushAndCommit = true, chatStream?: vscode.ChatResponseStream, fm?: FolderRepositoryManager): Promise { - const capiClient = await this.copilotApi; - if (!capiClient) { - return { error: vscode.l10n.t('Failed to initialize Copilot API. Please try again later.'), state: 'error' }; - } - - await this.promptAndUpdatePreferredGitHubRemote(true); - - const repoInfo = await this.repoInfo(fm); - if (!repoInfo) { - return { error: vscode.l10n.t('No repository information found. Please open a workspace with a GitHub repository.'), state: 'error' }; - } - const { owner, repo, remote, repository, ghRepository, baseRef } = repoInfo; - - // Check if user has permission to access the repository - try { - await ghRepository.octokit.api.repos.get({ owner, repo }); - } catch (error) { - if (error.status === 404 || error.status === 403) { - const currentUser = await this.credentialStore.getCurrentUser(remote.authProviderId); - return { - error: vscode.l10n.t( - 'Unable to access {0} as user {1}. Please check your permissions and try again.', - `\`${owner}/${repo}\``, - `\`${currentUser.login}\``, - ), - state: 'error', - }; - } - - // Re-throw other errors to be handled by the outer catch block - throw error; - } - - // Check if user has permission to assign Copilot in repository - if (!(await this.isAssignable())) { - return { - error: vscode.l10n.t( - 'Unable to assign GitHub Copilot coding agent in {0}. Please check your permissions and try again.', - `\`${owner}/${repo}\`` - ), - state: 'error', - }; - } - - // NOTE: This is as unobtrusive as possible with the current high-level APIs. - // We only create a new branch and commit if there are staged or working changes. - // This could be improved if we add lower-level APIs to our git extension (e.g. in-memory temp git index). - - const base_ref = baseRef; // This is the ref the PR will merge into - let head_ref: string | undefined; // This is the ref coding agent starts work from (omitted unless we push local changes) - const hasChanges = autoPushAndCommit && (repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0); - if (hasChanges) { - if (!CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled()) { - return { error: vscode.l10n.t('Uncommitted changes detected. Please commit or stash your changes before starting the remote agent. Enable \'{0}\' to push your changes automatically.', CODING_AGENT_AUTO_COMMIT_AND_PUSH), state: 'error' }; - } - try { - chatStream?.progress(vscode.l10n.t('Waiting for local changes')); - head_ref = await this.gitOperationsManager.commitAndPushChanges(repoInfo); - } catch (error) { - return { error: vscode.l10n.t('Failed to commit and push changes. Please try again later.'), innerError: error.message, state: 'error' }; - } - } - - try { - if (!(await ghRepository.hasBranch(base_ref))) { - if (!CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled()) { - // We won't auto-push a branch if the user has disabled the setting - return { error: vscode.l10n.t('The branch \'{0}\' does not exist on the remote repository \'{1}/{2}\'. Please create the remote branch first.', base_ref, owner, repo), state: 'error' }; - } - // Push the branch - Logger.appendLine(`Base ref needs to exist on remote. Auto pushing base_ref '${base_ref}' to remote repository '${owner}/${repo}'`, CopilotRemoteAgentManager.ID); - await repository.push(remote.remoteName, base_ref, true); - } - } catch (error) { - return { error: vscode.l10n.t('Failed to configure base branch \'{0}\' does not exist on the remote repository \'{1}/{2}\'. Please create the remote branch first.', base_ref, owner, repo), state: 'error' }; - } - - const title = extractTitle(prompt, problemContext); - const { problemStatement, isTruncated } = truncatePrompt(prompt, problemContext); - - if (isTruncated) { - chatStream?.progress(vscode.l10n.t('Truncating context')); - const truncationResult = await vscode.window.showWarningMessage( - vscode.l10n.t('Prompt size exceeded'), { modal: true, detail: vscode.l10n.t('Your prompt will be truncated to fit within coding agent\'s context window. This may affect the quality of the response.') }, CONTINUE_TRUNCATION); - const userCancelled = token?.isCancellationRequested || !truncationResult || truncationResult !== CONTINUE_TRUNCATION; - /* __GDPR__ - "remoteAgent.truncation" : { - "isCancelled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryEvent('remoteAgent.truncation', { - isCancelled: String(userCancelled), - }); - if (userCancelled) { - return { error: vscode.l10n.t('User cancelled due to truncation.'), state: 'error' }; - } - } - - const payload: RemoteAgentJobPayload = { - problem_statement: problemStatement, - event_type: 'visual_studio_code_remote_agent_tool_invoked', - pull_request: { - title, - body_placeholder: formatBodyPlaceholder(title), - base_ref, - body_suffix, - ...(head_ref && { head_ref }) - } - }; - - try { - chatStream?.progress(vscode.l10n.t('Delegating to coding agent')); - const response = await capiClient.postRemoteAgentJob(owner, repo, payload, isTruncated); - - // For v1 API, we need to fetch the job details to get the PR info - // Since the PR might not be created immediately, we need to poll for it - chatStream?.progress(vscode.l10n.t('Creating pull request')); - const jobInfo = await this.waitForJobWithPullRequest(capiClient, owner, repo, response.job_id, token); - if (!jobInfo || !jobInfo.pull_request) { - return { error: vscode.l10n.t('Failed to retrieve pull request information from job'), state: 'error' }; - } - - const { number } = jobInfo.pull_request; - - // Find the actual PR to get the HTML URL - const pullRequest = await this.findPullRequestById(number, true); - const htmlUrl = pullRequest?.html_url || `https://github.com/${owner}/${repo}/pull/${number}`; - - const webviewUri = await toOpenPullRequestWebviewUri({ owner, repo, pullRequestNumber: number }); - const prLlmString = `The remote agent has begun work and has created a pull request. Details about the pull request are being shown to the user. If the user wants to track progress or iterate on the agent's work, they should use the pull request.`; - - return { - state: 'success', - number, - link: htmlUrl, - webviewUri, - llmDetails: head_ref ? `Local pending changes have been pushed to branch '${head_ref}'. ${prLlmString}` : prLlmString, - sessionId: response.session_id - }; - } catch (error) { - return { error: vscode.l10n.t('Failed delegating to coding agent. Please try again later.'), innerError: error.message, state: 'error' }; - } - } - - async getSessionLogsFromAction(pullRequest: PullRequestModel) { - const capi = await this.copilotApi; - if (!capi) { - return []; - } - const lastRun = await this.getLatestCodingAgentFromAction(pullRequest); - if (!lastRun) { - return []; - } - - return await capi.getLogsFromZipUrl(lastRun.logs_url); - } - - async getWorkflowStepsFromAction(pullRequest: PullRequestModel): Promise { - const lastRun = await this.getLatestCodingAgentFromAction(pullRequest, 0, false); - if (!lastRun) { - return []; - } - - try { - const jobs = await pullRequest.githubRepository.getWorkflowJobs(lastRun.id); - const steps: SessionSetupStep[] = []; - - for (const job of jobs) { - if (job.steps) { - for (const step of job.steps) { - steps.push({ name: step.name, status: step.status }); - } - } - } - - return steps; - } catch (error) { - Logger.error(`Failed to get workflow steps: ${error}`, CopilotRemoteAgentManager.ID); - return []; - } - } - - async getLatestCodingAgentFromAction(pullRequest: PullRequestModel, sessionIndex = 0, completedOnly = true): Promise { - const capi = await this.copilotApi; - if (!capi) { - return; - } - const runs = await pullRequest.githubRepository.getWorkflowRunsFromAction(pullRequest.createdAt); - const workflowRuns = runs.flatMap(run => run.workflow_runs); - const padawanRuns = workflowRuns - .filter(run => run.path && run.path.startsWith(`dynamic/${COPILOT_SWE_AGENT}`)) - .filter(run => run.pull_requests?.some(pr => pr.id === pullRequest.id)); - - const session = padawanRuns.filter(s => !completedOnly || s.status === 'completed').at(sessionIndex); - if (!session) { - return; - } - - return this.getLatestRun(padawanRuns); - } - - async getSessionLogFromPullRequest(pullRequest: PullRequestModel, sessionIndex = 0, completedOnly = true): Promise { - const capi = await this.copilotApi; - if (!capi) { - return undefined; - } - - const sessions = await capi.getAllSessions(pullRequest.id); - const session = sessions.filter(s => !completedOnly || s.state === 'completed').at(sessionIndex); - if (!session) { - return undefined; - } - - const logs = await capi.getLogsFromSession(session.id); - - // If session is in progress, try to fetch workflow steps to show setup progress - let setupSteps: SessionSetupStep[] | undefined; - if (session.state === 'in_progress' || logs.trim().length === 0) { - try { - // Get workflow steps instead of logs - setupSteps = await this.getWorkflowStepsFromAction(pullRequest); - } catch (error) { - // If we can't fetch workflow steps, don't fail the entire request - Logger.warn(`Failed to fetch workflow steps for session ${session.id}: ${error}`, CopilotRemoteAgentManager.ID); - } - } - - return { info: session, logs, setupSteps }; - } - - async getSessionUrlFromPullRequest(pullRequest: PullRequestModel): Promise { - const capi = await this.copilotApi; - if (!capi) { - return; - } - - const sessions = await this.getLatestCodingAgentFromAction(pullRequest); - if (!sessions) { - return; - } - return sessions.html_url; - } - - private getLatestRun(runs: T[]): T { - return runs - .slice() - .sort((a, b) => { - const dateA = new Date(a.last_updated_at ?? a.updated_at ?? 0).getTime(); - const dateB = new Date(b.last_updated_at ?? b.updated_at ?? 0).getTime(); - return dateB - dateA; - })[0]; - } - - async extractHistory(history: ReadonlyArray): Promise { - if (!history) { - return; - } - const parts: string[] = []; - for (const turn of history) { - if (turn instanceof vscode.ChatRequestTurn) { - parts.push(`User: ${turn.prompt}`); - } else if (turn instanceof vscode.ChatResponseTurn) { - const textParts = turn.response - .filter(part => part instanceof vscode.ChatResponseMarkdownPart) - .map(part => part.value); - if (textParts.length > 0) { - parts.push(`Copilot: ${textParts.join('\n')}`); - } - } - } - const fullText = parts.join('\n'); // TODO: Summarization if too long - return fullText; - } - - private extractFileReferences(references: readonly ChatPromptReference[] | undefined): string | undefined { - if (!references || references.length === 0) { - return; - } - // 'file:///Users/jospicer/dev/joshbot/.github/workflows/build-vsix.yml' -> '.github/workflows/build-vsix.yml' - const parts: string[] = []; - for (const ref of references) { - if (ref.value instanceof vscode.Uri && ref.value.scheme === 'file') { // TODO: Add support for more kinds of references - const repositoryForFile = getRepositoryForFile(this.gitAPI, ref.value); - if (repositoryForFile) { - const relativePath = pathLib.relative(repositoryForFile.rootUri.fsPath, ref.value.fsPath); - parts.push(` - ${relativePath}`); - } - } - } - - if (!parts.length) { - return; - } - - parts.unshift('The user has attached the following files as relevant context:'); - return parts.join('\n'); - } - - public async provideChatSessions(token: vscode.CancellationToken): Promise { - try { - const capi = await this.copilotApi; - if (!capi) { - return []; - } - - // Check if the token is already cancelled - if (token.isCancellationRequested) { - return []; - } - - await this.waitRepoManagerInitialization(); - - let codingAgentPRs: CodingAgentPRAndStatus[] = await this.prsTreeModel.getCopilotPullRequests(); - Logger.debug(`Fetched PRs from API: ${codingAgentPRs.length}`, CopilotRemoteAgentManager.ID); - - return await Promise.all(codingAgentPRs.map(async prAndStatus => { - const timestampNumber = new Date(prAndStatus.item.createdAt).getTime(); - const status = copilotPRStatusToSessionStatus(prAndStatus.status); - const pullRequest = prAndStatus.item; - const tooltip = await issueMarkdown(pullRequest, this.context, this.repositoriesManager); - - const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); - const prLinkTitle = vscode.l10n.t('Open pull request in VS Code'); - - // If we have multiple repositories, include the repo name in the link text - // e.g., 'owner/repo #123' instead of just '#123' - let repoInfo = ''; - if (this.repositoriesManager.folderManagers.length > 1) { - const owner = pullRequest.remote.owner; - const repo = pullRequest.remote.repositoryName; - repoInfo = `${owner}/${repo} `; - } - const fileCount = pullRequest.fileChanges.size === 0 ? (await pullRequest.getFileChangesInfo()).length : pullRequest.fileChanges.size; - const description = new vscode.MarkdownString(`[${repoInfo}#${pullRequest.number}](${uri.toString()} "${prLinkTitle}")`); // pullRequest.base.ref === defaultBranch ? `PR #${pullRequest.number}`: `PR #${pullRequest.number} → ${pullRequest.base.ref}`; - const chatSession: ChatSessionWithPR = { - resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pullRequest.number }), - label: pullRequest.title || `Session ${pullRequest.number}`, - iconPath: this.getIconForSession(status), - pullRequest: pullRequest, - description: description, - tooltip, - status, - timing: { - startTime: timestampNumber - }, - changes: pullRequest.item.additions !== undefined && pullRequest.item.deletions !== undefined && (pullRequest.item.additions > 0 || pullRequest.item.deletions > 0) ? { - insertions: pullRequest.item.additions, - deletions: pullRequest.item.deletions, - files: fileCount - } : undefined - }; - return chatSession; - })); - } catch (error) { - Logger.error(`Failed to provide coding agents information: ${error}`, CopilotRemoteAgentManager.ID); - } - return []; - } - - public async provideChatSessionContent(resource: URI, token: vscode.CancellationToken): Promise { - try { - const capi = await this.copilotApi; - if (!capi || token.isCancellationRequested) { - return this.createEmptySession(); - } - - await this.waitRepoManagerInitialization(); - - let pullRequestNumber: number | undefined; - let sessionIndex: number | undefined; - - const indexedSessionId = SessionIdForPr.parse(resource); - if (indexedSessionId) { - pullRequestNumber = indexedSessionId.prNumber; - sessionIndex = indexedSessionId.sessionIndex; - } - - if (typeof pullRequestNumber === 'undefined') { - pullRequestNumber = parseInt(resource.path.slice(1)); - if (isNaN(pullRequestNumber)) { - Logger.error(`Invalid pull request number: ${resource}`, CopilotRemoteAgentManager.ID); - return this.createEmptySession(); - } - } - - const pullRequest = await this.findPullRequestById(pullRequestNumber, true); - if (!pullRequest) { - Logger.error(`Pull request not found: ${pullRequestNumber}`, CopilotRemoteAgentManager.ID); - return this.createEmptySession(); - } - - // Parallelize independent operations - const timelineEvents = pullRequest.getTimelineEvents(); - const changeModels = this.getChangeModels(pullRequest); - let sessions = await capi.getAllSessions(pullRequest.id); - - if (!sessions || sessions.length === 0) { - Logger.warn(`No sessions found for pull request ${pullRequestNumber}`, CopilotRemoteAgentManager.ID); - return this.createEmptySession(); - } - - if (!Array.isArray(sessions)) { - Logger.error(`getAllSessions returned non-array: ${typeof sessions}`, CopilotRemoteAgentManager.ID); - return this.createEmptySession(); - } - - if (typeof sessionIndex === 'number') { - const target = sessions.at(sessionIndex); - if (!target) { - Logger.error(`Session not found: ${sessionIndex}`, CopilotRemoteAgentManager.ID); - return this.createEmptySession(); - } - - sessions = [target]; - } - - // Create content builder with pre-fetched change models - const contentBuilder = new ChatSessionContentBuilder(CopilotRemoteAgentManager.ID, COPILOT, changeModels); - - // Parallelize operations that don't depend on each other - const history = await contentBuilder.buildSessionHistory(sessions, pullRequest, capi, timelineEvents); - return { - history, - activeResponseCallback: this.findActiveResponseCallback(sessions, pullRequest), - requestHandler: undefined // TODO(jospicer): chatSessionsProvider@2 uses a single chat participant to handle requests - }; - } catch (error) { - Logger.error(`Failed to provide chat session content: ${error}`, CopilotRemoteAgentManager.ID); - return this.createEmptySession(); - } - } - - private findActiveResponseCallback( - sessions: SessionInfo[], - pullRequest: PullRequestModel - ): ((stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => Thenable) | undefined { - // Only the latest in-progress or queued session gets activeResponseCallback - const pendingSession = sessions - .slice() - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) - .find(session => session.state === 'in_progress' || session.state === 'queued'); - - if (pendingSession) { - return this.createActiveResponseCallback(pullRequest, pendingSession.id); - } - return undefined; - } - - private createEmptySession(): vscode.ChatSession { - return { - history: [], - requestHandler: undefined - }; - } - - private createActiveResponseCallback(pullRequest: PullRequestModel, sessionId: string): (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => Thenable { - return async (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => { - // Use the shared streaming logic - await this.waitForQueuedToInProgress(sessionId, stream, token); - this._onDidCreatePullRequest.fire(pullRequest.number); - return this.streamSessionLogs(stream, pullRequest, sessionId, token); - }; - } - - private async streamNewLogContent(pullRequest: PullRequestModel, stream: vscode.ChatResponseStream, newLogContent: string): Promise<{ hasStreamedContent: boolean; hasSetupStepProgress: boolean }> { - try { - if (!newLogContent.trim()) { - return { hasStreamedContent: false, hasSetupStepProgress: false }; - } - - // Parse the new log content - const logChunks = parseSessionLogs(newLogContent); - let hasStreamedContent = false; - let hasSetupStepProgress = false; - - for (const chunk of logChunks) { - for (const choice of chunk.choices) { - const delta = choice.delta; - - if (delta.role === 'assistant') { - // Handle special case for run_custom_setup_step/run_setup - if (choice.finish_reason === 'tool_calls' && delta.tool_calls?.length && (delta.tool_calls[0].function.name === 'run_custom_setup_step' || delta.tool_calls[0].function.name === 'run_setup')) { - const toolCall = delta.tool_calls[0]; - let args: any = {}; - try { - args = JSON.parse(toolCall.function.arguments); - } catch { - // fallback to empty args - } - - if (delta.content && delta.content.trim()) { - // Finished setup step - create/update tool part - const toolPart = this.createToolInvocationPart(pullRequest, toolCall, args.name || delta.content); - if (toolPart) { - stream.push(toolPart); - hasStreamedContent = true; - } - } else { - // Running setup step - just track progress - hasSetupStepProgress = true; - Logger.appendLine(`Setup step in progress: ${args.name || 'Unknown step'}`, CopilotRemoteAgentManager.ID); - } - } else { - if (delta.content) { - if (!delta.content.startsWith('')) { - stream.markdown(delta.content); - hasStreamedContent = true; - } - } - - if (delta.tool_calls) { - for (const toolCall of delta.tool_calls) { - const toolPart = this.createToolInvocationPart(pullRequest, toolCall, delta.content || ''); - if (toolPart) { - stream.push(toolPart); - hasStreamedContent = true; - } - } - } - } - } - - // Handle finish reasons - if (choice.finish_reason && choice.finish_reason !== 'null') { - Logger.appendLine(`Streaming finish_reason: ${choice.finish_reason}`, CopilotRemoteAgentManager.ID); - } - } - } - - if (hasStreamedContent) { - Logger.appendLine(`Streamed content (markdown or tool parts), progress should be cleared`, CopilotRemoteAgentManager.ID); - } else if (hasSetupStepProgress) { - Logger.appendLine(`Setup step progress detected, keeping progress indicator`, CopilotRemoteAgentManager.ID); - } else { - Logger.appendLine(`No actual content streamed, progress may still be showing`, CopilotRemoteAgentManager.ID); - } - return { hasStreamedContent, hasSetupStepProgress }; - } catch (error) { - Logger.error(`Error streaming new log content: ${error}`, CopilotRemoteAgentManager.ID); - return { hasStreamedContent: false, hasSetupStepProgress: false }; - } - } - - private async streamSessionLogs(stream: vscode.ChatResponseStream, pullRequest: PullRequestModel, sessionId: string, token: vscode.CancellationToken): Promise { - const capi = await this.copilotApi; - if (!capi || token.isCancellationRequested) { - return; - } - - let lastLogLength = 0; - let lastProcessedLength = 0; - let hasActiveProgress = false; - const pollingInterval = 3000; // 3 seconds - - return new Promise((resolve, reject) => { - let isCompleted = false; - - const complete = async () => { - if (isCompleted) { - return; - } - isCompleted = true; - - await pullRequest.getFileChangesInfo(); - const multiDiffPart = await this.getFileChangesMultiDiffPart(pullRequest); - if (multiDiffPart) { - stream.push(multiDiffPart); - } - - resolve(); - }; - - const pollForUpdates = async (): Promise => { - try { - if (token.isCancellationRequested) { - complete(); - return; - } - - // Get the specific session info - const sessionInfo = await capi.getSessionInfo(sessionId); - if (!sessionInfo || token.isCancellationRequested) { - complete(); - return; - } - - // Get session logs - const logs = await capi.getLogsFromSession(sessionId); - - // Check if session is still in progress - if (sessionInfo.state !== 'in_progress') { - if (logs.length > lastProcessedLength) { - const newLogContent = logs.slice(lastProcessedLength); - const streamResult = await this.streamNewLogContent(pullRequest, stream, newLogContent); - if (streamResult.hasStreamedContent) { - hasActiveProgress = false; - } - } - hasActiveProgress = false; - complete(); - return; - } - - if (logs.length > lastLogLength) { - Logger.appendLine(`New logs detected, attempting to stream content`, CopilotRemoteAgentManager.ID); - const newLogContent = logs.slice(lastProcessedLength); - const streamResult = await this.streamNewLogContent(pullRequest, stream, newLogContent); - lastProcessedLength = logs.length; - - if (streamResult.hasStreamedContent) { - Logger.appendLine(`Content was streamed, resetting hasActiveProgress to false`, CopilotRemoteAgentManager.ID); - hasActiveProgress = false; - } else if (streamResult.hasSetupStepProgress) { - Logger.appendLine(`Setup step progress detected, keeping progress active`, CopilotRemoteAgentManager.ID); - // Keep hasActiveProgress as is, don't reset it - } else { - Logger.appendLine(`No content was streamed, keeping hasActiveProgress as ${hasActiveProgress}`, CopilotRemoteAgentManager.ID); - } - } - - lastLogLength = logs.length; - - if (!token.isCancellationRequested && sessionInfo.state === 'in_progress') { - if (!hasActiveProgress) { - Logger.appendLine(`Showing progress indicator (hasActiveProgress was false)`, CopilotRemoteAgentManager.ID); - stream.progress('Working...'); - hasActiveProgress = true; - } else { - Logger.appendLine(`NOT showing progress indicator (hasActiveProgress was true)`, CopilotRemoteAgentManager.ID); - } - setTimeout(pollForUpdates, pollingInterval); - } else { - complete(); - } - } catch (error) { - Logger.error(`Error polling for session updates: ${error}`, CopilotRemoteAgentManager.ID); - if (!token.isCancellationRequested) { - setTimeout(pollForUpdates, pollingInterval); - } else { - reject(error); - } - } - }; - - // Start polling - setTimeout(pollForUpdates, pollingInterval); - }); - } - - private async getChangeModels(pullRequest: PullRequestModel) { - try { - const repoInfo = await this.repoInfo(); - if (!repoInfo) { - return []; - } - - const { fm: folderManager } = repoInfo; - return await PullRequestModel.getChangeModels(folderManager, pullRequest); - } catch (error) { - Logger.error(`Failed to get change models: ${error}`, CopilotRemoteAgentManager.ID); - return []; - } - } - - private async getFileChangesMultiDiffPart(pullRequest: PullRequestModel): Promise { - try { - const changeModels = await this.getChangeModels(pullRequest); - Logger.warn('No file changes found for pull request, not showing diff.', CopilotRemoteAgentManager.ID); - if (changeModels.length === 0) { - return undefined; - } - - const diffEntries: vscode.ChatResponseDiffEntry[] = []; - for (const changeModel of changeModels) { - const { added, removed } = await changeModel.calculateChangedLinesCount(); - Logger.trace(`DiffEntry -> original='${changeModel.parentFilePath}' modified='${changeModel.filePath}' (+${added} -${removed})`, CopilotRemoteAgentManager.ID); - diffEntries.push({ - originalUri: changeModel.parentFilePath, - modifiedUri: changeModel.filePath, - goToFileUri: changeModel.filePath, - added, - removed, - }); - } - - const title = `Changes in Pull Request #${pullRequest.number}`; - return new vscode.ChatResponseMultiDiffPart(diffEntries, title); - } catch (error) { - Logger.error(`Failed to get file changes multi diff part: ${error}`, CopilotRemoteAgentManager.ID); - return undefined; - } - } - - private async findPullRequestById(number: number, fetch: boolean): Promise { - for (const folderManager of this.repositoriesManager.folderManagers) { - for (const githubRepo of folderManager.gitHubRepositories) { - const pullRequest = githubRepo.pullRequestModels.find(pr => pr.number === number); - if (pullRequest) { - return pullRequest; - } - - if (fetch) { - try { - const pullRequest = await githubRepo.getPullRequest(number, false); - if (pullRequest) { - return pullRequest; - } - } catch (error) { - // Continue to next repository if this one doesn't have the PR - Logger.debug(`PR ${number} not found in ${githubRepo.remote.owner}/${githubRepo.remote.repositoryName} (remote=${githubRepo.remote.url}): ${error}`, CopilotRemoteAgentManager.ID); - } - } - } - } - return undefined; - } - - private createToolInvocationPart(pullRequest: PullRequestModel, toolCall: any, deltaContent: string = ''): vscode.ChatToolInvocationPart | vscode.ChatResponseThinkingProgressPart | undefined { - if (!toolCall.function?.name || !toolCall.id) { - return undefined; - } - - // Hide reply_to_comment tool - if (toolCall.function.name === 'reply_to_comment') { - return undefined; - } - - const toolPart = new vscode.ChatToolInvocationPart(toolCall.function.name, toolCall.id); - toolPart.isComplete = true; - toolPart.isError = false; - toolPart.isConfirmed = true; - - try { - const toolDetails = parseToolCallDetails(toolCall, deltaContent); - toolPart.toolName = toolDetails.toolName; - - if (toolCall.toolName === 'think') { - return new vscode.ChatResponseThinkingProgressPart(toolCall.invocationMessage); - } - - if (toolCall.function.name === 'bash') { - toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`bash\n${toolDetails.invocationMessage}\n\`\`\``); - } else { - toolPart.invocationMessage = new vscode.MarkdownString(toolDetails.invocationMessage); - } - - if (toolDetails.pastTenseMessage) { - toolPart.pastTenseMessage = new vscode.MarkdownString(toolDetails.pastTenseMessage); - } - if (toolDetails.originMessage) { - toolPart.originMessage = new vscode.MarkdownString(toolDetails.originMessage); - } - if (toolDetails.toolSpecificData) { - if (StrReplaceEditorToolData.is(toolDetails.toolSpecificData)) { - if ((toolDetails.toolSpecificData.command === 'view' || toolDetails.toolSpecificData.command === 'edit') && toolDetails.toolSpecificData.fileLabel) { - const uri = vscode.Uri.file(pathLib.join(pullRequest.githubRepository.rootUri.fsPath, toolDetails.toolSpecificData.fileLabel)); - toolPart.invocationMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})`); - toolPart.invocationMessage.supportHtml = true; - toolPart.pastTenseMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})`); - } - } else { - toolPart.toolSpecificData = toolDetails.toolSpecificData; - } - } - } catch (error) { - toolPart.toolName = toolCall.function.name || 'unknown'; - toolPart.invocationMessage = new vscode.MarkdownString(`Tool: ${toolCall.function.name}`); - toolPart.isError = true; - } - - return toolPart; - } - - private async waitForQueuedToInProgress( - sessionId: string, - stream?: vscode.ChatResponseStream, - token?: vscode.CancellationToken - ): Promise { - const capi = await this.copilotApi; - if (!capi) { - return undefined; - } - - let sessionInfo: SessionInfo | undefined; - - const waitForQueuedMaxRetries = 3; - const waitForQueuedDelay = 5_000; // 5 seconds - - // Allow for a short delay before the session is marked as 'queued' - let waitForQueuedCount = 0; - do { - sessionInfo = await capi.getSessionInfo(sessionId); - if (sessionInfo && sessionInfo.state === 'queued') { - stream?.progress(vscode.l10n.t('Attaching to session')); - Logger.trace('Queued session found', CopilotRemoteAgentManager.ID); - break; - } - if (waitForQueuedCount < waitForQueuedMaxRetries) { - Logger.trace('Session not yet queued, waiting...', CopilotRemoteAgentManager.ID); - await new Promise(resolve => setTimeout(resolve, waitForQueuedDelay)); - } - ++waitForQueuedCount; - } while (waitForQueuedCount <= waitForQueuedMaxRetries && (!token || !token.isCancellationRequested)); - - if (!sessionInfo || sessionInfo.state !== 'queued') { - if (sessionInfo?.state === 'in_progress') { - Logger.trace('Session already in progress', CopilotRemoteAgentManager.ID); - return sessionInfo; - } - // Failure - Logger.trace('Failed to find queued session', CopilotRemoteAgentManager.ID); - return; - } - - const maxWaitTime = 2 * 60 * 1_000; // 2 minutes - const pollInterval = 3_000; // 3 seconds - const startTime = Date.now(); - - Logger.appendLine(`Session ${sessionInfo.id} is queued, waiting for transition to in_progress...`, CopilotRemoteAgentManager.ID); - while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) { - const sessionInfo = await capi.getSessionInfo(sessionId); - if (sessionInfo?.state === 'in_progress') { - Logger.appendLine(`Session ${sessionInfo.id} now in progress.`, CopilotRemoteAgentManager.ID); - return sessionInfo; - } - await new Promise(resolve => setTimeout(resolve, pollInterval)); - } - Logger.warn(`Timed out waiting for session ${sessionId} to transition from queued to in_progress`, CopilotRemoteAgentManager.ID); - } - - private async waitForNewSession( - pullRequest: PullRequestModel, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken, - waitForTransitionToInProgress: boolean = false - ): Promise { - // Get the current number of sessions - const capi = await this.copilotApi; - if (!capi) { - stream.markdown(vscode.l10n.t('Failed to connect to Copilot API.')); - return; - } - - const initialSessions = await capi.getAllSessions(pullRequest.id); - const initialSessionCount = initialSessions.length; - - // Poll for a new session to start - const maxWaitTime = 5 * 60 * 1000; // 5 minutes - const pollInterval = 3000; // 3 seconds - const startTime = Date.now(); - - while (Date.now() - startTime < maxWaitTime && !token.isCancellationRequested) { - const currentSessions = await capi.getAllSessions(pullRequest.id); - - // Check if a new session has started - if (currentSessions.length > initialSessionCount) { - const newSession = currentSessions - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]; - if (!waitForTransitionToInProgress) { - return newSession; - } - const inProgressSession = await this.waitForQueuedToInProgress(newSession.id, stream, token); - if (!inProgressSession) { - stream.markdown(vscode.l10n.t('Timed out waiting for coding agent to begin work. Please try again shortly.')); - return; - } - return inProgressSession; - } - - await new Promise(resolve => setTimeout(resolve, pollInterval)); - } - - stream.markdown(vscode.l10n.t('Timed out waiting for the coding agent to respond. The agent may still be processing your request.')); - return; - } - - private getIconForSession(status: vscode.ChatSessionStatus): vscode.Uri | vscode.ThemeIcon { - // Fallback to theme icons if no theme data available - switch (status) { - case vscode.ChatSessionStatus.Completed: - return new vscode.ThemeIcon('issues', new vscode.ThemeColor('testing.iconPassed')); - case vscode.ChatSessionStatus.Failed: - return new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconFailed')); - default: - return new vscode.ThemeIcon('issue-reopened', new vscode.ThemeColor('editorLink.activeForeground')); - } - } - - public refreshChatSessions(): void { - this.prsTreeModel.clearCopilotCaches(); - this._onDidChangeChatSessions.fire(); - } - - private async waitForJobWithPullRequest( - capiClient: CopilotApi, - owner: string, - repo: string, - jobId: string, - token?: vscode.CancellationToken - ): Promise { - const maxWaitTime = 30 * 1000; // 30 seconds - const pollInterval = 2000; // 2 seconds - const startTime = Date.now(); - - Logger.appendLine(`Waiting for job ${jobId} to have pull request information...`, CopilotRemoteAgentManager.ID); - - while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) { - const jobInfo = await capiClient.getJobByJobId(owner, repo, jobId); - if (jobInfo && jobInfo.pull_request && jobInfo.pull_request.number) { - Logger.appendLine(`Job ${jobId} now has pull request #${jobInfo.pull_request.number}`, CopilotRemoteAgentManager.ID); - return jobInfo; - } - await new Promise(resolve => setTimeout(resolve, pollInterval)); - } - - Logger.warn(`Timed out waiting for job ${jobId} to have pull request information`, CopilotRemoteAgentManager.ID); - return undefined; - } - - public async cancelMostRecentChatSession(pullRequest: PullRequestModel): Promise { - const capi = await this.copilotApi; - if (!capi) { - Logger.warn(`No Copilot API instance found`); - return; - } - - const folderManager = this.repositoriesManager.getManagerForIssueModel(pullRequest) ?? this.repositoriesManager.folderManagers[0]; - if (!folderManager) { - Logger.warn(`No folder manager found for pull request`); - return; - } - - const sessions = await capi.getAllSessions(pullRequest.id); - if (sessions.length > 0) { - const mostRecentSession = sessions[sessions.length - 1]; - const folder = folderManager.gitHubRepositories.find(repo => repo.remote.remoteName === pullRequest.remote.remoteName); - folder?.cancelWorkflow(mostRecentSession.workflow_run_id); - } else { - Logger.warn(`No active chat session found for pull request ${pullRequest.id}`); - } - } } \ No newline at end of file diff --git a/src/github/copilotRemoteAgent/chatSessionContentBuilder.ts b/src/github/copilotRemoteAgent/chatSessionContentBuilder.ts deleted file mode 100644 index 626c3fccc1..0000000000 --- a/src/github/copilotRemoteAgent/chatSessionContentBuilder.ts +++ /dev/null @@ -1,468 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nodePath from 'path'; -import * as marked from 'marked'; -import vscode from 'vscode'; -import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from '../../../common/sessionParsing'; -import { COPILOT_SWE_AGENT } from '../../common/copilot'; -import Logger from '../../common/logger'; -import { CommentEvent, CopilotFinishedEvent, CopilotStartedEvent, EventType, ReviewEvent, TimelineEvent } from '../../common/timelineEvent'; -import { toOpenPullRequestWebviewUri } from '../../common/uri'; -import { InMemFileChangeModel, RemoteFileChangeModel } from '../../view/fileChangeModel'; -import { AssistantDelta, Choice, ToolCall } from '../common'; -import { CopilotApi, SessionInfo } from '../copilotApi'; -import { PlainTextRenderer } from '../markdownUtils'; -import { PullRequestModel } from '../pullRequestModel'; - -export class ChatSessionContentBuilder { - constructor( - private loggerId: string, - private readonly handler: string, - private getChangeModels: Promise<(RemoteFileChangeModel | InMemFileChangeModel)[]> - ) { } - - public async buildSessionHistory( - sessions: SessionInfo[], - pullRequest: PullRequestModel, - capi: CopilotApi, - timelineEventsPromise: Promise - ): Promise> { - const sortedSessions = sessions - .filter((session, index, array) => - array.findIndex(s => s.id === session.id) === index - ) - .slice().sort((a, b) => - new Date(a.created_at).getTime() - new Date(b.created_at).getTime() - ); - - // Process all sessions concurrently while maintaining order - const sessionResults = await Promise.all( - sortedSessions.map(async (session, sessionIndex) => { - const firstHistoryEntry = async () => { - const sessionPrompt = await this.determineSessionPrompt(session, sessionIndex, pullRequest, timelineEventsPromise, capi); - - // Create request turn for this session - const sessionRequest = new vscode.ChatRequestTurn2( - sessionPrompt, - undefined, // command - [], // references - COPILOT_SWE_AGENT, - [], // toolReferences - [] - ); - return sessionRequest; - }; - const secondHistoryEntry = async () => { - const logs = await capi.getLogsFromSession(session.id); - // Create response turn - const responseHistory = await this.createResponseTurn(pullRequest, logs, session); - return responseHistory; - }; - const [first, second] = await Promise.all([ - firstHistoryEntry(), - secondHistoryEntry(), - ]); - - return { first, second, sessionIndex }; - }) - ); - - const history: Array = []; - - // Build history array in the correct order - for (const { first, second, sessionIndex } of sessionResults) { - history.push(first); - - if (second) { - // if this is the first response, then also add the PR card - if (sessionIndex === 0) { - const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); - const plaintextBody = marked.parse(pullRequest.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); - - const card = new vscode.ChatResponsePullRequestPart(uri, pullRequest.title, plaintextBody, pullRequest.author.specialDisplayName ?? pullRequest.author.login, `#${pullRequest.number}`); - const cardTurn = new vscode.ChatResponseTurn2([card], {}, COPILOT_SWE_AGENT); - history.push(cardTurn); - } - history.push(second); - } - } - return history; - } - - private async createResponseTurn(pullRequest: PullRequestModel, logs: string, session: SessionInfo): Promise { - if (logs.trim().length > 0) { - return await this.parseSessionLogsIntoResponseTurn(pullRequest, logs, session); - } else if (session.state === 'in_progress' || session.state === 'queued') { - // For in-progress sessions without logs, create a placeholder response - const placeholderParts = [new vscode.ChatResponseProgressPart('Initializing session')]; - const responseResult: vscode.ChatResult = {}; - return new vscode.ChatResponseTurn2(placeholderParts, responseResult, COPILOT_SWE_AGENT); - } else { - // For completed sessions without logs, add an empty response to maintain pairing - const emptyParts = [new vscode.ChatResponseMarkdownPart('_No logs available for this session_')]; - const responseResult: vscode.ChatResult = {}; - return new vscode.ChatResponseTurn2(emptyParts, responseResult, COPILOT_SWE_AGENT); - } - } - - private async determineSessionPrompt( - session: SessionInfo, - sessionIndex: number, - pullRequest: PullRequestModel, - timelineEventsPromise: Promise, - capi: CopilotApi - ): Promise { - let sessionPrompt = session.name || `Session ${sessionIndex + 1} (ID: ${session.id})`; - - if (sessionIndex === 0) { - sessionPrompt = await this.getInitialSessionPrompt(session, pullRequest, capi, sessionPrompt); - } else { - sessionPrompt = await this.getFollowUpSessionPrompt(sessionIndex, timelineEventsPromise, sessionPrompt); - } - - // TODO: @rebornix, remove @copilot prefix from session prompt for now - sessionPrompt = sessionPrompt.replace(/@copilot\s*/gi, '').trim(); - return sessionPrompt; - } - - private async getFollowUpSessionPrompt( - sessionIndex: number, - timelineEventsPromise: Promise, - defaultPrompt: string - ): Promise { - const timelineEvents = await timelineEventsPromise; - Logger.appendLine(`Found ${timelineEvents.length} timeline events`, this.loggerId); - const copilotStartedEvents = timelineEvents - .filter((event): event is CopilotStartedEvent => event.event === EventType.CopilotStarted) - .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - - const copilotFinishedEvents = timelineEvents - .filter((event): event is CopilotFinishedEvent => event.event === EventType.CopilotFinished) - .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); - - Logger.appendLine(`Session ${sessionIndex}: Found ${copilotStartedEvents.length} CopilotStarted events and ${copilotFinishedEvents.length} CopilotFinished events`, this.loggerId); - - const copilotStartedEvent = copilotStartedEvents[sessionIndex]; - if (!copilotStartedEvent) { - Logger.appendLine(`Session ${sessionIndex}: No CopilotStarted event found at index ${sessionIndex}`, this.loggerId); - return defaultPrompt; - } - - const currentSessionStartTime = new Date(copilotStartedEvent.createdAt).getTime(); - const previousSessionEndTime = this.getPreviousSessionEndTime(sessionIndex, copilotFinishedEvents); - - const relevantEvents = this.findRelevantTimelineEvents(timelineEvents, previousSessionEndTime, currentSessionStartTime); - - const matchingEvent = relevantEvents[0]; - if (matchingEvent) { - const prompt = this.extractPromptFromEvent(matchingEvent); - Logger.appendLine(`Session ${sessionIndex}: Found matching event - ${matchingEvent.event}`, this.loggerId); - return prompt; - } else { - Logger.appendLine(`Session ${sessionIndex}: No matching event found between times ${previousSessionEndTime} and ${currentSessionStartTime}`, this.loggerId); - Logger.appendLine(`Session ${sessionIndex}: Relevant events found: ${relevantEvents.length}`, this.loggerId); - return defaultPrompt; - } - } - - private getPreviousSessionEndTime(sessionIndex: number, copilotFinishedEvents: CopilotFinishedEvent[]): number { - if (sessionIndex > 0 && copilotFinishedEvents[sessionIndex - 1]) { - return new Date(copilotFinishedEvents[sessionIndex - 1].createdAt).getTime(); - } - return 0; - } - - private findRelevantTimelineEvents( - timelineEvents: readonly TimelineEvent[], - previousSessionEndTime: number, - currentSessionStartTime: number - ): TimelineEvent[] { - return timelineEvents - .filter(event => { - if (event.event !== EventType.Commented && event.event !== EventType.Reviewed) { - return false; - } - - const eventTime = new Date( - event.event === EventType.Commented ? (event as CommentEvent).createdAt : - event.event === EventType.Reviewed ? (event as ReviewEvent).submittedAt : '' - ).getTime(); - - // Must be after previous session and before current session - return eventTime > previousSessionEndTime && eventTime < currentSessionStartTime; - }) - .filter(event => { - if (event.event === EventType.Commented) { - const comment = event as CommentEvent; - return comment.body.includes('@copilot') || comment.body.includes(this.handler); - } else if (event.event === EventType.Reviewed) { - const review = event as ReviewEvent; - return review.body.includes('@copilot') || review.body.includes(this.handler); - } - return false; - }) - .sort((a, b) => { - const timeA = new Date( - a.event === EventType.Commented ? (a as CommentEvent).createdAt : - a.event === EventType.Reviewed ? (a as ReviewEvent).submittedAt : '' - ).getTime(); - const timeB = new Date( - b.event === EventType.Commented ? (b as CommentEvent).createdAt : - b.event === EventType.Reviewed ? (b as ReviewEvent).submittedAt : '' - ).getTime(); - return timeB - timeA; // Most recent first (closest to session start) - }); - } - - private extractPromptFromEvent(event: TimelineEvent): string { - let body = ''; - if (event.event === EventType.Commented) { - body = (event as CommentEvent).body; - } else if (event.event === EventType.Reviewed) { - body = (event as ReviewEvent).body; - } - - // Extract the prompt before any separator pattern (used in addFollowUpToExistingPR) - // but keep the @copilot mention - const separatorMatch = body.match(/^(.*?)\s*\n\n\s*---\s*\n\n/s); - if (separatorMatch) { - return separatorMatch[1].trim(); - } - - return body.trim(); - } - - private async getInitialSessionPrompt( - session: SessionInfo, - pullRequest: PullRequestModel, - capi: CopilotApi, - defaultPrompt: string - ): Promise { - try { - const jobInfo = await capi.getJobBySessionId( - pullRequest.base.repositoryCloneUrl.owner, - pullRequest.base.repositoryCloneUrl.repositoryName, - session.id - ); - if (jobInfo && jobInfo.problem_statement) { - let prompt = jobInfo.problem_statement; - const titleMatch = jobInfo.problem_statement.match(/TITLE: \s*(.*)/i); - if (titleMatch && titleMatch[1]) { - prompt = titleMatch[1].trim(); - } else { - const split = jobInfo.problem_statement.split('\n'); - if (split.length > 0) { - prompt = split[0].trim(); - } - } - Logger.appendLine(`Session 0: Found problem_statement from Jobs API: ${prompt}`, this.loggerId); - return prompt; - } - } catch (error) { - Logger.warn(`Failed to get job info for session ${session.id}: ${error}`, this.loggerId); - } - return defaultPrompt; - } - - private async parseSessionLogsIntoResponseTurn(pullRequest: PullRequestModel, logs: string, session: SessionInfo): Promise { - try { - const logChunks = parseSessionLogs(logs); - const responseParts: Array = []; - let currentResponseContent = ''; - - for (const chunk of logChunks) { - if (!chunk.choices || !Array.isArray(chunk.choices)) { - continue; - } - - for (const choice of chunk.choices) { - const delta = choice.delta; - if (delta.role === 'assistant') { - this.processAssistantDelta(delta, choice, pullRequest, responseParts, currentResponseContent); - } - - } - } - - if (currentResponseContent.trim()) { - responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())); - } - - if (session.state === 'completed' || session.state === 'failed' /** session can fail with proposed changes */) { - const fileChangesPart = await this.getFileChangesMultiDiffPart(pullRequest); - if (fileChangesPart) { - responseParts.push(fileChangesPart); - } - } - - if (responseParts.length > 0) { - const responseResult: vscode.ChatResult = {}; - return new vscode.ChatResponseTurn2(responseParts, responseResult, COPILOT_SWE_AGENT); - } - - return undefined; - } catch (error) { - Logger.error(`Failed to parse session logs into response turn: ${error}`, this.loggerId); - return undefined; - } - } - - private processAssistantDelta( - delta: AssistantDelta, - choice: Choice, - pullRequest: PullRequestModel, - responseParts: Array, - currentResponseContent: string, - ): string { - if (delta.role === 'assistant') { - // Handle special case for run_custom_setup_step - if ( - choice.finish_reason === 'tool_calls' && - delta.tool_calls?.length && - (delta.tool_calls[0].function.name === 'run_custom_setup_step' || delta.tool_calls[0].function.name === 'run_setup') - ) { - const toolCall = delta.tool_calls[0]; - let args: { name?: string } = {}; - try { - args = JSON.parse(toolCall.function.arguments); - } catch { - // fallback to empty args - } - - // Ignore if delta.content is empty/undefined (running state) - if (delta.content && delta.content.trim()) { - // Add any accumulated content as markdown first - if (currentResponseContent.trim()) { - responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())); - currentResponseContent = ''; - } - - const toolPart = this.createToolInvocationPart(pullRequest, toolCall, args.name || delta.content); - if (toolPart) { - responseParts.push(toolPart); - } - } - // Skip if content is empty (running state) - } else { - if (delta.content) { - if (!delta.content.startsWith('') && !delta.content.startsWith('')) { - currentResponseContent += delta.content; - } - } - - const isError = delta.content?.startsWith(''); - if (delta.tool_calls) { - // Add any accumulated content as markdown first - if (currentResponseContent.trim()) { - responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())); - currentResponseContent = ''; - } - - for (const toolCall of delta.tool_calls) { - const toolPart = this.createToolInvocationPart(pullRequest, toolCall, delta.content || ''); - if (toolPart) { - responseParts.push(toolPart); - } - } - - if (isError) { - const toolPart = new vscode.ChatToolInvocationPart('Command', 'command'); - // Remove at the start and at the end - const cleaned = (delta.content ?? '').replace(/^\s*\s*/i, '').replace(/\s*<\/error>\s*$/i, ''); - toolPart.invocationMessage = cleaned; - toolPart.isError = true; - responseParts.push(toolPart); - } - } - } - } - return currentResponseContent; - } - - private createToolInvocationPart(pullRequest: PullRequestModel, toolCall: ToolCall, deltaContent: string = ''): vscode.ChatToolInvocationPart | vscode.ChatResponseThinkingProgressPart | undefined { - if (!toolCall.function?.name || !toolCall.id) { - return undefined; - } - - // Hide reply_to_comment tool - if (toolCall.function.name === 'reply_to_comment') { - return undefined; - } - - const toolPart = new vscode.ChatToolInvocationPart(toolCall.function.name, toolCall.id); - toolPart.isComplete = true; - toolPart.isError = false; - toolPart.isConfirmed = true; - - try { - const toolDetails = parseToolCallDetails(toolCall, deltaContent); - toolPart.toolName = toolDetails.toolName; - - if (toolPart.toolName === 'think') { - return new vscode.ChatResponseThinkingProgressPart(toolDetails.invocationMessage); - } - - if (toolCall.function.name === 'bash') { - toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`bash\n${toolDetails.invocationMessage}\n\`\`\``); - } else { - toolPart.invocationMessage = new vscode.MarkdownString(toolDetails.invocationMessage); - } - - if (toolDetails.pastTenseMessage) { - toolPart.pastTenseMessage = new vscode.MarkdownString(toolDetails.pastTenseMessage); - } - if (toolDetails.originMessage) { - toolPart.originMessage = new vscode.MarkdownString(toolDetails.originMessage); - } - if (toolDetails.toolSpecificData) { - if (StrReplaceEditorToolData.is(toolDetails.toolSpecificData)) { - if ((toolDetails.toolSpecificData.command === 'view' || toolDetails.toolSpecificData.command === 'edit') && toolDetails.toolSpecificData.fileLabel) { - const uri = vscode.Uri.file(nodePath.join(pullRequest.githubRepository.rootUri.fsPath, toolDetails.toolSpecificData.fileLabel)); - toolPart.invocationMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})` + (toolDetails.toolSpecificData?.viewRange ? `, lines ${toolDetails.toolSpecificData.viewRange?.start} to ${toolDetails.toolSpecificData.viewRange?.end}` : '')); - toolPart.invocationMessage.supportHtml = true; - toolPart.pastTenseMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})` + (toolDetails.toolSpecificData?.viewRange ? `, lines ${toolDetails.toolSpecificData.viewRange?.start} to ${toolDetails.toolSpecificData.viewRange?.end}` : '')); - } - } else { - toolPart.toolSpecificData = toolDetails.toolSpecificData; - } - } - } catch (error) { - toolPart.toolName = toolCall.function.name || 'unknown'; - toolPart.invocationMessage = new vscode.MarkdownString(`Tool: ${toolCall.function.name}`); - toolPart.isError = true; - } - - return toolPart; - } - - private async getFileChangesMultiDiffPart(pullRequest: PullRequestModel): Promise { - try { - const changeModels = await this.getChangeModels; - - if (changeModels.length === 0) { - return undefined; - } - - const diffEntries: vscode.ChatResponseDiffEntry[] = []; - for (const changeModel of changeModels) { - const { added, removed } = await changeModel.calculateChangedLinesCount(); - diffEntries.push({ - originalUri: changeModel.parentFilePath, - modifiedUri: changeModel.filePath, - goToFileUri: changeModel.filePath, - added, - removed, - }); - } - - const title = `Changes in Pull Request #${pullRequest.number}`; - return new vscode.ChatResponseMultiDiffPart(diffEntries, title); - } catch (error) { - Logger.error(`Failed to get file changes multi diff part: ${error}`, this.loggerId); - return undefined; - } - } -} \ No newline at end of file diff --git a/src/github/copilotRemoteAgent/gitOperationsManager.ts b/src/github/copilotRemoteAgent/gitOperationsManager.ts deleted file mode 100644 index dd9d49640d..0000000000 --- a/src/github/copilotRemoteAgent/gitOperationsManager.ts +++ /dev/null @@ -1,193 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { adjectives, animals, colors, NumberDictionary, uniqueNamesGenerator } from '@joaomoreno/unique-names-generator'; -import vscode from 'vscode'; -import { Repository } from '../../api/api'; -import Logger from '../../common/logger'; -import { BRANCH_RANDOM_NAME_DICTIONARY, BRANCH_WHITESPACE_CHAR, GIT } from '../../common/settingKeys'; -import { RepoInfo } from '../common'; - -export class GitOperationsManager { - constructor(private loggerID: string) { } - - async commitAndPushChanges(repoInfo: RepoInfo) { - const { repository, remote, baseRef } = repoInfo; - const asyncBranch = await this.generateRandomBranchName(repository, 'copilot'); - - try { - await repository.createBranch(asyncBranch, true); - const commitMessage = 'Checkpoint from VS Code for coding agent session'; - - await this.performCommit(asyncBranch, repository, commitMessage); - await repository.push(remote.remoteName, asyncBranch, true); - this.showBranchSwitchNotification(repository, baseRef, asyncBranch); - return asyncBranch; // This is the new head ref - } catch (error) { - await this.rollbackToOriginalBranch(repository, baseRef); - Logger.error(`Failed to auto-commit and push pending changes: ${error}`, this.loggerID); - throw new Error(vscode.l10n.t('Could not auto-push pending changes. Manually commit or stash your changes and try again. ({0})', error.message)); - } - } - - private async performCommit(asyncBranch: string, repository: Repository, commitMessage: string): Promise { - try { - await repository.commit(commitMessage, { all: true }); - - if (repository.state.HEAD?.name !== asyncBranch || repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0) { - throw new Error(vscode.l10n.t('Uncommitted changes still detected.')); - } - } catch (error) { - // Fallback to interactive commit - const commitSuccessful = await this.handleInteractiveCommit(repository); - if (!commitSuccessful) { - throw new Error(vscode.l10n.t('Exclude your uncommitted changes and try again.')); - } - } - } - - private async handleInteractiveCommit(repository: Repository): Promise { - const COMMIT_YOUR_CHANGES = vscode.l10n.t('Commit your changes to continue coding agent session. Close integrated terminal to cancel.'); - - return vscode.window.withProgress({ - title: COMMIT_YOUR_CHANGES, - cancellable: true, - location: vscode.ProgressLocation.Notification - }, async (progress, token) => { - return new Promise((resolve) => { - const startingCommit = repository.state.HEAD?.commit; - const terminal = vscode.window.createTerminal({ - name: 'GitHub Coding Agent', - cwd: repository.rootUri.fsPath, - message: `\x1b[1m${COMMIT_YOUR_CHANGES}\x1b[0m` - }); - - terminal.show(); - - let disposed = false; - let timeoutId: NodeJS.Timeout; - let stateListener: vscode.Disposable | undefined; - let disposalListener: vscode.Disposable | undefined; - let cancellationListener: vscode.Disposable | undefined; - const cleanup = () => { - if (disposed) return; - disposed = true; - clearTimeout(timeoutId); - stateListener?.dispose(); - disposalListener?.dispose(); - cancellationListener?.dispose(); - terminal.dispose(); - }; - // Listen for cancellation if token is provided - if (token) { - cancellationListener = token.onCancellationRequested(() => { - cleanup(); - resolve(false); - }); - } - - // Listen for repository state changes - stateListener = repository.state.onDidChange(() => { - // Check if commit was successful (HEAD changed and no more staged changes) - if (repository.state.HEAD?.commit !== startingCommit) { - cleanup(); - resolve(true); - } - }); - // Set a timeout to avoid waiting forever - timeoutId = setTimeout(() => { - cleanup(); - resolve(false); - }, 5 * 60 * 1000); // 5 minutes timeout - // Listen for terminal disposal (user closed it) - disposalListener = vscode.window.onDidCloseTerminal((closedTerminal) => { - if (closedTerminal === terminal) { - setTimeout(() => { - if (!disposed) { - cleanup(); - // Check one more time if commit happened just before terminal was closed - resolve(repository.state.HEAD?.commit !== startingCommit); - } - }, 1000); - } - }); - }); - }); - } - - private showBranchSwitchNotification(repository: Repository, baseRef: string, newRef: string): void { - if (repository.state.HEAD?.name !== baseRef) { - const SWAP_BACK_TO_ORIGINAL_BRANCH = vscode.l10n.t(`Swap back to '{0}'`, baseRef); - vscode.window.showInformationMessage( - vscode.l10n.t(`Pending changes pushed to remote branch '{0}'.`, newRef), - SWAP_BACK_TO_ORIGINAL_BRANCH, - ).then(async (selection) => { - if (selection === SWAP_BACK_TO_ORIGINAL_BRANCH) { - await repository.checkout(baseRef); - } - }); - } - } - - private async rollbackToOriginalBranch(repository: Repository, baseRef: string): Promise { - if (repository.state.HEAD?.name !== baseRef) { - try { - await repository.checkout(baseRef); - } catch (checkoutError) { - Logger.error(`Failed to checkout back to original branch '${baseRef}': ${checkoutError}`, this.loggerID); - } - } - } - - // Adapted from https://github.com/microsoft/vscode/blob/e35e3b4e057450ea3d90c724fae5e3e9619b96fe/extensions/git/src/commands.ts#L3007 - private async generateRandomBranchName(repository: Repository, prefix: string): Promise { - const config = vscode.workspace.getConfiguration(GIT); - const branchWhitespaceChar = config.get(BRANCH_WHITESPACE_CHAR); - const branchRandomNameDictionary = config.get(BRANCH_RANDOM_NAME_DICTIONARY); - - // Default to legacy behaviour if config mismatches core - if (branchWhitespaceChar === undefined || branchRandomNameDictionary === undefined) { - return `copilot/vscode${Date.now()}`; - } - - const separator = branchWhitespaceChar; - const dictionaries: string[][] = []; - for (const dictionary of branchRandomNameDictionary) { - if (dictionary.toLowerCase() === 'adjectives') { - dictionaries.push(adjectives); - } - if (dictionary.toLowerCase() === 'animals') { - dictionaries.push(animals); - } - if (dictionary.toLowerCase() === 'colors') { - dictionaries.push(colors); - } - if (dictionary.toLowerCase() === 'numbers') { - dictionaries.push(NumberDictionary.generate({ length: 3 })); - } - } - - if (dictionaries.length === 0) { - return ''; - } - - // 5 attempts to generate a random branch name - for (let index = 0; index < 5; index++) { - const randomName = `${prefix}/${uniqueNamesGenerator({ - dictionaries, - length: dictionaries.length, - separator - })}`; - - // Check for local ref conflict - const refs = await repository.getRefs?.({ pattern: `refs/heads/${randomName}` }); - if (!refs || refs.length === 0) { - return randomName; - } - } - - return ''; - } -} diff --git a/src/github/copilotRemoteAgentUtils.ts b/src/github/copilotRemoteAgentUtils.ts deleted file mode 100644 index 306efb2449..0000000000 --- a/src/github/copilotRemoteAgentUtils.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import { MAX_PROBLEM_STATEMENT_LENGTH } from './copilotApi'; -import Logger from '../common/logger'; - -/** - * Truncation utility to ensure the problem statement sent to Copilot API is under the maximum length. - * Truncation is not ideal. The caller providing the prompt/context should be summarizing so this is a no-op whenever possible. - * - * @param prompt The final message submitted by the user - * @param context Any additional context collected by the caller (chat history, open files, etc...) - * @returns A complete 'problem statement' string that is under the maximum length, and a flag indicating if truncation occurred - */ -export function truncatePrompt(prompt: string, context?: string): { problemStatement: string; isTruncated: boolean } { - // Prioritize the userPrompt - // Take the last n characters that fit within the limit - if (prompt.length >= MAX_PROBLEM_STATEMENT_LENGTH) { - Logger.warn(`Truncation: Prompt length ${prompt.length} exceeds max of ${MAX_PROBLEM_STATEMENT_LENGTH}`); - prompt = prompt.slice(-MAX_PROBLEM_STATEMENT_LENGTH); - return { problemStatement: prompt, isTruncated: true }; - } - - if (context && (prompt.length + context.length >= MAX_PROBLEM_STATEMENT_LENGTH)) { - const availableLength = MAX_PROBLEM_STATEMENT_LENGTH - prompt.length - 2 /* new lines */; - Logger.warn(`Truncation: Combined prompt and context length ${prompt.length + context.length} exceeds max of ${MAX_PROBLEM_STATEMENT_LENGTH}`); - context = context.slice(-availableLength); - return { - problemStatement: prompt + (context ? `\n\n${context}` : ''), - isTruncated: true - }; - } - - // No truncation occurred - return { - problemStatement: prompt + (context ? `\n\n${context}` : ''), - isTruncated: false - }; -} - -export function extractTitle(prompt: string, context: string | undefined): string | undefined { - const fromTitle = () => { - if (!prompt) { - return; - } - if (prompt.length <= 20) { - return prompt; - } - return prompt.substring(0, 20) + '...'; - }; - const titleMatch = context?.match(/TITLE: \s*(.*)/i); - if (titleMatch && titleMatch[1]) { - return titleMatch[1].trim(); - } - return fromTitle(); - -} - -export function formatBodyPlaceholder(title: string | undefined): string { - return vscode.l10n.t('Coding agent has begun work on **{0}** and will update this pull request as work progresses.', title || vscode.l10n.t('your request')); -} \ No newline at end of file diff --git a/src/issues/issueFeatureRegistrar.ts b/src/issues/issueFeatureRegistrar.ts index dfe3043c74..7c09783e8f 100644 --- a/src/issues/issueFeatureRegistrar.ts +++ b/src/issues/issueFeatureRegistrar.ts @@ -60,7 +60,6 @@ import { USER_EXPRESSION, YamlIssueTemplate, } from './util'; -import { truncate } from '../common/utils'; import { OctokitCommon } from '../github/common'; import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager'; @@ -144,19 +143,6 @@ export class IssueFeatureRegistrar extends Disposable { this, ), ); - this._register( - vscode.commands.registerCommand( - 'issue.startCodingAgentFromTodo', - (todoInfo?: { document: vscode.TextDocument; lineNumber: number; line: string; insertIndex: number; range: vscode.Range }) => { - /* __GDPR__ - "issue.startCodingAgentFromTodo" : {} - */ - this.telemetry.sendTelemetryEvent('issue.startCodingAgentFromTodo'); - return this.startCodingAgentFromTodo(todoInfo); - }, - this, - ), - ); this._register( vscode.commands.registerCommand( 'issue.assignToCodingAgent', @@ -626,9 +612,6 @@ export class IssueFeatureRegistrar extends Disposable { this._register( vscode.languages.registerCodeActionsProvider('*', todoProvider, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }), ); - this._register( - vscode.languages.registerCodeLensProvider('*', todoProvider), - ); }); } @@ -1679,35 +1662,6 @@ ${options?.body ?? ''}\n return undefined; } - async startCodingAgentFromTodo(todoInfo?: { document: vscode.TextDocument; lineNumber: number; line: string; insertIndex: number; range: vscode.Range }) { - if (!todoInfo) { - return; - } - - const { document, line, insertIndex } = todoInfo; - const todoText = line.substring(insertIndex).trim(); - if (!todoText) { - vscode.window.showWarningMessage(vscode.l10n.t('No task description found in TODO comment')); - return; - } - - const relativePath = vscode.workspace.asRelativePath(document.uri); - const prompt = vscode.l10n.t('Work on TODO: {0} (from {1})', todoText, relativePath); - return vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - title: vscode.l10n.t('Delegating \'{0}\' to coding agent', truncate(todoText, 20)) - }, async (_progress) => { - try { - await this.copilotRemoteAgentManager.commandImpl({ - userPrompt: prompt, - source: 'todo' - }); - } catch (error) { - vscode.window.showErrorMessage(vscode.l10n.t('Failed to start coding agent session: {0}', error.message)); - } - }); - } - async assignToCodingAgent(issueModel: any) { if (!issueModel) { return; diff --git a/src/issues/issueTodoProvider.ts b/src/issues/issueTodoProvider.ts index 383e6007aa..a4e24414c6 100644 --- a/src/issues/issueTodoProvider.ts +++ b/src/issues/issueTodoProvider.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { isComment, MAX_LINE_LENGTH } from './util'; -import { CODING_AGENT, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../common/settingKeys'; +import { MAX_LINE_LENGTH } from './util'; +import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { escapeRegExp } from '../common/utils'; import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { ISSUE_OR_URL_EXPRESSION } from '../github/utils'; -export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.CodeLensProvider { +export class IssueTodoProvider implements vscode.CodeActionProvider { private expression: RegExp | undefined; constructor( @@ -97,43 +97,4 @@ export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.Code } while (range.end.line >= lineNumber); return codeActions; } - - async provideCodeLenses( - document: vscode.TextDocument, - _token: vscode.CancellationToken, - ): Promise { - if (this.expression === undefined) { - return []; - } - - // Check if CodeLens is enabled - const isCodeLensEnabled = vscode.workspace.getConfiguration(CODING_AGENT).get(SHOW_CODE_LENS, true); - if (!isCodeLensEnabled) { - return []; - } - - const codeLenses: vscode.CodeLens[] = []; - for (let lineNumber = 0; lineNumber < document.lineCount; lineNumber++) { - const textLine = document.lineAt(lineNumber); - const { text: line, firstNonWhitespaceCharacterIndex } = textLine; - const todoInfo = this.findTodoInLine(line); - if (!todoInfo) { - continue; - } - if (!(await isComment(document, new vscode.Position(lineNumber, firstNonWhitespaceCharacterIndex)))) { - continue; - } - const { match, search, insertIndex } = todoInfo; - const range = new vscode.Range(lineNumber, search, lineNumber, search + match[0].length); - if (this.copilotRemoteAgentManager && (await this.copilotRemoteAgentManager.isAvailable())) { - const startAgentCodeLens = new vscode.CodeLens(range, { - title: vscode.l10n.t('Delegate to agent'), - command: 'issue.startCodingAgentFromTodo', - arguments: [{ document, lineNumber, line, insertIndex, range }], - }); - codeLenses.push(startAgentCodeLens); - } - } - return codeLenses; - } } diff --git a/src/lm/tools/activePullRequestTool.ts b/src/lm/tools/activePullRequestTool.ts index 010c1fb2e4..fbcc258e67 100644 --- a/src/lm/tools/activePullRequestTool.ts +++ b/src/lm/tools/activePullRequestTool.ts @@ -6,28 +6,20 @@ import * as vscode from 'vscode'; import { FetchIssueResult } from './fetchIssueTool'; -import { COPILOT_LOGINS } from '../../common/copilot'; import { GitChangeType, InMemFileChange } from '../../common/file'; -import Logger from '../../common/logger'; import { CommentEvent, EventType, ReviewEvent } from '../../common/timelineEvent'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { PullRequestModel } from '../../github/pullRequestModel'; import { RepositoriesManager } from '../../github/repositoriesManager'; export abstract class PullRequestTool implements vscode.LanguageModelTool { constructor( - protected readonly folderManagers: RepositoriesManager, - private readonly copilotRemoteAgentManager: CopilotRemoteAgentManager + protected readonly folderManagers: RepositoriesManager ) { } protected abstract _findActivePullRequest(): PullRequestModel | undefined; protected abstract _confirmationTitle(): string; - private shouldIncludeCodingAgentSession(pullRequest?: PullRequestModel): boolean { - return !!pullRequest && this.copilotRemoteAgentManager.enabled && COPILOT_LOGINS.includes(pullRequest.author.login); - } - private _getPullRequestLabel(pullRequest: PullRequestModel): string { return `${pullRequest.title} (#${pullRequest.number})`; } @@ -50,87 +42,13 @@ export abstract class PullRequestTool implements vscode.LanguageModelTool line.startsWith('data:')) - .forEach(line => { - try { - const obj = JSON.parse(line.replace(/^data:\s*/, '')); - if (Array.isArray(obj.choices)) { - for (const choice of obj.choices) { - const delta = choice.delta || {}; - if (typeof delta.content === 'string' && !!delta.role) { - result.push(delta.content); - } - } - } - } catch { /* ignore parse errors */ } - }); - - return result; - } - - async fallbackSessionLogs( - pullRequest: PullRequestModel, - model: vscode.LanguageModelChat, - cancellationToken: vscode.CancellationToken - ) { - const logs = await this.copilotRemoteAgentManager.getSessionLogsFromAction(pullRequest); - // Summarize the Copilot agent's thinking process using the model - const messages = [ - vscode.LanguageModelChatMessage.Assistant('You are an expert summarizer. The following logs show the thinking process and performed actions of a GitHub Copilot agent that was in charge of working on the current pull request. Read the logs and always maintain the thinking process. You can remove information on the tool call results that you think are not necessary for building context.'), - vscode.LanguageModelChatMessage.User(`Copilot Agent Logs (JSON):\n${JSON.stringify(logs)}`) - ]; - - let summaryText: string | undefined; - try { - const response = await model.sendRequest(messages, { justification: 'Summarizing Copilot agent logs for the active pull request.' }, cancellationToken); - summaryText = await (typeof response.text === 'string' ? response.text : (typeof response.text?.[Symbol.asyncIterator] === 'function' ? (async () => { let out = ''; for await (const chunk of response.text) { out += chunk; } return out; })() : '')); - } catch (e) { - summaryText = ''; - } - - return summaryText; - } - - async fetchCodingAgentSession( - pullRequest: PullRequestModel, - model: vscode.LanguageModelChat, - token: vscode.CancellationToken - ): Promise { - let copilotSteps: string | string[] = []; - try { - const logs = await this.copilotRemoteAgentManager.getSessionLogFromPullRequest(pullRequest); - if (!logs) { - throw new Error('Could not get session logs'); - } - - copilotSteps = this.parseCopilotEventStream(logs.logs); - if (copilotSteps.length === 0) { - throw new Error('Empty Copilot agent logs received'); - } - } catch (e) { - Logger.debug(`Failed to fetch Copilot agent logs from API: ${e}.`, ActivePullRequestTool.toolId); - copilotSteps = await this.fallbackSessionLogs(pullRequest, model, token); - } - - return copilotSteps; - } - - async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise { + async invoke(_options: vscode.LanguageModelToolInvocationOptions, _token: vscode.CancellationToken): Promise { let pullRequest = this._findActivePullRequest(); if (!pullRequest) { return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart('There is no active pull request')]); } - let codingAgentSession: string | string[] = []; - if (this.shouldIncludeCodingAgentSession(pullRequest) && options.model) { - codingAgentSession = await this.fetchCodingAgentSession(pullRequest, options.model, token); - } - const status = await pullRequest.getStatusChecks(); const timeline = (pullRequest.timelineEvents && pullRequest.timelineEvents.length > 0) ? pullRequest.timelineEvents : await pullRequest.getTimelineEvents(); const pullRequestInfo = { @@ -169,7 +87,6 @@ export abstract class PullRequestTool implements vscode.LanguageModelTool 0, }, isDraft: pullRequest.isDraft ? 'is a draft and cannot be merged until marked as ready for review' : 'false', - codingAgentSession, changes: (await pullRequest.getFileChangesInfo()).map(change => { if (change instanceof InMemFileChange) { return change.diffHunks?.map(hunk => hunk.diffLines.map(line => line.raw).join('\n')).join('\n') || ''; diff --git a/src/lm/tools/copilotRemoteAgentTool.ts b/src/lm/tools/copilotRemoteAgentTool.ts deleted file mode 100644 index 822d2d556a..0000000000 --- a/src/lm/tools/copilotRemoteAgentTool.ts +++ /dev/null @@ -1,154 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as marked from 'marked'; -import * as vscode from 'vscode'; -import { COPILOT_ACCOUNTS } from '../../common/comment'; -import { ITelemetry } from '../../common/telemetry'; -import { toOpenPullRequestWebviewUri } from '../../common/uri'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; -import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; -import { PlainTextRenderer } from '../../github/markdownUtils'; -import { PrsTreeModel } from '../../view/prsTreeModel'; - -export interface CopilotRemoteAgentToolParameters { - // The LLM is inconsistent in providing repo information. - // For now, we only support the active repository in the current workspace. - // repo?: { - // owner?: string; - // name?: string; - // }; - title: string; - body?: string; - existingPullRequest?: string; -} - -export class CopilotRemoteAgentTool implements vscode.LanguageModelTool { - public static readonly toolId = 'github-pull-request_copilot-coding-agent'; - - constructor(private manager: CopilotRemoteAgentManager, private telemetry: ITelemetry, private prsTreeModel: PrsTreeModel) { } - - async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { - const { title, existingPullRequest } = options.input; - const folderManager = existingPullRequest ? undefined : await this.manager.tryPromptForAuthAndRepo(); - - // Check if the coding agent is available (enabled and assignable) - const isAvailable = await this.manager.isAvailable(); - if (!isAvailable) { - throw new Error(vscode.l10n.t('Copilot coding agent is not available for this repository. Make sure the agent is enabled and assignable to this repository.')); - } - - const targetRepo = await this.manager.repoInfo(folderManager); - const autoPushEnabled = this.manager.autoCommitAndPushEnabled; - const openPR = existingPullRequest || await this.getActivePullRequestWithSession(targetRepo); - - /* __GDPR__ - "remoteAgent.tool.prepare" : {} - */ - this.telemetry.sendTelemetryEvent('copilot.remoteAgent.tool.prepare', {}); - - return { - pastTenseMessage: vscode.l10n.t('Launched coding agent'), - invocationMessage: vscode.l10n.t('Launching coding agent'), - confirmationMessages: { - message: openPR - ? vscode.l10n.t('The coding agent will incorporate your feedback on existing pull request **#{0}**.', openPR) - : (targetRepo && autoPushEnabled - ? vscode.l10n.t('The coding agent will continue work on "**{0}**" in a new branch on "**{1}/{2}**". Any uncommitted changes will be **automatically pushed**.', title, targetRepo.owner, targetRepo.repo) - : vscode.l10n.t('The coding agent will start working on "**{0}**"', title)), - title: vscode.l10n.t('Start coding agent?'), - } - }; - } - - async invoke( - options: vscode.LanguageModelToolInvocationOptions, - _: vscode.CancellationToken - ): Promise { - const title = options.input.title; - const body = options.input.body || ''; - const existingPullRequest = options.input.existingPullRequest || ''; - const folderManager = existingPullRequest ? undefined : await this.manager.tryPromptForAuthAndRepo(); - - const targetRepo = await this.manager.repoInfo(folderManager); - if (!targetRepo) { - return new vscode.LanguageModelToolResult([ - new vscode.LanguageModelTextPart(vscode.l10n.t('No repository information found. Please open a workspace with a Git repository.')) - ]); - } - - let pullRequestNumber: number | undefined; - if (existingPullRequest) { - pullRequestNumber = parseInt(existingPullRequest, 10); - if (isNaN(pullRequestNumber)) { - return new vscode.LanguageModelToolResult([ - new vscode.LanguageModelTextPart(vscode.l10n.t('Invalid pull request number: {0}', existingPullRequest)) - ]); - } - } else { - pullRequestNumber = await this.getActivePullRequestWithSession(targetRepo); - } - - /* __GDPR__ - "copilot.remoteAgent.tool.invoke" : { - "hasExistingPR" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, - "hasBody" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryEvent('copilot.remoteAgent.tool.invoke', { - hasExistingPR: pullRequestNumber ? 'true' : 'false', - hasBody: body ? 'true' : 'false' - }); - - if (pullRequestNumber) { - await this.manager.addFollowUpToExistingPR(pullRequestNumber, title, body); - return new vscode.LanguageModelToolResult([ - new vscode.LanguageModelTextPart(vscode.l10n.t('Follow-up added to pull request #{0}.', pullRequestNumber)), - ]); - } - - const result = await this.manager.invokeRemoteAgent(title, body); - if (result.state === 'error') { - /* __GDPR__ - "copilot.remoteAgent.tool.error" : { - "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetry.sendTelemetryErrorEvent('copilot.remoteAgent.tool.error', { reason: 'invocationError' }); - throw new Error(result.error); - } - - let lmResult: (vscode.LanguageModelTextPart | vscode.LanguageModelDataPart)[] = [new vscode.LanguageModelTextPart(result.llmDetails)]; - const pr = await targetRepo.fm.resolvePullRequest(targetRepo.owner, targetRepo.repo, result.number); - if (pr) { - const plaintextBody = marked.parse(pr.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); - const preferredRendering = { - uri: (await toOpenPullRequestWebviewUri({ owner: pr.githubRepository.remote.owner, repo: pr.githubRepository.remote.repositoryName, pullRequestNumber: pr.number })).toString(), - title: pr.title, - description: plaintextBody, - author: COPILOT_ACCOUNTS[pr.author.login].name, - linkTag: `#${pr.number}` - }; - const buffer: Buffer = Buffer.from(JSON.stringify(preferredRendering)); - const data: Uint8Array = Uint8Array.from(buffer); - - // API might not be available for tests, this guarantees we are still able to test - const userAudience = vscode.LanguageModelPartAudience?.User ?? 1; - lmResult.push(new vscode.LanguageModelDataPart2(data, 'application/pull-request+json', [userAudience])); - } - - return new vscode.LanguageModelToolResult2(lmResult); - } - - protected async getActivePullRequestWithSession(repoInfo: { repo: string; owner: string; fm: FolderRepositoryManager } | undefined): Promise { - if (!repoInfo) { - return; - } - const activePR = repoInfo.fm.activePullRequest; - if (activePR && this.prsTreeModel.getCopilotStateForPR(repoInfo.owner, repoInfo.repo, activePR.number)) { - return activePR.number; - } - } -} \ No newline at end of file diff --git a/src/lm/tools/tools.ts b/src/lm/tools/tools.ts index 09c833a7a9..fef9f2ac20 100644 --- a/src/lm/tools/tools.ts +++ b/src/lm/tools/tools.ts @@ -5,13 +5,10 @@ 'use strict'; import * as vscode from 'vscode'; -import { ITelemetry } from '../../common/telemetry'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { CredentialStore } from '../../github/credentials'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { ChatParticipantState } from '../participants'; import { ActivePullRequestTool } from './activePullRequestTool'; -import { CopilotRemoteAgentTool } from './copilotRemoteAgentTool'; import { DisplayIssuesTool } from './displayIssuesTool'; import { FetchIssueTool } from './fetchIssueTool'; import { FetchNotificationTool } from './fetchNotificationTool'; @@ -20,16 +17,14 @@ import { ConvertToSearchSyntaxTool, SearchTool } from './searchTools'; import { SuggestFixTool } from './suggestFixTool'; import { IssueSummarizationTool } from './summarizeIssueTool'; import { NotificationSummarizationTool } from './summarizeNotificationsTool'; -import { PrsTreeModel } from '../../view/prsTreeModel'; -export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState, copilotRemoteAgentManager: CopilotRemoteAgentManager, telemetry: ITelemetry, prsTreeModel: PrsTreeModel) { +export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { registerFetchingTools(context, credentialStore, repositoriesManager, chatParticipantState); registerSummarizationTools(context); registerSuggestFixTool(context, credentialStore, repositoriesManager, chatParticipantState); registerSearchTools(context, credentialStore, repositoriesManager, chatParticipantState); - registerCopilotAgentTools(context, copilotRemoteAgentManager, telemetry, prsTreeModel); - context.subscriptions.push(vscode.lm.registerTool(ActivePullRequestTool.toolId, new ActivePullRequestTool(repositoriesManager, copilotRemoteAgentManager))); - context.subscriptions.push(vscode.lm.registerTool(OpenPullRequestTool.toolId, new OpenPullRequestTool(repositoriesManager, copilotRemoteAgentManager))); + context.subscriptions.push(vscode.lm.registerTool(ActivePullRequestTool.toolId, new ActivePullRequestTool(repositoriesManager))); + context.subscriptions.push(vscode.lm.registerTool(OpenPullRequestTool.toolId, new OpenPullRequestTool(repositoriesManager))); } function registerFetchingTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { @@ -46,10 +41,6 @@ function registerSuggestFixTool(context: vscode.ExtensionContext, credentialStor context.subscriptions.push(vscode.lm.registerTool(SuggestFixTool.toolId, new SuggestFixTool(credentialStore, repositoriesManager, chatParticipantState))); } -function registerCopilotAgentTools(context: vscode.ExtensionContext, copilotRemoteAgentManager: CopilotRemoteAgentManager, telemetry: ITelemetry, prsTreeModel: PrsTreeModel) { - context.subscriptions.push(vscode.lm.registerTool(CopilotRemoteAgentTool.toolId, new CopilotRemoteAgentTool(copilotRemoteAgentManager, telemetry, prsTreeModel))); -} - function registerSearchTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { context.subscriptions.push(vscode.lm.registerTool(ConvertToSearchSyntaxTool.toolId, new ConvertToSearchSyntaxTool(credentialStore, repositoriesManager, chatParticipantState))); context.subscriptions.push(vscode.lm.registerTool(SearchTool.toolId, new SearchTool(credentialStore, repositoriesManager, chatParticipantState))); diff --git a/src/test/github/copilotRemoteAgent.test.ts b/src/test/github/copilotRemoteAgent.test.ts deleted file mode 100644 index f0618b8bb1..0000000000 --- a/src/test/github/copilotRemoteAgent.test.ts +++ /dev/null @@ -1,344 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { default as assert } from 'assert'; -import { SinonSandbox, createSandbox } from 'sinon'; -import * as vscode from 'vscode'; -import { CopilotRemoteAgentManager, SessionIdForPr } from '../../github/copilotRemoteAgent'; -import { MockCommandRegistry } from '../mocks/mockCommandRegistry'; -import { MockTelemetry } from '../mocks/mockTelemetry'; -import { CredentialStore } from '../../github/credentials'; -import { RepositoriesManager } from '../../github/repositoriesManager'; -import { MockExtensionContext } from '../mocks/mockExtensionContext'; -import { PullRequestModel } from '../../github/pullRequestModel'; -import { MockGitHubRepository } from '../mocks/mockGitHubRepository'; -import { GitHubRemote } from '../../common/remote'; -import { Protocol } from '../../common/protocol'; -import { GitHubServerType } from '../../common/authentication'; -import { ReposManagerState } from '../../github/folderRepositoryManager'; -import { GitApiImpl } from '../../api/api1'; -import { MockPrsTreeModel } from '../mocks/mockPRsTreeModel'; -import { PrsTreeModel } from '../../view/prsTreeModel'; -import { COPILOT_SWE_AGENT } from '../../common/copilot'; - -const telemetry = new MockTelemetry(); -const protocol = new Protocol('https://github.com/github/test.git'); -const remote = new GitHubRemote('test', 'github/test', protocol, GitHubServerType.GitHubDotCom); - -describe('CopilotRemoteAgentManager', function () { - let sinon: SinonSandbox; - let manager: CopilotRemoteAgentManager; - let credentialStore: CredentialStore; - let reposManager: RepositoriesManager; - let context: MockExtensionContext; - let mockRepo: MockGitHubRepository; - let gitAPIImp: GitApiImpl; - let mockPrsTreeModel: MockPrsTreeModel; - - beforeEach(function () { - sinon = createSandbox(); - MockCommandRegistry.install(sinon); - - // Mock workspace configuration to return disabled by default - sinon.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => ({ - get: sinon.stub().callsFake((key: string, defaultValue?: any) => { - if (section === 'githubPR.copilotRemoteAgent' && key === 'enabled') { - return false; // Default to disabled - } - if (section === 'githubPR.copilotRemoteAgent' && key === 'autoCommitAndPushEnabled') { - return false; - } - if (section === 'githubPR.copilotRemoteAgent' && key === 'promptForConfirmation') { - return true; - } - return defaultValue; - }), - update: sinon.stub().resolves(), - has: sinon.stub().returns(true), - inspect: sinon.stub() - } as any)); - - context = new MockExtensionContext(); - credentialStore = new CredentialStore(telemetry, context); - reposManager = new RepositoriesManager(credentialStore, telemetry); - - mockRepo = new MockGitHubRepository(remote, credentialStore, telemetry, sinon); - - gitAPIImp = new GitApiImpl(reposManager); - - mockPrsTreeModel = new MockPrsTreeModel(); - manager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, gitAPIImp, mockPrsTreeModel as unknown as PrsTreeModel); - }); - - afterEach(function () { - manager.dispose(); - reposManager.dispose(); - credentialStore.dispose(); - context.dispose(); - mockRepo.dispose(); - sinon.restore(); - }); - - describe('enabled', function () { - it('should return false when coding agent is disabled by default', function () { - // The config should default to disabled state - const enabled = manager.enabled; - assert.strictEqual(enabled, false); - }); - - it('should reflect configuration changes', function () { - // Test would require mocking workspace configuration - // For now, just test the getter exists - assert.strictEqual(typeof manager.enabled, 'boolean'); - }); - }); - - describe('autoCommitAndPushEnabled', function () { - it('should return boolean value', function () { - const autoCommitEnabled = manager.autoCommitAndPushEnabled; - assert.strictEqual(typeof autoCommitEnabled, 'boolean'); - }); - }); - - describe('isAssignable()', function () { - it('should return false when no repository info is available', async function () { - // No folder managers setup - const result = await manager.isAssignable(); - assert.strictEqual(result, false); - }); - - it('should return false when assignable users cannot be fetched', async function () { - // Mock repository manager state but no assignable users - sinon.stub(manager, 'repoInfo').resolves(undefined); - - const result = await manager.isAssignable(); - assert.strictEqual(result, false); - }); - }); - - describe('isAvailable()', function () { - it('should return false when manager is disabled', async function () { - sinon.stub(manager, 'enabled').get(() => false); - - const result = await manager.isAvailable(); - assert.strictEqual(result, false); - }); - - it('should return false when no repo info is available', async function () { - sinon.stub(manager, 'enabled').get(() => true); - sinon.stub(manager, 'repoInfo').resolves(undefined); - - const result = await manager.isAvailable(); - assert.strictEqual(result, false); - }); - - it('should return false when copilot API is not available', async function () { - sinon.stub(manager, 'enabled').get(() => true); - sinon.stub(manager, 'repoInfo').resolves({ - owner: 'test', - repo: 'test', - baseRef: 'main', - remote: remote, - repository: {} as any, - ghRepository: mockRepo, - fm: {} as any - }); - // copilotApi will return undefined by default in tests - - const result = await manager.isAvailable(); - assert.strictEqual(result, false); - }); - }); - - describe('repoInfo()', function () { - it('should return undefined when no folder managers exist', async function () { - const result = await manager.repoInfo(); - assert.strictEqual(result, undefined); - }); - - it('should return undefined when no repository is found', async function () { - // Mock empty folder managers - sinon.stub(reposManager, 'folderManagers').get(() => []); - - const result = await manager.repoInfo(); - assert.strictEqual(result, undefined); - }); - }); - - describe('addFollowUpToExistingPR()', function () { - it('should return undefined when no repo info is available', async function () { - sinon.stub(manager, 'repoInfo').resolves(undefined); - - const result = await manager.addFollowUpToExistingPR(123, 'test prompt'); - assert.strictEqual(result, undefined); - }); - - it('should return undefined when PR is not found', async function () { - sinon.stub(manager, 'repoInfo').resolves({ - owner: 'test', - repo: 'test', - baseRef: 'main', - remote: remote, - repository: {} as any, - ghRepository: mockRepo, - fm: {} as any - }); - - sinon.stub(mockRepo, 'getPullRequest').resolves(undefined); - - const result = await manager.addFollowUpToExistingPR(123, 'test prompt'); - assert.strictEqual(result, undefined); - }); - }); - - describe('invokeRemoteAgent()', function () { - it('should return error when copilot API is not available', async function () { - const result = await manager.invokeRemoteAgent('test prompt', 'test context'); - - assert.strictEqual(result.state, 'error'); - if (result.state === 'error') { - assert(result.error.includes('Failed to initialize Copilot API')); - } - }); - - it('should return error when no repository info is available', async function () { - // Mock copilot API to be available but no repo info - sinon.stub(manager as any, '_copilotApiPromise').value(Promise.resolve({} as any)); - sinon.stub(manager, 'repoInfo').resolves(undefined); - - const result = await manager.invokeRemoteAgent('test prompt', 'test context'); - - assert.strictEqual(result.state, 'error'); - }); - }); - - describe('getSessionLogsFromAction()', function () { - it('should return empty array when copilot API is not available', async function () { - const mockPr = {} as PullRequestModel; - - const result = await manager.getSessionLogsFromAction(mockPr); - - assert.strictEqual(Array.isArray(result), true); - assert.strictEqual(result.length, 0); - }); - }); - - describe('getWorkflowStepsFromAction()', function () { - it('should return empty array when no workflow run is found', async function () { - const mockPr = {} as PullRequestModel; - sinon.stub(manager, 'getLatestCodingAgentFromAction').resolves(undefined); - - const result = await manager.getWorkflowStepsFromAction(mockPr); - - assert.strictEqual(Array.isArray(result), true); - assert.strictEqual(result.length, 0); - }); - }); - - describe('getSessionLogFromPullRequest()', function () { - it('should return undefined when copilot API is not available', async function () { - const mockPr = {} as PullRequestModel; - - const result = await manager.getSessionLogFromPullRequest(mockPr); - - assert.strictEqual(result, undefined); - }); - }); - - describe('provideChatSessions()', function () { - it('should return empty array when copilot API is not available', async function () { - const token = new vscode.CancellationTokenSource().token; - - const result = await manager.provideChatSessions(token); - - assert.strictEqual(Array.isArray(result), true); - assert.strictEqual(result.length, 0); - }); - - it('should return empty array when cancellation is requested', async function () { - const tokenSource = new vscode.CancellationTokenSource(); - tokenSource.cancel(); - - const result = await manager.provideChatSessions(tokenSource.token); - - assert.strictEqual(Array.isArray(result), true); - assert.strictEqual(result.length, 0); - }); - }); - - describe('provideChatSessionContent()', function () { - it('should return empty session when copilot API is not available', async function () { - const token = new vscode.CancellationTokenSource().token; - - const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), token); - - assert.strictEqual(Array.isArray(result.history), true); - assert.strictEqual(result.history.length, 0); - assert.strictEqual(result.requestHandler, undefined); - }); - - it('should return empty session when cancellation is requested', async function () { - const tokenSource = new vscode.CancellationTokenSource(); - tokenSource.cancel(); - - const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), tokenSource.token); - - assert.strictEqual(Array.isArray(result.history), true); - assert.strictEqual(result.history.length, 0); - }); - - it('should return empty session for invalid PR number', async function () { - const token = new vscode.CancellationTokenSource().token; - - const result = await manager.provideChatSessionContent(vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/invalid' }), token); - - assert.strictEqual(Array.isArray(result.history), true); - assert.strictEqual(result.history.length, 0); - }); - }); - - describe('event handlers', function () { - it('should expose onDidChangeStates event', function () { - assert.strictEqual(typeof manager.onDidChangeStates, 'function'); - }); - - it('should expose onDidChangeNotifications event', function () { - assert.strictEqual(typeof manager.onDidChangeNotifications, 'function'); - }); - - it('should expose onDidCreatePullRequest event', function () { - assert.strictEqual(typeof manager.onDidCreatePullRequest, 'function'); - }); - - it('should expose onDidChangeChatSessions event', function () { - assert.strictEqual(typeof manager.onDidChangeChatSessions, 'function'); - }); - }); - - describe('waitRepoManagerInitialization()', function () { - it('should resolve immediately when repos are loaded', async function () { - // Mock the state as already loaded - sinon.stub(reposManager, 'state').get(() => ReposManagerState.RepositoriesLoaded); - - // This should resolve quickly - const startTime = Date.now(); - await (manager as any).waitRepoManagerInitialization(); - const endTime = Date.now(); - - // Should be very fast since it should return immediately - assert(endTime - startTime < 100); - }); - - it('should resolve immediately when authentication is needed', async function () { - sinon.stub(reposManager, 'state').get(() => ReposManagerState.NeedsAuthentication); - - const startTime = Date.now(); - await (manager as any).waitRepoManagerInitialization(); - const endTime = Date.now(); - - assert(endTime - startTime < 100); - }); - }); -}); diff --git a/src/test/github/copilotRemoteAgentUtils.test.ts b/src/test/github/copilotRemoteAgentUtils.test.ts deleted file mode 100644 index 1008fcfc2b..0000000000 --- a/src/test/github/copilotRemoteAgentUtils.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { default as assert } from 'assert'; -import { truncatePrompt, extractTitle, formatBodyPlaceholder } from '../../github/copilotRemoteAgentUtils'; -import { MAX_PROBLEM_STATEMENT_LENGTH } from '../../github/copilotApi'; - -describe('copilotRemoteAgentUtils', () => { - - describe('truncatePrompt', () => { - it('should return prompt and context unchanged when under limit', () => { - const prompt = 'This is a short prompt'; - const context = 'This is some additional context'; - const result = truncatePrompt(prompt, context); - assert.strictEqual(result.problemStatement, `${prompt}\n\n${context}`); - assert.strictEqual(result.isTruncated, false); - }); - - it('should return only prompt when no context provided and under limit', () => { - const prompt = 'This is a short prompt'; - const result = truncatePrompt(prompt); - assert.strictEqual(result.problemStatement, 'This is a short prompt'); - assert.strictEqual(result.isTruncated, false); - }); - - it('should truncate prompt when it exceeds the maximum length', () => { - const longPrompt = 'a'.repeat(MAX_PROBLEM_STATEMENT_LENGTH + 100); - const result = truncatePrompt(longPrompt); - assert.strictEqual(result.problemStatement.length, MAX_PROBLEM_STATEMENT_LENGTH); - assert.strictEqual(result.isTruncated, true); - }); - - it('should truncate context when combined length exceeds limit', () => { - const prompt = 'Short prompt'; - const longContext = 'b'.repeat(MAX_PROBLEM_STATEMENT_LENGTH); - - const result = truncatePrompt(prompt, longContext); - - assert.strictEqual(result.isTruncated, true); - assert(result.problemStatement.startsWith(prompt)); - assert(result.problemStatement.includes('\n\n')); - const expectedAvailableLength = MAX_PROBLEM_STATEMENT_LENGTH - prompt.length; - const expectedContext = longContext.slice(-expectedAvailableLength + 2); - assert.strictEqual(result.problemStatement, `${prompt}\n\n${expectedContext}`); - }); - - it('long prompts are prioritized when truncating', () => { - const longPrompt = 'a'.repeat(MAX_PROBLEM_STATEMENT_LENGTH + 100); - const context = 'B'; - - const result = truncatePrompt(longPrompt, context); - - assert.strictEqual(result.isTruncated, true); - assert.strictEqual(result.problemStatement.length, MAX_PROBLEM_STATEMENT_LENGTH); - assert(!result.problemStatement.includes(context)); - }); - }); - - describe('extractTitle', () => { - it('should extract title from context with TITLE prefix', () => { - const context = 'Some initial text\nTITLE: Fix authentication bug\nSome other content'; - const result = extractTitle('', context); - assert.strictEqual(result, 'Fix authentication bug'); - }); - - it('should extract title with case insensitive matching', () => { - const context = 'Some text\ntitle: Add new feature\nMore text'; - const result = extractTitle('', context); - assert.strictEqual(result, 'Add new feature'); - }); - - it('should extract title with extra whitespace', () => { - const context = 'TITLE: Refactor code structure \n'; - const result = extractTitle('', context); - assert.strictEqual(result, 'Refactor code structure'); - }); - - it('should use prompt when no title is found', () => { - const context = 'Some text without any title marker\nJust regular content'; - const result = extractTitle('Default Title', context); - assert.strictEqual(result, 'Default Title'); - }); - - it('should use prompt when context is undefined', () => { - const result = extractTitle('Default Title', undefined); - assert.strictEqual(result, 'Default Title'); - }); - - it('should return truncated title when context is empty string', () => { - const title = 'TEST TEST TEST TEST TEST TEST'; // will truncate to 20 characters - const result = extractTitle(title, ''); - - assert.strictEqual(result, 'TEST TEST TEST TEST ...'); - }); - }); -}); \ No newline at end of file diff --git a/src/test/issues/issueTodoProvider.test.ts b/src/test/issues/issueTodoProvider.test.ts index 1551294b82..89555f8278 100644 --- a/src/test/issues/issueTodoProvider.test.ts +++ b/src/test/issues/issueTodoProvider.test.ts @@ -3,11 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { default as assert } from 'assert'; import * as vscode from 'vscode'; -import { IssueTodoProvider } from '../../issues/issueTodoProvider'; import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; -import { CODING_AGENT, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../../common/settingKeys'; import * as issueUtil from '../../issues/util'; const mockCopilotManager: Partial = { @@ -32,155 +29,4 @@ describe('IssueTodoProvider', function () { after(() => { (issueUtil as any).isComment = originalIsComment; }); - - it('should provide both actions when CopilotRemoteAgentManager is available', async function () { - const mockContext = { - subscriptions: [] - } as any as vscode.ExtensionContext; - - - const provider = new IssueTodoProvider(mockContext, mockCopilotManager as CopilotRemoteAgentManager); - - // Create a mock document with TODO comment - const document = { - lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }), - lineCount: 4 - } as vscode.TextDocument; - - const range = new vscode.Range(1, 0, 1, 20); - const context = { - only: vscode.CodeActionKind.QuickFix - } as vscode.CodeActionContext; - - const actions = await provider.provideCodeActions(document, range, context, new vscode.CancellationTokenSource().token); - - assert.strictEqual(actions.length, 2); - - // Find the actions - const createIssueAction = actions.find(a => a.title === 'Create GitHub Issue'); - const startAgentAction = actions.find(a => a.title === 'Delegate to agent'); - - assert.ok(createIssueAction, 'Should have Create GitHub Issue action'); - assert.ok(startAgentAction, 'Should have Delegate to agent action'); - - assert.strictEqual(createIssueAction?.command?.command, 'issue.createIssueFromSelection'); - assert.strictEqual(startAgentAction?.command?.command, 'issue.startCodingAgentFromTodo'); - }); - - it('should provide code lenses for TODO comments', async function () { - const mockContext = { - subscriptions: [] - } as any as vscode.ExtensionContext; - - const provider = new IssueTodoProvider(mockContext, mockCopilotManager as CopilotRemoteAgentManager); - - // Create a mock document with TODO comment - const document = { - lineAt: (line: number) => ({ - text: line === 1 ? ' // TODO: Fix this' : 'function test() {}' - }), - lineCount: 4 - } as vscode.TextDocument; - - const originalGetConfiguration = vscode.workspace.getConfiguration; - vscode.workspace.getConfiguration = (section?: string) => { - if (section === ISSUES_SETTINGS_NAMESPACE) { - return { - get: (key: string, defaultValue?: any) => { - if (key === CREATE_ISSUE_TRIGGERS) { - return ['TODO', 'todo', 'BUG', 'FIXME', 'ISSUE', 'HACK']; - } - return defaultValue; - } - } as any; - } else if (section === CODING_AGENT) { - return { - get: (key: string, defaultValue?: any) => { - if (key === SHOW_CODE_LENS) { - return true; - } - return defaultValue; - } - } as any; - } - return originalGetConfiguration(section); - }; - - try { - // Update triggers to ensure the expression is set - (provider as any).updateTriggers(); - - const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); - - assert.strictEqual(codeLenses.length, 1); - - // Verify the code lenses - const startAgentLens = codeLenses.find(cl => cl.command?.title === 'Delegate to agent'); - - assert.ok(startAgentLens, 'Should have Delegate to agent CodeLens'); - - assert.strictEqual(startAgentLens?.command?.command, 'issue.startCodingAgentFromTodo'); - - // Verify the range points to the TODO text - assert.strictEqual(startAgentLens?.range.start.line, 1); - } finally { - // Restore original configuration - vscode.workspace.getConfiguration = originalGetConfiguration; - } - }); - - it('should not provide code lenses when codeLens setting is disabled', async function () { - const mockContext = { - subscriptions: [] - } as any as vscode.ExtensionContext; - - const provider = new IssueTodoProvider(mockContext, mockCopilotManager as CopilotRemoteAgentManager); - - // Create a mock document with TODO comment - const document = { - lineAt: (lineNo: number) => ({ - text: lineNo === 1 ? ' // TODO: Fix this' : 'function test() {}', - firstNonWhitespaceCharacterIndex: lineNo === 1 ? 2 : 0, - } as vscode.TextLine), - lineCount: 4, - languageId: 'javascript' - } as vscode.TextDocument; - - const originalGetConfiguration = vscode.workspace.getConfiguration; - vscode.workspace.getConfiguration = (section?: string) => { - if (section === ISSUES_SETTINGS_NAMESPACE) { - return { - get: (key: string, defaultValue?: any) => { - if (key === CREATE_ISSUE_TRIGGERS) { - return ['TODO', 'todo', 'BUG', 'FIXME', 'ISSUE', 'HACK']; - } - return defaultValue; - } - } as any; - } else if (section === CODING_AGENT) { - return { - get: (key: string, defaultValue?: any) => { - if (key === SHOW_CODE_LENS) { - return false; - } - return defaultValue; - } - } as any; - } - return originalGetConfiguration(section); - }; - - try { - // Update triggers to ensure the expression is set - (provider as any).updateTriggers(); - - const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); - - // Should return empty array when CodeLens is disabled - assert.strictEqual(codeLenses.length, 0, 'Should not provide code lenses when setting is disabled'); - } finally { - // Restore original configuration - vscode.workspace.getConfiguration = originalGetConfiguration; - } - }); }); \ No newline at end of file diff --git a/src/test/lm/tools/copilotRemoteAgentTool.test.ts b/src/test/lm/tools/copilotRemoteAgentTool.test.ts deleted file mode 100644 index 26440130c8..0000000000 --- a/src/test/lm/tools/copilotRemoteAgentTool.test.ts +++ /dev/null @@ -1,384 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { default as assert } from 'assert'; -import { SinonSandbox, createSandbox } from 'sinon'; -import * as vscode from 'vscode'; -import { CopilotRemoteAgentTool, CopilotRemoteAgentToolParameters } from '../../../lm/tools/copilotRemoteAgentTool'; -import { CopilotRemoteAgentManager } from '../../../github/copilotRemoteAgent'; -import { MockTelemetry } from '../../mocks/mockTelemetry'; -import { RemoteAgentResult } from '../../../github/common'; -import { MockPrsTreeModel } from '../../mocks/mockPRsTreeModel'; -import { PrsTreeModel } from '../../../view/prsTreeModel'; -import { FolderRepositoryManager } from '../../../github/folderRepositoryManager'; - -class TestCopilotRemoteAgentTool extends CopilotRemoteAgentTool { - publicGetActivePullRequestWithSession(repoInfo?: { repo: string; owner: string; fm: FolderRepositoryManager }): Promise { - return this.getActivePullRequestWithSession(repoInfo); - } -} - -describe('CopilotRemoteAgentTool', function () { - let sinon: SinonSandbox; - let tool: TestCopilotRemoteAgentTool; - let mockManager: sinon.SinonStubbedInstance; - let telemetry: MockTelemetry; - let mockPrsTreeModel: PrsTreeModel; - - beforeEach(function () { - sinon = createSandbox(); - telemetry = new MockTelemetry(); - mockManager = sinon.createStubInstance(CopilotRemoteAgentManager); - - // Mock the VSCode Language Model API that may not be available in test environment - if (!(vscode as any).LanguageModelPartAudience) { - (vscode as any).LanguageModelPartAudience = { - Assistant: 0, - User: 1, - Extension: 2 - }; - } - mockPrsTreeModel = new MockPrsTreeModel() as unknown as PrsTreeModel; - - tool = new TestCopilotRemoteAgentTool(mockManager as any, telemetry, mockPrsTreeModel); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe('toolId', function () { - it('should have the correct tool ID', function () { - assert.strictEqual(CopilotRemoteAgentTool.toolId, 'github-pull-request_copilot-coding-agent'); - }); - }); - - describe('prepareInvocation()', function () { - const mockInput: CopilotRemoteAgentToolParameters = { - title: 'Test PR Title', - body: 'Test PR body', - }; - - it('should throw error when coding agent is not available', async function () { - mockManager.isAvailable.resolves(false); - - const options = { input: mockInput } as any; - - await assert.rejects( - async () => await tool.prepareInvocation(options), - /Copilot coding agent is not available/ - ); - }); - - it('should prepare invocation for new PR when agent is available', async function () { - mockManager.isAvailable.resolves(true); - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test-repo', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: {} as any - }); - sinon.stub(mockManager, 'autoCommitAndPushEnabled').get(() => true); - - const options = { input: mockInput } as any; - - const result = await tool.prepareInvocation(options); - - assert.strictEqual(result.pastTenseMessage, 'Launched coding agent'); - assert.strictEqual(result.invocationMessage, 'Launching coding agent'); - // Handle both string and MarkdownString types - const message = result.confirmationMessages?.message; - const messageText = typeof message === 'string' ? message : message?.value || ''; - assert(messageText.includes('test/test-repo')); - assert(messageText.includes('automatically pushed')); - }); - - it('should prepare invocation for existing PR', async function () { - mockManager.isAvailable.resolves(true); - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: {} as any - }); - - // Mock the config getter to avoid access issues - Object.defineProperty(mockManager, 'autoCommitAndPushEnabled', { - get: () => false - }); - - const inputWithExistingPR: CopilotRemoteAgentToolParameters = { - title: 'Test PR Title', - existingPullRequest: '123', - }; - - const options = { input: inputWithExistingPR } as any; - - const result = await tool.prepareInvocation(options); - - // Handle both string and MarkdownString types - const message = result.confirmationMessages?.message; - const messageText = typeof message === 'string' ? message : message?.value || ''; - assert(messageText.includes('existing pull request **#123**')); - }); - - it('should handle active PR with session', async function () { - mockManager.isAvailable.resolves(true); - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test-repo', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: { - activePullRequest: { number: 123 } as any - } as any - }); - - // Mock the config getter to avoid access issues - Object.defineProperty(mockManager, 'autoCommitAndPushEnabled', { - get: () => false - }); - - const options = { input: mockInput } as any; - - const result = await tool.prepareInvocation(options); - - // Handle both string and MarkdownString types - const message = result.confirmationMessages?.message; - const messageText = typeof message === 'string' ? message : message?.value || ''; - assert(messageText.includes('existing pull request **#123**')); - }); - }); - - describe('invoke()', function () { - const mockInput: CopilotRemoteAgentToolParameters = { - title: 'Test PR Title', - body: 'Test PR body', - }; - - const mockOptions = { - input: mockInput - } as any; - - const mockToken = new vscode.CancellationTokenSource().token; - - it('should return error when no repository information is found', async function () { - mockManager.repoInfo.resolves(undefined); - - const result = await tool.invoke(mockOptions, mockToken); - - assert(result); - - // VSCode wraps the result with content array - if ((result as any).content && Array.isArray((result as any).content)) { - const content = (result as any).content; - const textValue = content[0]?.value || content[0]?.text || ''; - assert(textValue.includes('No repository information found') || textValue.includes('repository information')); - } else { - // Check that it returns a text result with error message - const resultParts = (result as any)._parts || (result as any).parts; - assert(Array.isArray(resultParts)); - assert(resultParts.length > 0); - const firstPart = resultParts[0]; - assert(firstPart.value?.includes('No repository information found') || firstPart.text?.includes('No repository information found')); - } - }); - - it('should return error for invalid existing PR number', async function () { - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: {} as any - }); - - const invalidInput = { - ...mockInput, - existingPullRequest: 'invalid' - }; - - const result = await tool.invoke({ input: invalidInput } as any, mockToken); - - assert(result); - - // VSCode wraps the result with content array - if ((result as any).content && Array.isArray((result as any).content)) { - const content = (result as any).content; - const textValue = content[0]?.value || content[0]?.text || ''; - assert(textValue.includes('Invalid pull request number') || textValue.includes('invalid')); - } else { - const resultParts = (result as any)._parts || (result as any).parts; - assert(Array.isArray(resultParts)); - assert(resultParts.length > 0); - const firstPart = resultParts[0]; - assert(firstPart.value?.includes('Invalid pull request number') || firstPart.text?.includes('Invalid pull request number')); - } - }); - - it('should add follow-up to existing PR', async function () { - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test-repo', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: {} as any - }); - mockManager.addFollowUpToExistingPR.resolves('Follow-up added'); - - const inputWithExistingPR: CopilotRemoteAgentToolParameters = { - title: 'Test PR Title', - existingPullRequest: '123', - }; - - const optionsWithExistingPR = { - input: inputWithExistingPR - } as any; - - const result = await tool.invoke(optionsWithExistingPR, mockToken); - - assert(result); - - // VSCode wraps the result with content array - if ((result as any).content && Array.isArray((result as any).content)) { - const content = (result as any).content; - const textValue = content[0]?.value || content[0]?.text || ''; - assert(textValue.includes('Follow-up added to pull request #123') || textValue.includes('follow-up') || textValue.includes('Follow-up added')); - } else { - const resultParts = (result as any)._parts || (result as any).parts; - assert(Array.isArray(resultParts)); - const firstPart = resultParts[0]; - assert(firstPart.value?.includes('Follow-up added to pull request #123') || firstPart.text?.includes('Follow-up added to pull request #123')); - } - }); - - it('should invoke remote agent for new PR successfully', async function () { - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test-repo', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: { - resolvePullRequest: sinon.stub().resolves({ - number: 789, - title: 'Test PR', - body: 'Test body', - author: { login: 'copilot-swe-agent' }, - githubRepository: { - remote: { - owner: 'test', - repositoryName: 'test-repo' - } - } - }) - } as any - }); - - const successResult: RemoteAgentResult = { - state: 'success', - number: 789, - link: 'https://github.com/test/test-repo/pull/789', - webviewUri: vscode.Uri.parse('https://example.com'), - llmDetails: 'Agent created PR successfully', - sessionId: '123-456' - }; - - mockManager.invokeRemoteAgent.resolves(successResult); - - const result = await tool.invoke(mockOptions, mockToken); - - assert(result); - const resultParts = (result as any).content; - assert(Array.isArray(resultParts)); - assert(resultParts.length >= 1); - const firstPart = resultParts[0]; - assert(firstPart.value?.includes('Agent created PR successfully') || firstPart.text?.includes('Agent created PR successfully')); - }); - - it('should throw error when invocation fails', async function () { - mockManager.repoInfo.resolves({ - owner: 'test', - repo: 'test-repo', - baseRef: 'main', - remote: {} as any, - repository: {} as any, - ghRepository: {} as any, - fm: {} as any - }); - - const errorResult: RemoteAgentResult = { - state: 'error', - error: 'Something went wrong' - }; - - mockManager.invokeRemoteAgent.resolves(errorResult); - - await assert.rejects( - async () => await tool.invoke(mockOptions, mockToken), - /Something went wrong/ - ); - }); - }); - - describe('publicGetActivePullRequestWithSession()', function () { - it('should return undefined when no repo info is provided', async function () { - const result = await tool.publicGetActivePullRequestWithSession(undefined); - assert.strictEqual(result, undefined); - }); - - it('should return undefined when no active PR exists', async function () { - const repoInfo = { - owner: 'test', - repo: 'test-repo', - fm: { - activePullRequest: undefined - } as FolderRepositoryManager - }; - - const result = await tool.publicGetActivePullRequestWithSession(repoInfo); - assert.strictEqual(result, undefined); - }); - - it('should return undefined when active PR has no copilot state', async function () { - const repoInfo = { - owner: 'test', - repo: 'test-repo', - fm: { - activePullRequest: { number: 456 } - } as FolderRepositoryManager - }; - - const result = await tool.publicGetActivePullRequestWithSession(repoInfo); - assert.strictEqual(result, undefined); - }); - - it('should return PR number when active PR has copilot state', async function () { - const repoInfo = { - owner: 'test', - repo: 'test-repo', - fm: { - activePullRequest: { number: 123 } - } as FolderRepositoryManager - }; - - const result = await tool.publicGetActivePullRequestWithSession(repoInfo); - assert.strictEqual(result, 123); - }); - }); -}); diff --git a/src/test/view/prsTree.test.ts b/src/test/view/prsTree.test.ts index fc98daf733..f6aa290541 100644 --- a/src/test/view/prsTree.test.ts +++ b/src/test/view/prsTree.test.ts @@ -20,7 +20,7 @@ import { MockCommandRegistry } from '../mocks/mockCommandRegistry'; import { MockGitHubRepository } from '../mocks/mockGitHubRepository'; import { PullRequestGitHelper } from '../../github/pullRequestGitHelper'; import { PullRequestModel } from '../../github/pullRequestModel'; -import { GitHubRemote, Remote } from '../../common/remote'; +import { GitHubRemote } from '../../common/remote'; import { Protocol } from '../../common/protocol'; import { CredentialStore, GitHub } from '../../github/credentials'; import { parseGraphQLPullRequest } from '../../github/utils'; @@ -32,9 +32,7 @@ import { DataUri } from '../../common/uri'; import { IAccount, ITeam } from '../../github/interface'; import { asPromise } from '../../common/utils'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; -import { MockPrsTreeModel } from '../mocks/mockPRsTreeModel'; import { PrsTreeModel } from '../../view/prsTreeModel'; describe('GitHub Pull Requests view', function () { @@ -45,9 +43,7 @@ describe('GitHub Pull Requests view', function () { let credentialStore: CredentialStore; let reposManager: RepositoriesManager; let createPrHelper: CreatePullRequestHelper; - let copilotManager: CopilotRemoteAgentManager; let mockThemeWatcher: MockThemeWatcher; - let gitAPI: GitApiImpl; let mockNotificationsManager: MockNotificationManager; let prsTreeModel: PrsTreeModel; @@ -65,9 +61,7 @@ describe('GitHub Pull Requests view', function () { ); prsTreeModel = new PrsTreeModel(telemetry, reposManager, context); credentialStore = new CredentialStore(telemetry, context); - gitAPI = new GitApiImpl(reposManager); - copilotManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, gitAPI, prsTreeModel); - provider = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotManager); + provider = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager); mockNotificationsManager = new MockNotificationManager(); createPrHelper = new CreatePullRequestHelper(); diff --git a/src/test/view/reviewCommentController.test.ts b/src/test/view/reviewCommentController.test.ts index ba473d2bd8..b6afcd4695 100644 --- a/src/test/view/reviewCommentController.test.ts +++ b/src/test/view/reviewCommentController.test.ts @@ -20,7 +20,7 @@ import { PullRequestBuilder } from '../builders/rest/pullRequestBuilder'; import { convertRESTPullRequestToRawPullRequest } from '../../github/utils'; import { IResolvedPullRequestModel, PullRequestModel } from '../../github/pullRequestModel'; import { Protocol } from '../../common/protocol'; -import { GitHubRemote, Remote } from '../../common/remote'; +import { GitHubRemote } from '../../common/remote'; import { GHPRCommentThread } from '../../github/prComment'; import { DiffLine } from '../../common/diffHunk'; import { MockGitHubRepository } from '../mocks/mockGitHubRepository'; @@ -37,7 +37,6 @@ import { GitHubServerType } from '../../common/authentication'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; import { mergeQuerySchemaWithShared } from '../../github/common'; import { AccountType } from '../../github/interface'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; import { asPromise } from '../../common/utils'; import { PrsTreeModel } from '../../view/prsTreeModel'; @@ -65,7 +64,6 @@ describe('ReviewCommentController', function () { let reviewManager: ReviewManager; let reposManager: RepositoriesManager; let gitApiImpl: GitApiImpl; - let copilotManager: CopilotRemoteAgentManager; let mockThemeWatcher: MockThemeWatcher; let mockPrsTreeModel: PrsTreeModel; @@ -83,8 +81,7 @@ describe('ReviewCommentController', function () { reposManager = new RepositoriesManager(credentialStore, telemetry); gitApiImpl = new GitApiImpl(reposManager); mockPrsTreeModel = new MockPrsTreeModel() as unknown as PrsTreeModel; - copilotManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, gitApiImpl, mockPrsTreeModel); - provider = new PullRequestsTreeDataProvider(mockPrsTreeModel, telemetry, context, reposManager, copilotManager); + provider = new PullRequestsTreeDataProvider(mockPrsTreeModel, telemetry, context, reposManager); const activePrViewCoordinator = new WebviewViewCoordinator(context); const createPrHelper = new CreatePullRequestHelper(); manager = new FolderRepositoryManager(0, context, repository, telemetry, gitApiImpl, credentialStore, createPrHelper, mockThemeWatcher); diff --git a/src/view/prStatusDecorationProvider.ts b/src/view/prStatusDecorationProvider.ts index c8f44b1eba..7e423dff2d 100644 --- a/src/view/prStatusDecorationProvider.ts +++ b/src/view/prStatusDecorationProvider.ts @@ -10,7 +10,6 @@ import { Protocol } from '../common/protocol'; import { NOTIFICATION_SETTING, NotificationVariants, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { EventType } from '../common/timelineEvent'; import { createPRNodeUri, fromPRNodeUri, fromQueryUri, parsePRNodeIdentifier, PRNodeUriParams, Schemes, toQueryUri } from '../common/uri'; -import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { getStatusDecoration } from '../github/markdownUtils'; import { PullRequestModel } from '../github/pullRequestModel'; import { NotificationsManager } from '../notifications/notificationsManager'; @@ -22,7 +21,7 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil >(); onDidChangeFileDecorations: vscode.Event = this._onDidChangeFileDecorations.event; - constructor(private readonly _prsTreeModel: PrsTreeModel, private readonly _copilotManager: CopilotRemoteAgentManager, private readonly _notificationProvider: NotificationsManager) { + constructor(private readonly _prsTreeModel: PrsTreeModel, private readonly _notificationProvider: NotificationsManager) { super(); this._register(vscode.window.registerFileDecorationProvider(this)); this._register( @@ -31,7 +30,7 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil }) ); - this._register(this._copilotManager.onDidChangeNotifications(items => { + this._register(this._prsTreeModel.onDidChangeCopilotNotifications(items => { const repoItems = new Set(); const uris: vscode.Uri[] = []; for (const item of items) { diff --git a/src/view/prsTreeDataProvider.ts b/src/view/prsTreeDataProvider.ts index e433636fca..40f2d680c6 100644 --- a/src/view/prsTreeDataProvider.ts +++ b/src/view/prsTreeDataProvider.ts @@ -15,7 +15,6 @@ import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE, QUERIES, REMOTES } from '../co import { ITelemetry } from '../common/telemetry'; import { createPRNodeIdentifier } from '../common/uri'; import { EXTENSION_ID } from '../constants'; -import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { FolderRepositoryManager, ReposManagerState } from '../github/folderRepositoryManager'; import { PullRequestChangeEvent } from '../github/githubRepository'; import { PRType } from '../github/interface'; @@ -52,7 +51,7 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T return this._view; } - constructor(public readonly prsTreeModel: PrsTreeModel, private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, private readonly _reposManager: RepositoriesManager, private readonly _copilotManager: CopilotRemoteAgentManager) { + constructor(public readonly prsTreeModel: PrsTreeModel, private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, private readonly _reposManager: RepositoriesManager) { super(); this._register(this.prsTreeModel.onDidChangeData(e => { if (e instanceof FolderRepositoryManager) { @@ -97,17 +96,15 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T } }); - this._register(this._copilotManager.onDidChangeStates(() => { + this._register(this.prsTreeModel.onDidChangeCopilotStates(() => { this.refreshAllQueryResults(); })); - this._register(this._copilotManager.onDidChangeNotifications(() => { + this._register(this.prsTreeModel.onDidChangeCopilotNotifications(() => { this.updateBadge(); })); this.updateBadge(); - this._register(this._copilotManager.onDidCreatePullRequest(() => this.refreshAllQueryResults(true))); - // Listen for PR overview panel changes to sync the tree view this._register(PullRequestOverviewPanel.onVisible(pullRequest => { // Only sync if view is already visible (don't open the view) @@ -348,7 +345,7 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T this._register(this._notificationsProvider.onDidChangeNotifications(() => { this.updateBadge(); })); - this._register(new PRStatusDecorationProvider(this.prsTreeModel, this._copilotManager, this._notificationsProvider)); + this._register(new PRStatusDecorationProvider(this.prsTreeModel, this._notificationsProvider)); this.initializeCategories(); this.refreshAll(); @@ -566,7 +563,6 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T this._notificationsProvider!, this._context, this.prsTreeModel, - this._copilotManager, ); } else { result = gitHubFolderManagers.map( @@ -578,8 +574,7 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T this._telemetry, this._notificationsProvider!, this._context, - this.prsTreeModel, - this._copilotManager + this.prsTreeModel ), ); } diff --git a/src/view/reviewsManager.ts b/src/view/reviewsManager.ts index 2995e0c8a8..faa41ce511 100644 --- a/src/view/reviewsManager.ts +++ b/src/view/reviewsManager.ts @@ -64,7 +64,7 @@ export class ReviewsManager extends Disposable { } this._prsTreeDataProvider.dispose(); - this._prsTreeDataProvider = this._register(new PullRequestsTreeDataProvider(this._prsTreeModel, this._telemetry, this._context, this._reposManager, this._copilotManager)); + this._prsTreeDataProvider = this._register(new PullRequestsTreeDataProvider(this._prsTreeModel, this._telemetry, this._context, this._reposManager)); this._prsTreeDataProvider.initialize(this._reviewManagers.map(manager => manager.reviewModel), this._notificationsManager); } })); diff --git a/src/view/treeNodes/categoryNode.ts b/src/view/treeNodes/categoryNode.ts index 92fe0bf800..b7f0d2baa2 100644 --- a/src/view/treeNodes/categoryNode.ts +++ b/src/view/treeNodes/categoryNode.ts @@ -11,7 +11,6 @@ import { ITelemetry } from '../../common/telemetry'; import { toQueryUri } from '../../common/uri'; import { formatError } from '../../common/utils'; import { isCopilotQuery } from '../../github/copilotPrWatcher'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { FolderRepositoryManager, ItemsResponseResult } from '../../github/folderRepositoryManager'; import { PRType } from '../../github/interface'; import { PullRequestModel } from '../../github/pullRequestModel'; @@ -144,7 +143,6 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { public readonly type: PRType, private _notificationProvider: NotificationsManager, private _prsTreeModel: PrsTreeModel, - private _copilotManager: CopilotRemoteAgentManager, _categoryLabel?: string, private _categoryQuery?: string, ) { diff --git a/src/view/treeNodes/workspaceFolderNode.ts b/src/view/treeNodes/workspaceFolderNode.ts index 7963f3b3ee..46fd88e494 100644 --- a/src/view/treeNodes/workspaceFolderNode.ts +++ b/src/view/treeNodes/workspaceFolderNode.ts @@ -7,7 +7,6 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { PR_SETTINGS_NAMESPACE, QUERIES } from '../../common/settingKeys'; import { ITelemetry } from '../../common/telemetry'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; import { PRType } from '../../github/interface'; import { PullRequestModel } from '../../github/pullRequestModel'; @@ -33,8 +32,7 @@ export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem { private telemetry: ITelemetry, private notificationProvider: NotificationsManager, private context: vscode.ExtensionContext, - private readonly _prsTreeModel: PrsTreeModel, - private readonly _copilotMananger: CopilotRemoteAgentManager + private readonly _prsTreeModel: PrsTreeModel ) { super(parent); this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; @@ -68,7 +66,7 @@ export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem { if (!shouldDispose && this._children) { return this._children; } - this._children = await WorkspaceFolderNode.getCategoryTreeNodes(this.folderManager, this.telemetry, this, this.notificationProvider, this.context, this._prsTreeModel, this._copilotMananger); + this._children = await WorkspaceFolderNode.getCategoryTreeNodes(this.folderManager, this.telemetry, this, this.notificationProvider, this.context, this._prsTreeModel); return this._children; } @@ -79,17 +77,16 @@ export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem { notificationProvider: NotificationsManager, context: vscode.ExtensionContext, prsTreeModel: PrsTreeModel, - copilotManager: CopilotRemoteAgentManager ) { const queries = await WorkspaceFolderNode.getQueries(folderManager); const queryCategories: Map = new Map(); for (const queryInfo of queries) { if (isLocalQuery(queryInfo)) { - queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.LocalPullRequest, notificationProvider, prsTreeModel, copilotManager)); + queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.LocalPullRequest, notificationProvider, prsTreeModel)); } else if (isAllQuery(queryInfo)) { - queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.All, notificationProvider, prsTreeModel, copilotManager)); + queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.All, notificationProvider, prsTreeModel)); } else { - queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.Query, notificationProvider, prsTreeModel, copilotManager, queryInfo.label, queryInfo.query)); + queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.Query, notificationProvider, prsTreeModel, queryInfo.label, queryInfo.query)); } }