Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessions.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import type { Attachment, SendOptions, SessionOptions, SweCustomAgent } from '@github/copilot/sdk';5import * as l10n from '@vscode/l10n';6import * as vscode from 'vscode';7import { ChatExtendedRequestHandler, ChatRequestTurn2, Uri } from 'vscode';8import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService';9import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';10import { INativeEnvService } from '../../../platform/env/common/envService';11import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';12import { IGitService } from '../../../platform/git/common/gitService';13import { toGitUri } from '../../../platform/git/common/utils';14import { ILogService } from '../../../platform/log/common/logService';15import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';16import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';17import { isUri } from '../../../util/common/types';18import { DeferredPromise, IntervalTimer, raceCancellation } from '../../../util/vs/base/common/async';19import { CancellationToken } from '../../../util/vs/base/common/cancellation';20import { isCancellationError } from '../../../util/vs/base/common/errors';21import { Emitter, Event } from '../../../util/vs/base/common/event';22import { Disposable, DisposableStore, IDisposable, IReference } from '../../../util/vs/base/common/lifecycle';23import { ResourceMap } from '../../../util/vs/base/common/map';24import { relative } from '../../../util/vs/base/common/path';25import { basename, dirname, extUri } from '../../../util/vs/base/common/resources';26import { StopWatch } from '../../../util/vs/base/common/stopwatch';27import { hasKey } from '../../../util/vs/base/common/types';28import { EXTENSION_ID } from '../../common/constants';29import { GitBranchNameGenerator } from '../../prompt/node/gitBranch';30import { IChatSessionMetadataStore, RepositoryProperties } from '../common/chatSessionMetadataStore';31import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService';32import { IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';33import { IChatFolderMruService, IFolderRepositoryManager, IsolationMode } from '../common/folderRepositoryManager';34import { getWorkingDirectory, IWorkspaceInfo } from '../common/workspaceInfo';35import { ICustomSessionTitleService } from '../copilotcli/common/customSessionTitleService';36import { IChatDelegationSummaryService } from '../copilotcli/common/delegationSummaryService';37import { clearPendingCopilotCLIRequestContext, setPendingCopilotCLIRequestContext, takePendingCopilotCLIRequestContext } from '../copilotcli/common/pendingRequestContext';38import { SessionIdForCLI } from '../copilotcli/common/utils';39import { getCopilotCLISessionDir } from '../copilotcli/node/cliHelpers';40import { ICopilotCLISDK } from '../copilotcli/node/copilotCli';41import { CopilotCLIPromptResolver } from '../copilotcli/node/copilotcliPromptResolver';42import { builtinSlashSCommands, CopilotCLICommand, copilotCLICommands, ICopilotCLISession } from '../copilotcli/node/copilotcliSession';43import { ICopilotCLISessionItem, ICopilotCLISessionService } from '../copilotcli/node/copilotcliSessionService';44import { buildMcpServerMappings } from '../copilotcli/node/mcpHandler';45import { ICopilotCLISessionTracker } from '../copilotcli/vscode-node/copilotCLISessionTracker';46import { ICopilotCLITerminalIntegration, TerminalOpenLocation } from './copilotCLITerminalIntegration';47import { CopilotCloudSessionsProvider } from './copilotCloudSessionsProvider';48import { UNTRUSTED_FOLDER_MESSAGE } from './folderRepositoryManagerImpl';49import { IPullRequestDetectionService } from './pullRequestDetectionService';50import { getSelectedSessionOptions, ISessionOptionGroupBuilder, OPEN_REPOSITORY_COMMAND_ID, toRepositoryOptionItem, toWorkspaceFolderOptionItem } from './sessionOptionGroupBuilder';51import { ISessionRequestLifecycle } from './sessionRequestLifecycle';52import { ICopilotCLIChatSessionInitializer, SessionInitOptions } from '../copilotcli/vscode-node/copilotCLIChatSessionInitializer';53import { convertReferenceToVariable } from '../copilotcli/vscode-node/copilotCLIPromptReferences';545556export interface ICopilotCLIChatSessionItemProvider extends IDisposable {57refreshSession(refreshOptions: { reason: 'update'; sessionId: string } | { reason: 'update'; sessionIds: string[] } | { reason: 'delete'; sessionId: string }): Promise<void>;58}5960const OPEN_IN_COPILOT_CLI_COMMAND_ID = 'github.copilot.cli.openInCopilotCLI';61const CHECK_FOR_STEERING_DELAY = 100; // ms6263const _invalidCopilotCLISessionIdsWithErrorMessage = new Map<string, string>();6465// Re-export for backward compatibility66export { resolveBranchLockState, resolveBranchSelection, resolveIsolationSelection } from './sessionOptionGroupBuilder';6768/**69* Escape XML special characters70*/71function escapeXml(text: string): string {72return text73.replace(/&/g, '&')74.replace(/</g, '<')75.replace(/>/g, '>')76.replace(/"/g, '"')77.replace(/'/g, ''');78}7980function getIssueRuntimeInfo(): { readonly platform: string; readonly vscodeInfo: string; readonly extensionVersion: string } {81const extensionVersion = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON?.version;8283return {84platform: `${process.platform}-${process.arch}`,85vscodeInfo: `${vscode.env.appName} ${vscode.version}`,86extensionVersion: extensionVersion ?? 'unknown'87};88}8990function getSessionLoadFailureIssueInfo(invalidSessionMessage: string): { readonly issueBody: string; readonly issueUrl: string } {91const runtimeInfo = getIssueRuntimeInfo();92const issueTitle = '[Copilot CLI] Failed to load chat session';93const issueBody = `## Description\n\nFailed to load a Copilot CLI chat session.\n\n## Environment\n\n- Platform: ${runtimeInfo.platform}\n- VS Code: ${runtimeInfo.vscodeInfo}\n- Chat Extension Version: ${runtimeInfo.extensionVersion}\n\n## Error\n\n\`\`\`\n${invalidSessionMessage}\n\`\`\``;94const issueUrl = `https://github.com/microsoft/vscode/issues/new?title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}`;9596return { issueBody, issueUrl };97}9899/**100* Resolves candidate session directories for a CLI terminal, ordered by101* terminal affinity.102*103* Sessions whose owning terminal matches `terminal` are returned first so the104* link provider's file-existence probing hits the correct session-state dir105* before unrelated ones. Unrelated sessions are still included at the tail106* because a new session may not have registered its terminal yet (session IDs107* arrive later via MCP?).108*/109export async function resolveSessionDirsForTerminal(110sessionTracker: ICopilotCLISessionTracker,111terminal: vscode.Terminal,112): Promise<Uri[]> {113const activeIds = sessionTracker.getSessionIds();114const matching: Uri[] = [];115const rest: Uri[] = [];116for (const id of activeIds) {117const sessionTerminal = await sessionTracker.getTerminal(id);118const dir = Uri.file(getCopilotCLISessionDir(id));119if (sessionTerminal === terminal) {120matching.push(dir);121} else {122rest.push(dir);123}124}125return [...matching, ...rest];126}127128export class CopilotCLIChatSessionContentProvider extends Disposable implements vscode.ChatSessionContentProvider, ICopilotCLIChatSessionItemProvider {129private readonly _onDidCommitChatSessionItem = this._register(new Emitter<{ original: vscode.ChatSessionItem; modified: vscode.ChatSessionItem }>());130public readonly onDidCommitChatSessionItem: Event<{ original: vscode.ChatSessionItem; modified: vscode.ChatSessionItem }> = this._onDidCommitChatSessionItem.event;131132private readonly controller: vscode.ChatSessionItemController;133private readonly newSessions = new ResourceMap<vscode.ChatSessionItem>();134constructor(135@ICopilotCLISessionService private readonly sessionService: ICopilotCLISessionService,136@IChatSessionWorktreeService private readonly copilotCLIWorktreeManagerService: IChatSessionWorktreeService,137@IFolderRepositoryManager private readonly folderRepositoryManager: IFolderRepositoryManager,138@IConfigurationService private readonly configurationService: IConfigurationService,139@ICustomSessionTitleService private readonly customSessionTitleService: ICustomSessionTitleService,140@IRunCommandExecutionService private readonly commandExecutionService: IRunCommandExecutionService,141@ILogService private readonly logService: ILogService,142@IPullRequestDetectionService private readonly _prDetectionService: IPullRequestDetectionService,143@ISessionOptionGroupBuilder private readonly _optionGroupBuilder: ISessionOptionGroupBuilder,144@IGitService private readonly _gitService: IGitService,145@IChatSessionWorkspaceFolderService private readonly _workspaceFolderService: IChatSessionWorkspaceFolderService,146@IChatSessionMetadataStore private readonly _metadataStore: IChatSessionMetadataStore,147@IWorkspaceService private readonly _workspaceService: IWorkspaceService,148@IChatSessionWorktreeService chatSessionWorktreeService: IChatSessionWorktreeService,149) {150super();151152let isRefreshing = false;153const refreshSessions = async () => {154if (isRefreshing) {155return;156}157isRefreshing = true;158const stopwatch = new StopWatch();159void this._metadataStore.refresh().catch(error => this.logService.error(error, 'Failed to refresh session metadata store during session list refresh'));160try {161const sessions = await this.sessionService.getAllSessions(CancellationToken.None);162const items = await Promise.all(sessions.map(async session => this.toChatSessionItem(session)));163164const count = items.length;165void this.commandExecutionService.executeCommand('setContext', 'github.copilot.chat.cliSessionsEmpty', count === 0);166167controller.items.replace(items);168} finally {169isRefreshing = false;170this.logService.info(`[CopilotCLIChatSessionContentProvider] listSessions took ${stopwatch.elapsed()}ms`);171}172};173const controller = this.controller = this._register(vscode.chat.createChatSessionItemController(174'copilotcli',175async () => {176await refreshSessions();177}178));179this._register(configurationService.onDidChangeConfiguration(e => {180if (e.affectsConfiguration(ConfigKey.Advanced.CLIShowExternalSessions.fullyQualifiedId)) {181void refreshSessions();182}183}));184this._register(this._workspaceFolderService.onDidChangeWorkspaceFolderChanges(e => {185this.refreshSession({ reason: 'update', sessionId: e.sessionId });186}));187this._register(chatSessionWorktreeService.onDidChangeWorktreeChanges(e => {188this.refreshSession({ reason: 'update', sessionId: e.sessionId });189}));190controller.newChatSessionItemHandler = async (context) => {191const sessionId = this.sessionService.createNewSessionId();192const resource = SessionIdForCLI.getResource(sessionId);193const session = controller.createChatSessionItem(resource, context.request.prompt ?? context.request.command ?? '');194this.customSessionTitleService.generateSessionTitle(sessionId, context.request, CancellationToken.None)195.then(async title => {196if (title) {197await this.customSessionTitleService.setCustomSessionTitle(sessionId, title);198}199// Given we're done generating a title, refresh the contents of this session so that the new title is picked up.200if (this.controller.items.get(resource)) {201this.refreshSession({ reason: 'update', sessionId }).catch(() => { /* expected if session was deleted */ });202}203})204.catch(ex => this.logService.error(ex, 'Failed to generate custom session title'));205206controller.items.add(session);207this.newSessions.set(resource, session);208return session;209};210if (this.configurationService.getConfig(ConfigKey.Advanced.CLIForkSessionsEnabled)) {211controller.forkHandler = async (sessionResource: Uri, request: ChatRequestTurn2 | undefined, token: vscode.CancellationToken) => {212const sessionId = SessionIdForCLI.parse(sessionResource);213const folderInfo = await this.folderRepositoryManager.getFolderRepository(sessionId, undefined, token);214const forkedSessionId = await this.sessionService.forkSession({ sessionId, requestId: request?.id, workspace: folderInfo }, token);215const item = await this.sessionService.getSessionItem(forkedSessionId, token);216if (!item) {217throw new Error(`Failed to get session item for forked session ${forkedSessionId}`);218}219return this.toChatSessionItem(item, undefined, token);220};221}222// Defers the slow `buildChanges` (git diff) call to when the editor renders the item.223if (this.configurationService.getConfig(ConfigKey.Advanced.CLIChatLazyLoadSessionItem)) {224controller.resolveChatSessionItem = async (item, token) => {225const sessionId = SessionIdForCLI.parse(item.resource);226const session = await this.sessionService.getSessionItem(sessionId, token);227if (!session || token.isCancellationRequested) {228return;229}230const updatedItem = await this.toChatSessionItem(session, { includeChanges: true }, token);231controller.items.add(updatedItem);232};233}234this._register(this.sessionService.onDidDeleteSession(async (e) => {235controller.items.delete(SessionIdForCLI.getResource(e));236}));237this._register(this.sessionService.onDidChangeSession(async (e) => {238// Push path: VS Code uses the item we provide as source of truth and does not239// re-invoke `resolveChatSessionItem` for already-visible rows. Include changes240// eagerly so the visible row reflects the latest diff info.241const item = await this.toChatSessionItem(e, { includeChanges: true });242controller.items.add(item);243}));244this._register(this.sessionService.onDidCreateSession(async (e) => {245const resource = SessionIdForCLI.getResource(e.id);246if (controller.items.get(resource)) {247return;248}249const item = await this.toChatSessionItem(e, { includeChanges: true });250controller.items.add(item);251}));252253// Handle worktree cleanup/recreation when archive state changes254if (controller.onDidChangeChatSessionItemState) {255this._register(controller.onDidChangeChatSessionItemState(async (item) => {256const sessionId = SessionIdForCLI.parse(item.resource);257if (item.archived) {258try {259const result = await this.copilotCLIWorktreeManagerService.cleanupWorktreeOnArchive(sessionId);260this.logService.trace(`[CopilotCLI] Worktree cleanup for session ${sessionId}: ${result.cleaned ? 'cleaned' : result.reason}`);261} catch (error) {262this.logService.error(`[CopilotCLI] Failed to cleanup worktree for archived session ${sessionId}:`, error);263}264} else {265try {266const result = await this.copilotCLIWorktreeManagerService.recreateWorktreeOnUnarchive(sessionId);267this.logService.trace(`[CopilotCLI] Worktree recreation for session ${sessionId}: ${result.recreated ? 'recreated' : result.reason}`);268if (result.recreated) {269await this.refreshSession({ reason: 'update', sessionId });270}271} catch (error) {272this.logService.error(`[CopilotCLI] Failed to recreate worktree for unarchived session ${sessionId}:`, error);273}274}275}));276}277278const newInputStates: WeakRef<vscode.ChatSessionInputState>[] = [];279controller.getChatSessionInputState = async (sessionResource, context, token) => {280const isExistingSession = sessionResource && !this.sessionService.isNewSessionId(SessionIdForCLI.parse(sessionResource));281if (isExistingSession) {282const groups = await this._optionGroupBuilder.buildExistingSessionInputStateGroups(sessionResource, token);283return controller.createChatSessionInputState(groups);284} else {285const groups = await this._optionGroupBuilder.provideChatSessionProviderOptionGroups(context.previousInputState);286const state = controller.createChatSessionInputState(groups);287// Only wire dynamic updates for new sessions (existing sessions are fully locked).288// Note: don't use the getChatSessionInputState token here — it's a one-shot token289// that may be disposed by the time the user interacts with the dropdowns.290newInputStates.push(new WeakRef(state));291state.onDidChange(() => {292void this._optionGroupBuilder.handleInputStateChange(state);293});294return state;295}296};297298// Refresh new-session dropdown groups when git or workspace state changes299// (e.g. after git init, opening a repo, or adding/removing workspace folders).300const refreshActiveInputState = () => {301// Sweep stale WeakRefs before iterating302for (let i = newInputStates.length - 1; i >= 0; i--) {303if (!newInputStates[i].deref()) {304newInputStates.splice(i, 1);305}306}307for (const weakRef of newInputStates) {308const state = weakRef.deref();309if (state) {310void this._optionGroupBuilder.rebuildInputState(state);311}312}313};314this._register(this._gitService.onDidFinishInitialization(refreshActiveInputState));315this._register(this._gitService.onDidOpenRepository(refreshActiveInputState));316this._register(this._gitService.onDidCloseRepository(refreshActiveInputState));317this._register(this._workspaceService.onDidChangeWorkspaceFolders(refreshActiveInputState));318}319320public getAssociatedSessions(folder: Uri): string[] {321return this._metadataStore.getSessionIdsForFolder(folder);322}323324public async updateInputStateAfterFolderSelection(inputState: vscode.ChatSessionInputState, folderUri: vscode.Uri): Promise<void> {325await this._optionGroupBuilder.rebuildInputState(inputState, folderUri);326}327328public async refreshSession(refreshOptions: { reason: 'update'; sessionId: string } | { reason: 'update'; sessionIds: string[] } | { reason: 'delete'; sessionId: string }): Promise<void> {329if (refreshOptions.reason === 'delete') {330const uri = SessionIdForCLI.getResource(refreshOptions.sessionId);331this.controller.items.delete(uri);332} else if (refreshOptions.reason === 'update' && hasKey(refreshOptions, { 'sessionIds': true })) {333await Promise.allSettled(refreshOptions.sessionIds.map(async sessionId => {334const item = await this.sessionService.getSessionItem(sessionId, CancellationToken.None);335if (item) {336// Push path — include changes eagerly (see `onDidChangeSession`).337const chatSessionItem = await this.toChatSessionItem(item, { includeChanges: true });338this.controller.items.add(chatSessionItem);339}340}));341} else {342const item = await this.sessionService.getSessionItem(refreshOptions.sessionId, CancellationToken.None);343if (item) {344// Push path — include changes eagerly (see `onDidChangeSession`).345const chatSessionItem = await this.toChatSessionItem(item, { includeChanges: true });346this.controller.items.add(chatSessionItem);347}348}349}350351public async toChatSessionItem(session: ICopilotCLISessionItem, options?: { readonly includeChanges?: boolean }, token?: vscode.CancellationToken): Promise<vscode.ChatSessionItem> {352token = token ?? CancellationToken.None;353const resource = SessionIdForCLI.getResource(session.id);354const item = this.controller.createChatSessionItem(resource, session.label);355356let worktreeProperties = await raceCancellation(this.copilotCLIWorktreeManagerService.getWorktreeProperties(session.id), token);357const workingDirectory = worktreeProperties?.worktreePath ? vscode.Uri.file(worktreeProperties.worktreePath)358: session.workingDirectory;359if (token.isCancellationRequested) {360return item;361362}363item.timing = session.timing;364item.status = session.status ?? vscode.ChatSessionStatus.Completed;365366// `buildChanges` runs `git diff` and is the slow leg of populating an item. Skip it on the367// eager pass and let `resolveChatSessionItem` fill it in lazily for visible items.368// But if computing changes is easy (cached or the like), then include them right away to avoid a second update pass.369if (options?.includeChanges || ((await this.hasCachedChanges(session.id, worktreeProperties)))) {370const changes = await this.buildChanges(session.id, worktreeProperties, workingDirectory, token);371if (token.isCancellationRequested) {372return item;373}374// We need to get an updated version of worktree properties here because when the375// changes are being computed, the worktree properties are also updated with the376// repository state which we are passing along through the metadata377worktreeProperties = await raceCancellation(this.copilotCLIWorktreeManagerService.getWorktreeProperties(session.id), token);378if (token.isCancellationRequested) {379return item;380}381382item.changes = changes;383}384385if (token.isCancellationRequested) {386return item;387}388389const [badge, metadata] = await Promise.all([390this.buildBadge(worktreeProperties, workingDirectory),391this.buildMetadata(session.id, worktreeProperties, workingDirectory),392]);393item.badge = badge;394item.metadata = metadata;395return item;396}397398private async buildBadge(399worktreeProperties: Awaited<ReturnType<IChatSessionWorktreeService['getWorktreeProperties']>>,400workingDirectory: vscode.Uri | undefined,401): Promise<vscode.MarkdownString | undefined> {402const repositories = this._gitService.repositories.filter(r => r.kind !== 'worktree');403const shouldShow = vscode.workspace.workspaceFolders === undefined ||404vscode.workspace.isAgentSessionsWorkspace ||405repositories.length > 1;406if (!shouldShow) {407return undefined;408}409const badgeUri = worktreeProperties?.repositoryPath410? vscode.Uri.file(worktreeProperties.repositoryPath)411: workingDirectory;412if (!badgeUri) {413return undefined;414}415const isTrusted = await vscode.workspace.isResourceTrusted(badgeUri);416const isRepo = !!worktreeProperties?.repositoryPath;417const icon = isTrusted ? (isRepo ? '$(repo)' : '$(folder)') : '$(workspace-untrusted)';418const badge = new vscode.MarkdownString(`${icon} ${basename(badgeUri)}`);419badge.supportThemeIcons = true;420return badge;421}422423private async hasCachedChanges(sessionId: string, worktreeProperties: Awaited<ReturnType<IChatSessionWorktreeService['getWorktreeProperties']>>): Promise<boolean> {424if (!this.configurationService.getConfig(ConfigKey.Advanced.CLIChatLazyLoadSessionItem)) {425return true;426}427const [hasCachedWorktreeChanges, hasCachedWorkspaceChanges] = await Promise.all([428this.copilotCLIWorktreeManagerService.hasCachedChanges(sessionId),429this._workspaceFolderService.hasCachedChanges(sessionId)430]);431return hasCachedWorktreeChanges || hasCachedWorkspaceChanges;432}433434private async buildChanges(435sessionId: string,436worktreeProperties: Awaited<ReturnType<IChatSessionWorktreeService['getWorktreeProperties']>>,437workingDirectory: vscode.Uri | undefined,438token: CancellationToken = CancellationToken.None439): Promise<vscode.ChatSessionChangedFile[]> {440const changes: vscode.ChatSessionChangedFile[] = [];441if (worktreeProperties?.repositoryPath && await vscode.workspace.isResourceTrusted(vscode.Uri.file(worktreeProperties.repositoryPath))) {442if (token.isCancellationRequested) {443return [];444}445changes.push(...(await raceCancellation(this.copilotCLIWorktreeManagerService.getWorktreeChanges(sessionId), token) ?? []));446} else if (workingDirectory && await vscode.workspace.isResourceTrusted(workingDirectory)) {447if (token.isCancellationRequested) {448return [];449}450const workspaceChanges = await raceCancellation(this._workspaceFolderService.getWorkspaceChanges(sessionId), token) ?? [];451const repositoryProperties = await raceCancellation(this._metadataStore.getRepositoryProperties(sessionId), token);452453changes.push(...workspaceChanges.map(change => {454const originalRef = repositoryProperties?.mergeBaseCommit ?? 'HEAD';455456return new vscode.ChatSessionChangedFile(457vscode.Uri.file(change.filePath),458change.originalFilePath459? toGitUri(vscode.Uri.file(change.originalFilePath), originalRef)460: undefined,461change.modifiedFilePath462? vscode.Uri.file(change.modifiedFilePath)463: undefined,464change.statistics.additions,465change.statistics.deletions);466}));467}468return changes;469}470471private async buildMetadata(472sessionId: string,473worktreeProperties: Awaited<ReturnType<IChatSessionWorktreeService['getWorktreeProperties']>>,474workingDirectory: vscode.Uri | undefined,475): Promise<{ readonly [key: string]: unknown }> {476if (worktreeProperties) {477const sessionParentId = await this._metadataStore.getSessionParentId(sessionId);478479return {480sessionParentId,481autoCommit: worktreeProperties.autoCommit !== false,482baseCommit: worktreeProperties?.baseCommit,483baseBranchName: worktreeProperties.version === 2484? worktreeProperties.baseBranchName485: undefined,486baseBranchProtected: worktreeProperties.version === 2487? worktreeProperties.baseBranchProtected === true488: undefined,489branchName: worktreeProperties?.branchName,490upstreamBranchName: worktreeProperties.version === 2491? worktreeProperties.upstreamBranchName492: undefined,493isolationMode: IsolationMode.Worktree,494repositoryPath: worktreeProperties?.repositoryPath,495worktreePath: worktreeProperties?.worktreePath,496pullRequestUrl: worktreeProperties.version === 2497? worktreeProperties.pullRequestUrl498: undefined,499pullRequestState: worktreeProperties.version === 2500? worktreeProperties.pullRequestState501: undefined,502firstCheckpointRef: worktreeProperties.version === 2503? worktreeProperties.firstCheckpointRef504: undefined,505baseCheckpointRef: worktreeProperties.version === 2506? worktreeProperties.baseCheckpointRef507: undefined,508lastCheckpointRef: worktreeProperties.version === 2509? worktreeProperties.lastCheckpointRef510: undefined,511hasGitHubRemote: worktreeProperties.version === 2512? worktreeProperties.hasGitHubRemote513: undefined,514incomingChanges: worktreeProperties.version === 2515? worktreeProperties.incomingChanges516: undefined,517outgoingChanges: worktreeProperties.version === 2518? worktreeProperties.outgoingChanges519: undefined,520uncommittedChanges: worktreeProperties.version === 2521? worktreeProperties.uncommittedChanges522: undefined523} satisfies { readonly [key: string]: unknown };524}525526const [sessionParentId, sessionRequestDetails, repositoryProperties] = await Promise.all([527this._metadataStore.getSessionParentId(sessionId),528this._metadataStore.getRequestDetails(sessionId),529this._metadataStore.getRepositoryProperties(sessionId)530]);531532let lastCheckpointRef: string | undefined;533for (let i = sessionRequestDetails.length - 1; i >= 0; i--) {534const checkpointRef = sessionRequestDetails[i]?.checkpointRef;535if (checkpointRef !== undefined) {536lastCheckpointRef = checkpointRef;537break;538}539}540541const firstCheckpointRef = lastCheckpointRef542? `${lastCheckpointRef.slice(0, lastCheckpointRef.lastIndexOf('/'))}/0`543: undefined;544545return {546sessionParentId,547isolationMode: IsolationMode.Workspace,548repositoryPath: repositoryProperties?.repositoryPath,549branchName: repositoryProperties?.branchName,550baseBranchName: repositoryProperties?.baseBranchName,551upstreamBranchName: repositoryProperties?.upstreamBranchName,552workingDirectoryPath: workingDirectory?.fsPath,553hasGitHubRemote: repositoryProperties?.hasGitHubRemote,554incomingChanges: repositoryProperties?.incomingChanges,555outgoingChanges: repositoryProperties?.outgoingChanges,556uncommittedChanges: repositoryProperties?.uncommittedChanges,557firstCheckpointRef,558lastCheckpointRef559} satisfies { readonly [key: string]: unknown };560}561562async provideChatSessionContent(resource: Uri, token: vscode.CancellationToken, context?: { readonly inputState: vscode.ChatSessionInputState }): Promise<vscode.ChatSession> {563const stopwatch = new StopWatch();564try {565const copilotcliSessionId = SessionIdForCLI.parse(resource);566if (copilotcliSessionId.startsWith('untitled:') || copilotcliSessionId.startsWith('untitled-')) {567return {568history: [],569requestHandler: undefined,570};571}572if (this.sessionService.isNewSessionId(copilotcliSessionId)) {573const session = this.newSessions.get(resource);574if (!session) {575throw new Error('Session not found');576}577578const options: Record<string, vscode.ChatSessionProviderOptionItem> = {};579for (const group of (context?.inputState.groups || [])) {580if (group.selected) {581options[group.id] = { ...group.selected, locked: true };582}583}584585return {586title: session.label,587history: [],588options,589requestHandler: undefined,590};591} else {592this.newSessions.delete(resource);593// Fire-and-forget: detect PR when the user opens a session.594this._prDetectionService.detectPullRequest(copilotcliSessionId);595596const folderRepo = await this.folderRepositoryManager.getFolderRepository(copilotcliSessionId, undefined, token);597const [history, title, optionGroups] = await Promise.all([598this.getSessionHistory(copilotcliSessionId, folderRepo, token),599this.sessionService.getSessionTitle(copilotcliSessionId, token),600this._optionGroupBuilder.buildExistingSessionInputStateGroups(resource, token),601]);602603const options: Record<string, vscode.ChatSessionProviderOptionItem> = {};604for (const group of optionGroups) {605if (group.selected) {606options[group.id] = { ...group.selected, locked: true };607}608}609610return {611title,612history,613options,614requestHandler: undefined,615};616}617} finally {618this.logService.info(`[CopilotCLIChatSessionContentProvider] provideChatSessionContent for ${resource.toString()} took ${stopwatch.elapsed()}ms`);619}620}621622private async getSessionHistory(sessionId: string, workspaceInfo: IWorkspaceInfo, token: vscode.CancellationToken) {623try {624_invalidCopilotCLISessionIdsWithErrorMessage.delete(sessionId);625const history = await this.sessionService.getChatHistory({ sessionId, workspace: workspaceInfo }, token);626return history;627} catch (error) {628if (!isUnknownEventTypeError(error)) {629throw error;630}631632const partialHistory = await this.sessionService.tryGetPartialSessionHistory(sessionId);633if (partialHistory) {634_invalidCopilotCLISessionIdsWithErrorMessage.set(sessionId, error.message || String(error));635return partialHistory;636}637638throw error;639}640}641642}643644export class CopilotCLIChatSessionParticipant extends Disposable {645646constructor(647private readonly sessionItemProvider: ICopilotCLIChatSessionItemProvider,648private readonly promptResolver: CopilotCLIPromptResolver,649private readonly cloudSessionProvider: CopilotCloudSessionsProvider | undefined,650private readonly branchNameGenerator: GitBranchNameGenerator | undefined,651@IGitService private readonly gitService: IGitService,652@ICopilotCLISessionService private readonly sessionService: ICopilotCLISessionService,653@IChatSessionWorktreeService private readonly copilotCLIWorktreeManagerService: IChatSessionWorktreeService,654@ITelemetryService private readonly telemetryService: ITelemetryService,655@ILogService private readonly logService: ILogService,656@IChatDelegationSummaryService private readonly chatDelegationSummaryService: IChatDelegationSummaryService,657@IConfigurationService private readonly configurationService: IConfigurationService,658@ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK,659@ICopilotCLIChatSessionInitializer private readonly sessionInitializer: ICopilotCLIChatSessionInitializer,660@ISessionRequestLifecycle private readonly sessionRequestLifecycle: ISessionRequestLifecycle,661@IPullRequestDetectionService private readonly prDetectionService: IPullRequestDetectionService,662@ISessionOptionGroupBuilder private readonly _optionGroupBuilder: ISessionOptionGroupBuilder,663) {664super();665666this._register(this.prDetectionService.onDidDetectPullRequest(sessionId => {667this.sessionItemProvider.refreshSession({ reason: 'update', sessionId }).catch(error => this.logService.error(error, 'Failed to refresh session after PR detection'));668}));669}670671createHandler(): ChatExtendedRequestHandler {672return this.handleRequest.bind(this);673}674675/**676* Outer request handler that supports *yielding* for session steering.677*678* ## How steering works end-to-end679*680* 1. The user sends a message while the session is already processing a681* previous request (status is `InProgress` or `NeedsInput`).682* 2. VS Code signals this by setting `context.yieldRequested = true` on the683* *previous* request's context object.684* 3. This handler polls `context.yieldRequested` every 100 ms. Once detected685* the outer `Promise.race` resolves, returning control to VS Code so it686* can dispatch the new (steering) request.687* 4. Crucially, the inner `handleRequestImpl` promise is **not** cancelled688* or disposed – the original SDK session continues running in the689* background.690* 5. When the new request arrives, `handleRequest` on the underlying691* {@link CopilotCLISession} detects the session is still busy and routes692* through `_handleRequestSteering`, which sends the new prompt with693* `mode: 'immediate'` and waits for both the steering send and the694* original request to complete.695*/696private async handleRequest(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult | void> {697const disposables = new DisposableStore();698try {699const handled = this.handleRequestImpl(request, context, stream, token);700const interval = disposables.add(new IntervalTimer());701const yielded = new DeferredPromise<void>();702interval.cancelAndSet(() => {703if (context.yieldRequested) {704yielded.complete();705}706}, CHECK_FOR_STEERING_DELAY);707708return await Promise.race([yielded.p, handled]);709} finally {710disposables.dispose();711}712}713714private sendTelemetryForHandleRequest(request: vscode.ChatRequest, context: vscode.ChatContext): void {715const { chatSessionContext } = context;716const hasChatSessionItem = String(!!chatSessionContext?.chatSessionItem);717const sessionId = chatSessionContext ? SessionIdForCLI.parse(chatSessionContext.chatSessionItem.resource) : undefined;718const isUntitled = sessionId ? String(this.sessionService.isNewSessionId(sessionId)) : 'false';719const hasDelegatePrompt = String(request.command === 'delegate');720721/* __GDPR__722"copilotcli.chat.invoke" : {723"owner": "joshspicer",724"comment": "Event sent when a CopilotCLI chat request is made.",725"chatRequestId": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The unique chat request ID." },726"hasChatSessionItem": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Invoked with a chat session item." },727"isUntitled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Indicates if the chat session is untitled." },728"hasDelegatePrompt": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Indicates if the prompt is a /delegate command." }729}730*/731this.telemetryService.sendMSFTTelemetryEvent('copilotcli.chat.invoke', {732chatRequestId: request.id,733hasChatSessionItem,734isUntitled,735hasDelegatePrompt736});737}738739private async authenticate(): Promise<NonNullable<SessionOptions['authInfo']>> {740const authInfo = await this.copilotCLISDK.getAuthInfo().catch((ex) => this.logService.error(ex, 'Authorization failed'));741if (!authInfo) {742this.logService.error(`Authorization failed`);743throw new Error(vscode.l10n.t('Authorization failed. Please sign into GitHub and try again.'));744}745if ((authInfo.type === 'token' && !authInfo.token) && !this.configurationService.getConfig(ConfigKey.Shared.DebugOverrideProxyUrl)) {746this.logService.error(`Authorization failed`);747throw new Error(vscode.l10n.t('Authorization failed. Please sign into GitHub and try again.'));748}749return authInfo;750}751752/**753* Resolve the input and attachments for the SDK session based on request type.754*755* The VS Code chat API creates the session before firing the request handler,756* so delegated or remotely-steered requests pre-resolve and cache their prompt metadata757* before the handler runs.758*/759private async resolveInput(760request: vscode.ChatRequest,761session: ICopilotCLISession,762isNewSession: boolean,763token: vscode.CancellationToken,764): Promise<{ input: { prompt: string; command?: CopilotCLICommand; source?: SendOptions['source'] }; attachments: Attachment[] }> {765const contextForRequest = takePendingCopilotCLIRequestContext(session.sessionId);766767if (contextForRequest) {768return { input: { prompt: contextForRequest.prompt, source: contextForRequest.source }, attachments: contextForRequest.attachments };769}770771if (request.command && !request.prompt && !isNewSession) {772const input = (copilotCLICommands as readonly string[]).includes(request.command)773? { command: request.command as CopilotCLICommand, prompt: '' }774: { prompt: `/${request.command}` };775return { input, attachments: [] };776}777778const { prompt, attachments } = await this.promptResolver.resolvePrompt(request, undefined, [], session.workspace, [], token);779const input = (request.command && (copilotCLICommands as readonly string[]).includes(request.command))780? { command: request.command as CopilotCLICommand, prompt }781: { prompt };782return { input, attachments };783}784785private generateNewBranchName(request: vscode.ChatRequest, token: vscode.CancellationToken): Promise<string | undefined> {786const requestTurn = new ChatRequestTurn2(request.prompt ?? '', request.command, [], '', [], [], undefined, undefined, undefined);787const fakeContext: vscode.ChatContext = {788history: [requestTurn],789yieldRequested: false,790};791const branchNamePromise = (request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);792return branchNamePromise;793}794private async handleRequestImpl(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken): Promise<vscode.ChatResult | void> {795const { chatSessionContext } = context;796const disposables = new DisposableStore();797let sdkSessionId: string | undefined = undefined;798let session: IReference<ICopilotCLISession> | undefined = undefined;799try {800this.sendTelemetryForHandleRequest(request, context);801802const authInfo = await this.authenticate();803804if (!chatSessionContext || !SessionIdForCLI.isCLIResource(request.sessionResource)) {805return await this.handleDelegationFromAnotherChat(request, undefined, request.references, context, stream, authInfo, token);806}807808const { resource } = chatSessionContext.chatSessionItem;809const sessionId = SessionIdForCLI.parse(resource);810const isNewSession = this.sessionService.isNewSessionId(sessionId);811const invalidSessionMessage = _invalidCopilotCLISessionIdsWithErrorMessage.get(sessionId);812813if (invalidSessionMessage) {814const { issueUrl } = getSessionLoadFailureIssueInfo(invalidSessionMessage);815const warningMessage = new vscode.MarkdownString();816warningMessage.appendMarkdown(l10n.t({817message: "Failed loading this session. If this issue persists, please [report an issue]({issueUrl}). \nError: ",818args: { issueUrl },819comment: [`{Locked=']({'}`]820}));821warningMessage.appendText(invalidSessionMessage);822stream.warning(warningMessage);823return {};824}825826const branchNamePromise = isNewSession ? this.generateNewBranchName(request, token) : Promise.resolve(undefined);827828if (isNewSession) {829this._optionGroupBuilder.lockInputStateGroups(chatSessionContext.inputState);830}831832const selectedOptions = getSelectedSessionOptions(chatSessionContext.inputState);833const sessionResult = await this.getOrCreateSession(request, chatSessionContext.chatSessionItem.resource, { ...selectedOptions, newBranch: branchNamePromise, stream }, disposables, token);834({ session } = sessionResult);835836837if (isNewSession && !sessionResult.trusted) {838await this._optionGroupBuilder.rebuildInputState(chatSessionContext.inputState);839}840841const { model, agent } = sessionResult;842if (!session || token.isCancellationRequested) {843return {};844}845846if (isNewSession && session.object.workspace.worktreeProperties) {847const branchName = session.object.workspace.worktreeProperties.branchName;848this._optionGroupBuilder.updateBranchInInputState(chatSessionContext.inputState, branchName);849}850851sdkSessionId = session.object.sessionId;852853await this.sessionRequestLifecycle.startRequest(sdkSessionId, request, context.history.length === 0, session.object.workspace, agent?.name);854855if (request.command === 'delegate') {856await this.handleDelegationToCloud(session.object, request, context, stream, token);857} else {858const { input, attachments } = await this.resolveInput(request, session.object, isNewSession, token);859await session.object.handleRequest(request, input, attachments, model, authInfo, token);860}861862return {};863} catch (ex) {864if (isCancellationError(ex)) {865return {};866}867throw ex;868} finally {869if (sdkSessionId && session) {870await this.sessionRequestLifecycle.endRequest(871sdkSessionId, request,872{ status: session.object.status, workspace: session.object.workspace, createdPullRequestUrl: session.object.createdPullRequestUrl },873token,874);875this.sessionItemProvider.refreshSession({ reason: 'update', sessionId: sdkSessionId })876.catch(error => this.logService.error(error, 'Failed to refresh session item after handling request'));877}878disposables.dispose();879}880}881882private async getOrCreateSession(request: vscode.ChatRequest, chatResource: vscode.Uri, options: SessionInitOptions, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; isNewSession: boolean; model: { model: string; reasoningEffort?: string } | undefined; agent: SweCustomAgent | undefined; trusted: boolean }> {883const result = await this.sessionInitializer.getOrCreateSession(request, chatResource, options, disposables, token);884const { session, isNewSession, model, agent, trusted } = result;885if (!session || token.isCancellationRequested) {886return { session: undefined, isNewSession, model, agent, trusted };887}888889if (isNewSession) {890this.sessionItemProvider.refreshSession({ reason: 'update', sessionId: session.object.sessionId });891}892893return { session, isNewSession, model, agent, trusted };894}895896private async handleDelegationToCloud(session: ICopilotCLISession, request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) {897if (!this.cloudSessionProvider) {898stream.warning(l10n.t('No cloud agent available'));899return;900}901902// Check for uncommitted changes903const worktreeProperties = await this.copilotCLIWorktreeManagerService.getWorktreeProperties(session.sessionId);904const repositoryPath = worktreeProperties?.repositoryPath ? Uri.file(worktreeProperties.repositoryPath) : getWorkingDirectory(session.workspace);905const repository = repositoryPath ? await this.gitService.getRepository(repositoryPath) : undefined;906const hasChanges = (repository?.changes?.indexChanges && repository.changes.indexChanges.length > 0);907908if (hasChanges) {909stream.warning(l10n.t('You have uncommitted changes in your workspace. The cloud agent will start from the last committed state. Consider committing your changes first if you want to include them.'));910}911912const prInfo = await this.cloudSessionProvider.delegate(request, stream, context, token, { prompt: request.prompt, chatContext: context });913await this.recordPushToSession(session, `/delegate ${request.prompt}`, prInfo);914915}916917private async handleDelegationFromAnotherChat(918request: vscode.ChatRequest,919userPrompt: string | undefined,920otherReferences: readonly vscode.ChatPromptReference[] | undefined,921context: vscode.ChatContext,922stream: vscode.ChatResponseStream,923authInfo: NonNullable<SessionOptions['authInfo']>,924token: vscode.CancellationToken925): Promise<vscode.ChatResult> {926let summary: string | undefined;927const requestPromptPromise = (async () => {928if (this.hasHistoryToSummarize(context.history)) {929stream.progress(l10n.t('Analyzing chat history'));930summary = await this.chatDelegationSummaryService.summarize(context, token);931summary = summary ? `**Summary**\n${summary}` : undefined;932}933934// Give priority to userPrompt if provided (e.g., from confirmation metadata)935userPrompt = userPrompt || request.prompt;936return summary ? `${userPrompt}\n${summary}` : userPrompt;937})();938const branchNamePromise = this.generateNewBranchName(request, token);939const { workspaceInfo, cancelled } = await this.sessionInitializer.initializeWorkingDirectory(undefined, { stream, isolation: IsolationMode.Worktree, newBranch: branchNamePromise }, request.toolInvocationToken, token);940941if (cancelled || token.isCancellationRequested) {942stream.markdown(l10n.t('Copilot CLI delegation cancelled.'));943return {};944}945const { prompt, attachments, references } = await this.promptResolver.resolvePrompt(request, await requestPromptPromise, (otherReferences || []).concat([]), workspaceInfo, [], token);946947const mcpServerMappings = buildMcpServerMappings(request.tools);948const session = await this.sessionInitializer.createDelegatedSession(request, workspaceInfo, { mcpServerMappings }, token);949950if (summary) {951const summaryRef = await this.chatDelegationSummaryService.trackSummaryUsage(session.object.sessionId, summary);952if (summaryRef) {953references.push(summaryRef);954}955}956957setPendingCopilotCLIRequestContext(session.object.sessionId, { prompt, attachments });958void vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {959resource: SessionIdForCLI.getResource(session.object.sessionId),960prompt: userPrompt || request.prompt,961attachedContext: references.map(ref => convertReferenceToVariable(ref, attachments))962}).then(undefined, error => {963clearPendingCopilotCLIRequestContext(session.object.sessionId);964this.logService.error(error, '[CopilotCLIChatSessionContentProvider] Failed to open Copilot CLI session');965});966967stream.markdown(l10n.t('A Copilot CLI session has begun working on your request. Follow its progress in the sessions list.'));968969return {};970}971972private hasHistoryToSummarize(history: readonly (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]): boolean {973if (!history || history.length === 0) {974return false;975}976const allResponsesEmpty = history.every(turn => {977if (turn instanceof vscode.ChatResponseTurn) {978return turn.response.length === 0;979}980return true;981});982return !allResponsesEmpty;983}984985private async recordPushToSession(986session: ICopilotCLISession,987userPrompt: string,988prInfo: vscode.ChatResponsePullRequestPart989): Promise<void> {990// Add user message event991session.addUserMessage(userPrompt);992993// Add assistant message event with embedded PR metadata994const assistantMessage = `A cloud agent has begun working on your request. Follow its progress in the associated chat and pull request.\n<pr_metadata uri="${prInfo.uri?.toString()}" title="${escapeXml(prInfo.title)}" description="${escapeXml(prInfo.description)}" author="${escapeXml(prInfo.author)}" linkTag="${escapeXml(prInfo.linkTag)}"/>`;995session.addUserAssistantMessage(assistantMessage);996}997}998999export function registerCLIChatCommands(1000copilotCLISessionService: ICopilotCLISessionService,1001copilotCLIWorktreeManagerService: IChatSessionWorktreeService,1002gitService: IGitService,1003copilotCliWorkspaceSession: IChatSessionWorkspaceFolderService,1004contentProvider: CopilotCLIChatSessionContentProvider,1005folderRepositoryManager: IFolderRepositoryManager,1006copilotCLIFolderMruService: IChatFolderMruService,1007envService: INativeEnvService,1008fileSystemService: IFileSystemService,1009sessionTracker: ICopilotCLISessionTracker,1010terminalIntegration: ICopilotCLITerminalIntegration,1011logService: ILogService1012): IDisposable {1013const disposableStore = new DisposableStore();10141015async function deleteSessionById(id: string): Promise<void> {1016const worktree = await copilotCLIWorktreeManagerService.getWorktreeProperties(id);1017const worktreePath = await copilotCLIWorktreeManagerService.getWorktreePath(id);10181019await copilotCLISessionService.deleteSession(id);1020await copilotCliWorkspaceSession.deleteTrackedWorkspaceFolder(id);10211022if (worktreePath) {1023const worktreeExists = await fileSystemService.stat(worktreePath).then(() => true, () => false);1024if (worktreeExists) {1025try {1026const repository = worktree ? await gitService.getRepository(vscode.Uri.file(worktree.repositoryPath), true) : undefined;1027if (!repository) {1028throw new Error(l10n.t('No active repository found to delete worktree.'));1029}1030await gitService.deleteWorktree(repository.rootUri, worktreePath.fsPath);1031} catch (error) {1032vscode.window.showErrorMessage(l10n.t('Failed to delete worktree: {0}', error instanceof Error ? error.message : String(error)));1033}1034}1035}10361037await contentProvider.refreshSession({ reason: 'delete', sessionId: id });1038}10391040// Terminal integration setup: resolve session dirs for terminal links.1041disposableStore.add(terminalIntegration);1042terminalIntegration.setSessionDirResolver(terminal =>1043resolveSessionDirsForTerminal(sessionTracker, terminal)1044);1045disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.delete', async (sessionItem?: vscode.ChatSessionItem) => {1046if (sessionItem?.resource) {1047const id = SessionIdForCLI.parse(sessionItem.resource);1048const worktreePath = await copilotCLIWorktreeManagerService.getWorktreePath(id);10491050const confirmMessage = worktreePath1051? l10n.t('Are you sure you want to delete the session and its associated worktree?')1052: l10n.t('Are you sure you want to delete the session?');10531054const deleteLabel = l10n.t('Delete');1055const result = await vscode.window.showWarningMessage(1056confirmMessage,1057{ modal: true },1058deleteLabel1059);10601061if (result === deleteLabel) {1062await deleteSessionById(id);1063}1064}1065}));1066disposableStore.add(vscode.commands.registerCommand('agents.github.copilot.cli.deleteSessions', async (sessionItems?: vscode.ChatSessionItem[], options?: { skipConfirmation?: boolean }) => {1067if (!sessionItems?.length) {1068return;1069}10701071if (!options?.skipConfirmation) {1072const deleteLabel = l10n.t('Delete');1073const confirmMessage = sessionItems.length === 11074? l10n.t('Are you sure you want to delete the session?')1075: l10n.t('Are you sure you want to delete {0} sessions?', sessionItems.length);1076const result = await vscode.window.showWarningMessage(1077confirmMessage,1078{ modal: true },1079deleteLabel1080);1081if (result !== deleteLabel) {1082return;1083}1084}10851086for (const sessionItem of sessionItems) {1087if (sessionItem.resource) {1088const id = SessionIdForCLI.parse(sessionItem.resource);1089await deleteSessionById(id);1090}1091}1092}));1093disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.resumeInTerminal', async (sessionItem?: vscode.ChatSessionItem) => {1094if (sessionItem?.resource) {1095const id = SessionIdForCLI.parse(sessionItem.resource);1096const existingTerminal = await sessionTracker.getTerminal(id);1097if (existingTerminal) {1098existingTerminal.show();1099return;1100}11011102const terminalName = sessionItem.label || id;1103const cliArgs = ['--resume', id];1104const token = new vscode.CancellationTokenSource();1105try {1106const folderInfo = await folderRepositoryManager.getFolderRepository(id, undefined, token.token);1107const cwd = folderInfo.worktree ?? folderInfo.repository ?? folderInfo.folder;1108const terminal = await terminalIntegration.openTerminal(terminalName, cliArgs, cwd?.fsPath);1109if (terminal) {1110sessionTracker.setSessionTerminal(id, terminal);1111terminalIntegration.setTerminalSessionDir(terminal, Uri.file(getCopilotCLISessionDir(id)));1112}1113} finally {1114token.dispose();1115}1116}1117}));1118disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.rename', async (sessionItem?: vscode.ChatSessionItem) => {1119if (!sessionItem?.resource) {1120return;1121}1122const id = SessionIdForCLI.parse(sessionItem.resource);1123const newTitle = await vscode.window.showInputBox({1124prompt: l10n.t('New agent session title'),1125value: sessionItem.label,1126validateInput: value => {1127if (!value.trim()) {1128return l10n.t('Title cannot be empty');1129}1130return undefined;1131}1132});1133if (newTitle) {1134const trimmedTitle = newTitle.trim();1135if (trimmedTitle) {1136await copilotCLISessionService.renameSession(id, trimmedTitle);1137await contentProvider.refreshSession({ reason: 'update', sessionId: id });1138}1139}1140}));1141disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.setTitle', async (sessionItem?: vscode.ChatSessionItem, title?: string) => {1142if (!sessionItem?.resource || !title) {1143return;1144}1145const trimmedTitle = title.trim();1146if (trimmedTitle) {1147const id = SessionIdForCLI.parse(sessionItem.resource);1148await copilotCLISessionService.renameSession(id, trimmedTitle);1149await contentProvider.refreshSession({ reason: 'update', sessionId: id });1150}1151}));11521153const createCopilotCLITerminal = async (location: TerminalOpenLocation = 'editor', name?: string, cwd?: string): Promise<void> => {1154// TODO@rebornix should be set by CLI1155const terminalName = name || process.env.COPILOTCLI_TERMINAL_TITLE || l10n.t('Copilot CLI');1156await terminalIntegration.openTerminal(terminalName, [], cwd, location);1157};11581159disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.newSession', async () => {1160await createCopilotCLITerminal('editor', l10n.t('Copilot CLI'));1161}));1162disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.newSessionToSide', async () => {1163await createCopilotCLITerminal('editorBeside', l10n.t('Copilot CLI'));1164}));1165disposableStore.add(vscode.commands.registerCommand(OPEN_IN_COPILOT_CLI_COMMAND_ID, async (sourceControlContext?: unknown) => {1166const rootUri = getSourceControlRootUri(sourceControlContext);1167await createCopilotCLITerminal('editor', l10n.t('Copilot CLI'), rootUri?.fsPath);1168}));1169disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.openWorktreeInNewWindow', async (sessionItem?: vscode.ChatSessionItem) => {1170if (!sessionItem?.resource) {1171return;1172}11731174const id = SessionIdForCLI.parse(sessionItem.resource);1175const folderInfo = await folderRepositoryManager.getFolderRepository(id, undefined, CancellationToken.None);1176const folder = folderInfo.worktree ?? folderInfo.repository ?? folderInfo.folder;1177if (folder) {1178await vscode.commands.executeCommand('vscode.openFolder', folder, { forceNewWindow: true });1179}1180}));1181disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.openWorktreeInTerminal', async (sessionItem?: vscode.ChatSessionItem) => {1182if (!sessionItem?.resource) {1183return;1184}11851186const id = SessionIdForCLI.parse(sessionItem.resource);1187const folderInfo = await folderRepositoryManager.getFolderRepository(id, undefined, CancellationToken.None);1188const folder = folderInfo.worktree ?? folderInfo.repository ?? folderInfo.folder;1189if (folder) {1190vscode.window.createTerminal({ cwd: folder }).show();1191}1192}));1193disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.copyWorktreeBranchName', async (sessionItem?: vscode.ChatSessionItem) => {1194if (!sessionItem?.resource) {1195return;1196}11971198const id = SessionIdForCLI.parse(sessionItem.resource);1199const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(id);1200if (worktreeProperties?.branchName) {1201await vscode.env.clipboard.writeText(worktreeProperties.branchName);1202}1203}));1204async function selectFolder() {1205// Open folder picker dialog1206const folderUris = await vscode.window.showOpenDialog({1207canSelectFiles: false,1208canSelectFolders: true,1209canSelectMany: false,1210openLabel: l10n.t('Open Folder...'),1211});12121213return folderUris && folderUris.length > 0 ? folderUris[0] : undefined;1214}12151216function getSourceControlRootUri(sourceControlContext?: unknown): vscode.Uri | undefined {1217if (!sourceControlContext) {1218return undefined;1219}12201221if (Array.isArray(sourceControlContext)) {1222return getSourceControlRootUri(sourceControlContext[0]);1223}12241225if (isUri(sourceControlContext)) {1226return sourceControlContext;1227}12281229if (typeof sourceControlContext !== 'object') {1230return undefined;1231}12321233const candidate = sourceControlContext as {1234rootUri?: unknown;1235sourceControl?: { rootUri?: unknown };1236repository?: { rootUri?: unknown };1237};12381239if (isUri(candidate.rootUri)) {1240return candidate.rootUri;1241}12421243if (isUri(candidate.sourceControl?.rootUri)) {1244return candidate.sourceControl.rootUri;1245}12461247if (isUri(candidate.repository?.rootUri)) {1248return candidate.repository.rootUri;1249}12501251return undefined;1252}12531254// Command handler receives `{ inputState, sessionResource }` context args (new API)1255disposableStore.add(vscode.commands.registerCommand(OPEN_REPOSITORY_COMMAND_ID, async ({ inputState }: { inputState: vscode.ChatSessionInputState; sessionResource: vscode.Uri | undefined }) => {1256let selectedFolderUri: Uri | undefined = undefined;1257const mruItems = await copilotCLIFolderMruService.getRecentlyUsedFolders(CancellationToken.None);12581259if (mruItems.length === 0) {1260selectedFolderUri = await selectFolder();1261} else {1262type RecentFolderQuickPickItem = vscode.QuickPickItem & ({ folderUri: vscode.Uri; openFolder: false } | { folderUri: undefined; openFolder: true });1263const items: RecentFolderQuickPickItem[] = mruItems1264.map(item => {1265const optionItem = item.repository1266? toRepositoryOptionItem(item.folder)1267: toWorkspaceFolderOptionItem(item.folder, basename(item.folder));12681269return {1270label: optionItem.name,1271description: `~/${relative(envService.userHome.fsPath, item.folder.fsPath)}`,1272iconPath: optionItem.icon,1273folderUri: item.folder,1274openFolder: false1275};1276});12771278items.unshift({1279label: l10n.t('Open Folder...'),1280iconPath: new vscode.ThemeIcon('folder-opened'),1281folderUri: undefined,1282openFolder: true1283}, {1284kind: vscode.QuickPickItemKind.Separator,1285label: '',1286folderUri: undefined,1287openFolder: true1288});12891290const selectedFolder = new DeferredPromise<Uri | undefined>();1291const disposables = new DisposableStore();1292const quickPick = disposables.add(vscode.window.createQuickPick<RecentFolderQuickPickItem>());1293quickPick.items = items;1294quickPick.placeholder = l10n.t('Select a recent folder');1295quickPick.matchOnDescription = true;1296quickPick.ignoreFocusOut = true;1297quickPick.matchOnDetail = true;1298quickPick.show();1299disposables.add(quickPick.onDidHide(() => {1300selectedFolder.complete(undefined);1301}));1302disposables.add(quickPick.onDidAccept(async () => {1303if (quickPick.selectedItems.length === 0 && !quickPick.value) {1304selectedFolder.complete(undefined);1305quickPick.hide();1306} else if (quickPick.selectedItems.length && quickPick.selectedItems[0].folderUri) {1307selectedFolder.complete(quickPick.selectedItems[0].folderUri);1308quickPick.hide();1309} else if (quickPick.selectedItems.length && quickPick.selectedItems[0].openFolder) {1310selectedFolder.complete(await selectFolder());1311quickPick.hide();1312} else if (quickPick.value) {1313const fileOrFolder = vscode.Uri.file(quickPick.value);1314try {1315const stat = await vscode.workspace.fs.stat(fileOrFolder);1316let directory: Uri | undefined = undefined;1317if (stat.type & vscode.FileType.Directory) {1318quickPick.hide();1319directory = fileOrFolder;1320} else if (stat.type & vscode.FileType.File) {1321directory = dirname(fileOrFolder);1322}1323if (directory) {1324// Possible user selected a folder thats inside an existing workspace folder.1325selectedFolder.complete(vscode.workspace.getWorkspaceFolder(directory)?.uri || directory);1326quickPick.hide();1327}1328} catch {1329// ignore1330}1331}1332}));1333selectedFolderUri = await selectedFolder.p;1334disposables.dispose();1335}13361337if (!selectedFolderUri) {1338return;1339}1340if (!(await checkPathExists(selectedFolderUri, fileSystemService))) {1341await copilotCLIFolderMruService.deleteRecentlyUsedFolder(selectedFolderUri);1342const message = l10n.t('The path \'{0}\' does not exist on this computer.', selectedFolderUri.fsPath);1343vscode.window.showErrorMessage(l10n.t('Path does not exist'), { modal: true, detail: message });1344return;1345}13461347// First check if user trusts the folder.1348const trusted = await vscode.workspace.requestResourceTrust({1349uri: selectedFolderUri,1350message: UNTRUSTED_FOLDER_MESSAGE1351});1352if (!trusted) {1353return;1354}135513561357// Update inputState groups with newly selected folder and reload branches1358if (inputState) {1359await contentProvider.updateInputStateAfterFolderSelection(inputState, selectedFolderUri);1360}1361}));13621363const applyChanges = async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1364const resource = isUri(sessionItemOrResource)1365? sessionItemOrResource1366: sessionItemOrResource?.resource;13671368if (!resource) {1369return;1370}13711372try {1373// Apply changes1374const sessionId = SessionIdForCLI.parse(resource);1375await copilotCLIWorktreeManagerService.applyWorktreeChanges(sessionId);13761377// Close the multi-file diff editor if it's open1378const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1379const worktreePath = worktreeProperties ? Uri.file(worktreeProperties.worktreePath) : undefined;13801381if (worktreePath) {1382// Select the tabs to close1383const multiDiffTabToClose = vscode.window.tabGroups.all.flatMap(g => g.tabs)1384.filter(({ input }) => input instanceof vscode.TabInputTextMultiDiff && input.textDiffs.some(input =>1385extUri.isEqualOrParent(vscode.Uri.file(input.original.fsPath), worktreePath, true) ||1386extUri.isEqualOrParent(vscode.Uri.file(input.modified.fsPath), worktreePath, true)));13871388if (multiDiffTabToClose.length > 0) {1389// Close the tabs1390await vscode.window.tabGroups.close(multiDiffTabToClose, true);1391}1392}13931394// Pick up new git state1395await contentProvider.refreshSession({ reason: 'update', sessionId });1396} catch (error) {1397vscode.window.showErrorMessage(l10n.t('Failed to apply changes to the current workspace. Please stage or commit your changes in the current workspace and try again.'), { modal: true });1398}1399};14001401disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.applyCopilotCLIAgentSessionChanges', applyChanges));1402disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.applyCopilotCLIAgentSessionChanges.apply', applyChanges));14031404const mergeChanges = async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri, syncWithRemote: boolean = false) => {1405const resource = sessionItemOrResource instanceof vscode.Uri1406? sessionItemOrResource1407: sessionItemOrResource?.resource;14081409if (!resource) {1410return;1411}14121413let branchName: string | undefined;1414let worktreePath: string | undefined;1415let baseBranchName: string | undefined;1416let baseWorktreePath: string | undefined;14171418try {1419const sessionId = SessionIdForCLI.parse(resource);1420const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1421if (!worktreeProperties || worktreeProperties.version !== 2) {1422vscode.window.showErrorMessage(l10n.t('Merging changes is only supported for worktree-based sessions.'));1423return;1424}14251426branchName = worktreeProperties.branchName;1427worktreePath = worktreeProperties.worktreePath;1428baseBranchName = worktreeProperties.baseBranchName;1429baseWorktreePath = worktreeProperties.repositoryPath;1430} catch (error) {1431logService.error(`Failed to check worktree properties for merge changes: ${error instanceof Error ? error.message : String(error)}`);1432return;1433}14341435const contextValueSegments: string[] = [];1436contextValueSegments.push(`source branch name: ${branchName}`);1437contextValueSegments.push(`source worktree path: ${worktreePath}`);1438contextValueSegments.push(`target branch name: ${baseBranchName}`);1439contextValueSegments.push(`target worktree path: ${baseWorktreePath}`);14401441const prompt = syncWithRemote1442? `${builtinSlashSCommands.merge} and ${builtinSlashSCommands.sync}`1443: builtinSlashSCommands.merge;14441445await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1446resource,1447prompt,1448attachedContext: [{1449id: 'git-merge-changes',1450value: contextValueSegments.join('\n'),1451icon: new vscode.ThemeIcon('git-merge'),1452fullName: `${branchName} → ${baseBranchName}`,1453kind: 'generic'1454}]1455});1456};14571458disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.mergeCopilotCLIAgentSessionChanges.merge', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1459await mergeChanges(sessionItemOrResource);1460}));14611462disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.mergeCopilotCLIAgentSessionChanges.mergeAndSync', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1463await mergeChanges(sessionItemOrResource, true);1464}));14651466disposableStore.add(vscode.commands.registerCommand('github.copilot.sessions.refreshChanges', async (resource?: vscode.Uri) => {1467if (!resource) {1468return;1469}14701471const sessionId = SessionIdForCLI.parse(resource);1472const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1473const workspaceFolder = await copilotCliWorkspaceSession.getSessionWorkspaceFolder(sessionId);14741475if (!worktreeProperties && !workspaceFolder) {1476return;1477}14781479if (worktreeProperties) {1480// Worktree1481await copilotCLIWorktreeManagerService.setWorktreeProperties(sessionId, {1482...worktreeProperties,1483changes: undefined1484});1485} else if (workspaceFolder) {1486// Workspace1487copilotCliWorkspaceSession.clearWorkspaceChanges(sessionId);1488}14891490await contentProvider.refreshSession({ reason: 'update', sessionId });1491}));14921493disposableStore.add(vscode.commands.registerCommand('github.copilot.sessions.initializeRepository', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1494const resource = sessionItemOrResource instanceof vscode.Uri1495? sessionItemOrResource1496: sessionItemOrResource?.resource;14971498if (!resource) {1499return;1500}15011502const sessionId = SessionIdForCLI.parse(resource);1503const workspaceFolder = await copilotCliWorkspaceSession.getSessionWorkspaceFolder(sessionId);1504if (!workspaceFolder) {1505return;1506}15071508const repository = await gitService.initRepository(workspaceFolder);1509if (!repository) {1510return;1511}15121513const repositoryProperties = repository.state.HEAD?.name1514? {1515repositoryPath: repository.rootUri.fsPath,1516branchName: repository.state.HEAD.name1517} satisfies RepositoryProperties1518: undefined;15191520await copilotCliWorkspaceSession.trackSessionWorkspaceFolder(sessionId, workspaceFolder.fsPath, repositoryProperties);1521copilotCliWorkspaceSession.clearWorkspaceChanges(sessionId);15221523await contentProvider.refreshSession({ reason: 'update', sessionId });1524}));15251526disposableStore.add(vscode.commands.registerCommand('github.copilot.sessions.commit', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1527const resource = sessionItemOrResource instanceof vscode.Uri1528? sessionItemOrResource1529: sessionItemOrResource?.resource;15301531if (!resource) {1532return;1533}15341535await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1536resource,1537prompt: builtinSlashSCommands.commit,1538});1539}));15401541disposableStore.add(vscode.commands.registerCommand('github.copilot.sessions.commitAndSync', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1542const resource = sessionItemOrResource instanceof vscode.Uri1543? sessionItemOrResource1544: sessionItemOrResource?.resource;15451546if (!resource) {1547return;1548}15491550await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1551resource,1552prompt: `${builtinSlashSCommands.commit} and ${builtinSlashSCommands.sync}`,1553});1554}));15551556disposableStore.add(vscode.commands.registerCommand('github.copilot.sessions.sync', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1557const resource = sessionItemOrResource instanceof vscode.Uri1558? sessionItemOrResource1559: sessionItemOrResource?.resource;15601561if (!resource) {1562return;1563}15641565await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1566resource,1567prompt: builtinSlashSCommands.sync,1568});1569}));15701571disposableStore.add(vscode.commands.registerCommand('github.copilot.sessions.discardChanges', async (sessionResource: vscode.Uri, ref: string, ...resources: vscode.Uri[]) => {1572if (!isUri(sessionResource) || !ref || resources.length === 0 || resources.some(r => !isUri(r))) {1573return;1574}15751576const sessionId = SessionIdForCLI.parse(sessionResource);1577const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1578const workspaceFolder = await copilotCliWorkspaceSession.getSessionWorkspaceFolder(sessionId);15791580const repositoryUri = worktreeProperties ? Uri.file(worktreeProperties.worktreePath) : workspaceFolder;1581const repository = repositoryUri ? await gitService.getRepository(repositoryUri) : undefined;1582if (!repository) {1583return;1584}15851586const confirmAction = l10n.t('Discard Changes');1587const message = resources.length === 11588? l10n.t('Are you sure you want to discard the changes in \'{0}\'? This action cannot be undone.', basename(resources[0]))1589: l10n.t('Are you sure you want to discard the changes in these {0} files? This action cannot be undone.', resources.length);15901591const choice = await vscode.window.showWarningMessage(message, { modal: true }, confirmAction);1592if (choice !== confirmAction) {1593return;1594}15951596await gitService.restore(repository.rootUri, resources.map(r => r.fsPath), { ref });1597}));15981599disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.createPullRequestCopilotCLIAgentSession.createPR', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1600const resource = sessionItemOrResource instanceof vscode.Uri1601? sessionItemOrResource1602: sessionItemOrResource?.resource;16031604if (!resource) {1605return;1606}16071608try {1609const sessionId = SessionIdForCLI.parse(resource);1610const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1611if (!worktreeProperties || worktreeProperties.version !== 2) {1612vscode.window.showErrorMessage(l10n.t('Creating a pull request is only supported for worktree-based sessions.'));1613return;1614}1615} catch (error) {1616logService.error(`Failed to check worktree properties for createPR: ${error instanceof Error ? error.message : String(error)}`);1617return;1618}16191620await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1621resource,1622prompt: builtinSlashSCommands.createPr,1623});1624}));16251626disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.createDraftPullRequestCopilotCLIAgentSession.createDraftPR', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1627const resource = sessionItemOrResource instanceof vscode.Uri1628? sessionItemOrResource1629: sessionItemOrResource?.resource;16301631if (!resource) {1632return;1633}16341635try {1636const sessionId = SessionIdForCLI.parse(resource);1637const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1638if (!worktreeProperties || worktreeProperties.version !== 2) {1639vscode.window.showErrorMessage(l10n.t('Creating a draft pull request is only supported for worktree-based sessions.'));1640return;1641}1642} catch (error) {1643logService.error(`Failed to check worktree properties for createDraftPR: ${error instanceof Error ? error.message : String(error)}`);1644return;1645}16461647await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1648resource,1649prompt: builtinSlashSCommands.createDraftPr,1650});1651}));16521653disposableStore.add(vscode.commands.registerCommand('github.copilot.chat.createPullRequestCopilotCLIAgentSession.updatePR', async (sessionItemOrResource?: vscode.ChatSessionItem | vscode.Uri) => {1654const resource = sessionItemOrResource instanceof vscode.Uri1655? sessionItemOrResource1656: sessionItemOrResource?.resource;16571658if (!resource) {1659return;1660}16611662let pullRequestUrl: string | undefined = undefined;16631664try {1665const sessionId = SessionIdForCLI.parse(resource);1666const worktreeProperties = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1667if (!worktreeProperties || worktreeProperties.version !== 2) {1668vscode.window.showErrorMessage(l10n.t('Updating a pull request is only supported for worktree-based sessions.'));1669return;1670}16711672pullRequestUrl = worktreeProperties.pullRequestUrl;1673} catch (error) {1674logService.error(`Failed to check worktree properties for updatePR: ${error instanceof Error ? error.message : String(error)}`);1675return;1676}16771678if (!pullRequestUrl) {1679vscode.window.showErrorMessage(l10n.t('No pull request URL found for this session.'));1680return;1681}16821683await vscode.commands.executeCommand('workbench.action.chat.openSessionWithPrompt.copilotcli', {1684resource,1685prompt: builtinSlashSCommands.updatePr,1686attachedContext: [{1687id: 'github-pull-request',1688fullName: pullRequestUrl,1689icon: new vscode.ThemeIcon('git-pull-request'),1690value: vscode.Uri.parse(pullRequestUrl),1691kind: 'generic'1692}]1693});1694}));16951696disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.commitToWorktree', async (args?: { worktreeUri?: vscode.Uri; fileUri?: vscode.Uri }) => {1697logService.trace(`[commitToWorktree] Command invoked, args: ${JSON.stringify(args, null, 2)}`);1698if (!args?.worktreeUri || !args?.fileUri) {1699logService.debug('[commitToWorktree] Missing worktreeUri or fileUri, aborting');1700return;1701}17021703const worktreeUri = vscode.Uri.from(args.worktreeUri);1704const fileUri = vscode.Uri.from(args.fileUri);1705try {1706const fileName = basename(fileUri);1707await gitService.add(worktreeUri, [fileUri.fsPath]);1708logService.debug(`[commitToWorktree] Committing with message: Update customization: ${fileName}`);1709await gitService.commit(worktreeUri, l10n.t('Update customization: {0}', fileName), { noVerify: true, signCommit: false });1710logService.trace('[commitToWorktree] Commit successful');17111712// Clear the worktree changes cache so getWorktreeChanges() recomputes1713const sessionIds = await contentProvider.getAssociatedSessions(worktreeUri);1714await Promise.all(sessionIds.map(async sessionId => {1715const props = await copilotCLIWorktreeManagerService.getWorktreeProperties(sessionId);1716if (props) {1717await copilotCLIWorktreeManagerService.setWorktreeProperties(sessionId, { ...props, changes: undefined });1718} else {1719logService.error('[commitToWorktree] No worktree properties found for session:', sessionId);1720}1721}));17221723logService.trace('[commitToWorktree] Notifying sessions change');1724if (sessionIds.length) {1725await contentProvider.refreshSession({ reason: 'update', sessionIds });1726}1727} catch (error) {1728const { stdout = '', stderr = '', gitErrorCode } = error as { stdout?: string; stderr?: string; gitErrorCode?: string };1729const normalizedStdout = stdout.toLowerCase();1730const normalizedStderr = stderr.toLowerCase();1731if (normalizedStdout.includes('nothing to commit') || normalizedStderr.includes('nothing to commit') || gitErrorCode === 'NoLocalChanges' || gitErrorCode === 'NotAGitRepository') {1732logService.debug('[commitToWorktree] Nothing to commit or non-applicable repository state, skipping');1733return;1734}1735logService.error('[commitToWorktree] Error:', error);1736vscode.window.showErrorMessage(l10n.t('Failed to commit: {0}', error instanceof Error ? error.message : String(error)));1737}1738}));17391740disposableStore.add(vscode.commands.registerCommand('github.copilot.cli.sessions.commitToRepository', async (args?: { repositoryUri?: vscode.Uri; fileUri?: vscode.Uri }) => {1741logService.trace(`[commitToRepository] Command invoked, args: ${JSON.stringify(args, null, 2)}`);1742if (!args?.repositoryUri || !args?.fileUri) {1743logService.debug('[commitToRepository] Missing repositoryUri or fileUri, aborting');1744return;1745}17461747const repositoryUri = vscode.Uri.from(args.repositoryUri);1748const fileUri = vscode.Uri.from(args.fileUri);1749try {1750const fileName = basename(fileUri);1751await gitService.add(repositoryUri, [fileUri.fsPath]);17521753const message = l10n.t('Update customization: {0}', fileName);1754logService.debug(`[commitToRepository] Committing with message: ${message}`);1755await gitService.commit(repositoryUri, message, { noVerify: true, signCommit: false });1756logService.trace('[commitToRepository] Commit successful');1757} catch (error) {1758const stderr = (error as { stderr?: string })?.stderr ?? '';1759const stdout = (error as { stdout?: string })?.stdout ?? '';1760const gitErrorCode = (error as { gitErrorCode?: string })?.gitErrorCode;17611762// Benign: nothing was staged or no local changes to commit1763if (stderr.includes('nothing to commit') || stdout.includes('nothing to commit') || gitErrorCode === 'NoLocalChanges') {1764logService.debug('[commitToRepository] Nothing to commit, skipping');1765return;1766}17671768// Benign: repository URI doesn't point to a git repo1769if (gitErrorCode === 'NotAGitRepository') {1770logService.debug('[commitToRepository] Not a git repository, skipping');1771return;1772}17731774logService.error('[commitToRepository] Error:', error);1775vscode.window.showErrorMessage(l10n.t("Could not save your customization to the default branch — this can happen when the worktree and the base repository have conflicting changes. Your change is still saved in this session's worktree."));1776}1777}));17781779return disposableStore;1780}17811782/**1783* Check if a path exists and is a directory.1784*/1785async function checkPathExists(filePath: vscode.Uri, fileSystemService: IFileSystemService): Promise<boolean> {1786try {1787const stat = await fileSystemService.stat(filePath);1788return stat.type === vscode.FileType.Directory;1789} catch {1790return false;1791}1792}17931794function isUnknownEventTypeError(error: unknown): boolean {1795const message = error instanceof Error ? error.message : String(error);1796return /Unknown event type:/i.test(message);1797}179817991800