Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4540,6 +4540,14 @@
"advanced"
]
},
"github.copilot.chat.cli.aiGenerateBranchNames.enabled": {
"type": "boolean",
"default": false,
"markdownDescription": "%github.copilot.config.cli.aiGenerateBranchNames.enabled%",
"tags": [
"advanced"
]
},
"github.copilot.chat.cli.forkSessions.enabled": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@
"github.copilot.config.cli.branchSupport.enabled": "Enable branch support for Copilot CLI.",
"github.copilot.config.cli.forkSessions.enabled": "Enable forking sessions in Copilot CLI.",
"github.copilot.config.cli.planExitMode.enabled": "Enable Plan Mode exit handling in Copilot CLI.",
"github.copilot.config.cli.aiGenerateBranchNames.enabled": "Enable AI-generated branch names in Copilot CLI.",
"github.copilot.config.cli.isolationOption.enabled": "Enable the isolation mode option for Copilot CLI. When enabled, users can choose between Worktree and Workspace modes.",
"github.copilot.config.cli.autoCommit.enabled": "Enable automatic commit for Copilot CLI. When enabled, changes made by Copilot CLI will be automatically committed to the repository at the end of each turn.",
"github.copilot.config.cli.sessionController.enabled": "Enable the new session controller API for Copilot CLI. Requires VS Code reload.",
Expand Down
27 changes: 16 additions & 11 deletions src/extension/chatSessions/copilotcli/node/copilotcliSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,20 +472,25 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
this._sdkSession.respondToExitPlanMode(event.data.requestId, { approved: false });
return;
}
const actionDescriptions: Record<ActionType, string> = {
'autopilot': l10n.t('Auto-approve all tool calls and continue until the task is done'),
'interactive': l10n.t('Let the agent continue in interactive mode, asking for user input and approval for each action.'),
'exit_only': l10n.t('Exit plan mode, but do not execute the plan. I will execute the plan myself after reviewing it.'),
'autopilot_fleet': l10n.t('Auto-approve all tool calls, including fleet management actions, and continue until the task is done.'),
};
const actionDescriptions: Record<string, { label: string; description: string }> = {
'autopilot': { label: 'Autopilot', description: l10n.t('Auto-approve all tool calls and continue until the task is done') },
'interactive': { label: 'Interactive', description: l10n.t('Let the agent continue in interactive mode, asking for input and approval for each action.') },
'exit_only': { label: 'Approve and exit', description: l10n.t('Exit planning, but do not execute the plan. I will execute the plan myself.') },
'autopilot_fleet': { label: 'Autopilot Fleet', description: l10n.t('Auto-approve all tool calls, including fleet management actions, and continue until the task is done.') },
} satisfies Record<ActionType, { label: string; description: string }>;

const approved = true;
event.data.actions;
try {
const planPath = this._sdkSession.getPlanPath();

const userInputRequest: IQuestion = {
question: l10n.t('Approve this plan?'),
question: planPath ? l10n.t('Approve this plan {0}?', `[Plan.md](${Uri.file(planPath).toString()})`) : l10n.t('Approve this plan?'),
header: l10n.t('Approve this plan?'),
options: event.data.actions.map(a => ({ label: (actionDescriptions as Record<string, string>)[a] ?? a, recommended: a === event.data.recommendedAction })),
options: event.data.actions.map(a => ({
label: actionDescriptions[a]?.label ?? a,
recommended: a === event.data.recommendedAction,
description: actionDescriptions[a]?.description ?? '',
})),
allowFreeformInput: true,
};
const answer = await this._userQuestionHandler.askUserQuestion(userInputRequest, this._toolInvocationToken as unknown as never, token);
Expand All @@ -498,8 +503,8 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
this._sdkSession.respondToExitPlanMode(event.data.requestId, { approved: false, feedback: answer.freeText });
} else {
let selectedAction: ActionType = answer.selected[0] as ActionType;
Object.entries(actionDescriptions).forEach(([action, description]) => {
if (description === selectedAction) {
Object.entries(actionDescriptions).forEach(([action, item]) => {
if (item.label === selectedAction) {
selectedAction = action as ActionType;
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class MockSdkSession {
async getSelectedModel() { return this._selectedModel; }
async setSelectedModel(model: string) { this._selectedModel = model; }
async getEvents() { return []; }
getPlanPath(): string | null { return null; }
}

function createWorkspaceService(root: string): IWorkspaceService {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class UserQuestionHandler implements IUserQuestionHandler {
// Log all available keys in carouselAnswers for debugging
this._logService.trace(`[AskQuestionsTool] Question & answers ${question.question}, Answers object: ${JSON.stringify(carouselAnswers)}`);

const answer = carouselAnswers.answers[question.question];
const answer = carouselAnswers.answers[question.question] ?? carouselAnswers.answers[question.header];
if (answer === undefined) {
return undefined;
} else if (answer.freeText) {
Expand Down
10 changes: 9 additions & 1 deletion src/extension/chatSessions/vscode-node/chatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib
const gitService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IGitService));
const sessionTracker = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(ICopilotCLISessionTracker));
const terminalIntegration = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(ICopilotCLITerminalIntegration));
const branchNameGenerator = copilotcliAgentInstaService.createInstance(GitBranchNameGenerator);
const aiGeneratedBranchNames = instantiationService.invokeFunction(accessor =>
accessor.get(IConfigurationService).getConfig(ConfigKey.Advanced.CLIAIGenerateBranchNames)
);
const branchNameGenerator = aiGeneratedBranchNames ? copilotcliAgentInstaService.createInstance(GitBranchNameGenerator) : undefined;

const copilotcliChatSessionParticipant = this._register(copilotcliAgentInstaService.createInstance(
CopilotCLIChatSessionParticipant,
Expand Down Expand Up @@ -260,13 +263,18 @@ export class ChatSessionsContrib extends Disposable implements IExtensionContrib
const gitService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IGitService));
const gitExtensionService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IGitExtensionService));
const toolsService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IToolsService));
const aiGeneratedBranchNames = instantiationService.invokeFunction(accessor =>
accessor.get(IConfigurationService).getConfig(ConfigKey.Advanced.CLIAIGenerateBranchNames)
);
const branchNameGenerator = aiGeneratedBranchNames ? copilotcliAgentInstaService.createInstance(GitBranchNameGenerator) : undefined;

const copilotcliChatSessionParticipant = this._register(copilotcliAgentInstaService.createInstance(
CopilotCLIChatSessionParticipantV1,
copilotcliChatSessionContentProvider,
promptResolver,
copilotcliSessionItemProvider,
cloudSessionProvider,
branchNameGenerator
));
const copilotCLISessionService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(ICopilotCLISessionService));
const copilotCLIWorktreeManagerService = copilotcliAgentInstaService.invokeFunction(accessor => accessor.get(IChatSessionWorktreeService));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
private readonly contentProvider: CopilotCLIChatSessionContentProvider,
private readonly promptResolver: CopilotCLIPromptResolver,
private readonly cloudSessionProvider: CopilotCloudSessionsProvider | undefined,
private readonly branchNameGenerator: GitBranchNameGenerator,
private readonly branchNameGenerator: GitBranchNameGenerator | undefined,
@IGitService private readonly gitService: IGitService,
@ICopilotCLIModels private readonly copilotCLIModels: ICopilotCLIModels,
@ICopilotCLIAgents private readonly copilotCLIAgents: ICopilotCLIAgents,
Expand Down Expand Up @@ -1043,7 +1043,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
history: [requestTurn],
yieldRequested: false,
};
const branchNamePromise = isNewSession && request.prompt ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);
const branchNamePromise = (isNewSession && request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);
const [model, agent] = await Promise.all([
this.getModelId(request, token),
this.getAgent(id, request, token),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type { Attachment, SessionOptions, SweCustomAgent } from '@github/copilot/sdk';
import * as l10n from '@vscode/l10n';
import * as vscode from 'vscode';
import { ChatExtendedRequestHandler, ChatSessionProviderOptionItem, Uri } from 'vscode';
import { ChatExtendedRequestHandler, ChatRequestTurn2, ChatSessionProviderOptionItem, Uri } from 'vscode';
import { IRunCommandExecutionService } from '../../../platform/commands/common/runCommandExecutionService';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { INativeEnvService } from '../../../platform/env/common/envService';
Expand All @@ -33,6 +33,7 @@ import { StopWatch } from '../../../util/vs/base/common/stopwatch';
import { URI } from '../../../util/vs/base/common/uri';
import { EXTENSION_ID } from '../../common/constants';
import { ChatVariablesCollection, extractDebugTargetSessionIds, isPromptFile } from '../../prompt/common/chatVariablesCollection';
import { GitBranchNameGenerator } from '../../prompt/node/gitBranch';
import { IToolsService } from '../../tools/common/toolsService';
import { IChatSessionMetadataStore, RepositoryProperties, StoredModeInstructions } from '../common/chatSessionMetadataStore';
import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService';
Expand Down Expand Up @@ -1094,6 +1095,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
private readonly promptResolver: CopilotCLIPromptResolver,
private readonly sessionItemProvider: CopilotCLIChatSessionItemProvider,
private readonly cloudSessionProvider: CopilotCloudSessionsProvider | undefined,
private readonly branchNameGenerator: GitBranchNameGenerator | undefined,
@IGitService private readonly gitService: IGitService,
@ICopilotCLIModels private readonly copilotCLIModels: ICopilotCLIModels,
@ICopilotCLIAgents private readonly copilotCLIAgents: ICopilotCLIAgents,
Expand Down Expand Up @@ -1302,12 +1304,18 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
return {};
}

const requestTurn = new ChatRequestTurn2(request.prompt ?? '', request.command, [], '', [], [], undefined, undefined, undefined);
const fakeContext: vscode.ChatContext = {
history: [requestTurn],
yieldRequested: false,
};
const branchNamePromise = (isUntitled && request.prompt && this.branchNameGenerator) ? this.branchNameGenerator.generateBranchName(fakeContext, token) : Promise.resolve(undefined);
const [model, agent] = await Promise.all([
this.getModelId(request, token),
this.getAgent(id, request, token),
]);

const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { model, agent }, disposables, token);
const sessionResult = await this.getOrCreateSession(request, chatSessionContext, stream, { model, agent, branchName: branchNamePromise }, disposables, token);
const session = sessionResult.session;
if (session) {
disposables.add(session);
Expand Down Expand Up @@ -1365,7 +1373,10 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
} else {
// Construct the full prompt with references to be sent to CLI.
const { prompt, attachments } = await this.promptResolver.resolvePrompt(request, undefined, [], session.object.workspace, [], token);
await session.object.handleRequest(request, { prompt }, attachments, model, authInfo, token);
const input = (request.command && (copilotCLICommands as readonly string[]).includes(request.command))
? { command: request.command as CopilotCLICommand, prompt }
: { prompt: prompt };
await session.object.handleRequest(request, input, attachments, model, authInfo, token);
await this.commitWorktreeChangesIfNeeded(request, session.object, token);
}

Expand Down Expand Up @@ -1671,13 +1682,13 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
}
}

private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { model: string | undefined; agent: SweCustomAgent | undefined }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; trusted: boolean }> {
private async getOrCreateSession(request: vscode.ChatRequest, chatSessionContext: vscode.ChatSessionContext, stream: vscode.ChatResponseStream, options: { model: string | undefined; agent: SweCustomAgent | undefined; branchName: Promise<string | undefined> }, disposables: DisposableStore, token: vscode.CancellationToken): Promise<{ session: IReference<ICopilotCLISession> | undefined; trusted: boolean }> {
const { resource } = chatSessionContext.chatSessionItem;
const existingSessionId = this.sessionItemProvider.untitledSessionIdMapping.get(SessionIdForCLI.parse(resource));
const id = existingSessionId ?? SessionIdForCLI.parse(resource);
const isNewSession = chatSessionContext.isUntitled && !existingSessionId;

const { workspaceInfo, cancelled, trusted } = await this.getOrInitializeWorkingDirectory(chatSessionContext, stream, request.toolInvocationToken, token);
const { workspaceInfo, cancelled, trusted } = await this.getOrInitializeWorkingDirectory(chatSessionContext, options.branchName, stream, request.toolInvocationToken, token);
const workingDirectory = getWorkingDirectory(workspaceInfo);
const worktreeProperties = workspaceInfo.worktreeProperties;
if (cancelled || token.isCancellationRequested) {
Expand Down Expand Up @@ -1752,6 +1763,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {

private async getOrInitializeWorkingDirectory(
chatSessionContext: vscode.ChatSessionContext | undefined,
branchName: Promise<string | undefined>,
stream: vscode.ChatResponseStream,
toolInvocationToken: vscode.ChatParticipantToolToken,
token: vscode.CancellationToken
Expand All @@ -1770,7 +1782,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
// Use FolderRepositoryManager to initialize folder/repository with worktree creation
const branch = _sessionBranch.get(id);
const isolation = _sessionIsolation.get(id) ?? undefined;
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(id, { stream, toolInvocationToken, branch: branch ?? undefined, isolation, folder: undefined }, token);
folderInfo = await this.folderRepositoryManager.initializeFolderRepository(id, { stream, toolInvocationToken, branch: branch ?? undefined, isolation, folder: undefined, newBranch: branchName }, token);
} else {
// Existing session - use getFolderRepository for resolution with trust check
folderInfo = await this.folderRepositoryManager.getFolderRepository(id, { promptForTrust: true, stream }, token);
Expand Down Expand Up @@ -1821,7 +1833,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
})();

const [{ workspaceInfo, cancelled }, model, agent] = await Promise.all([
this.getOrInitializeWorkingDirectory(undefined, stream, request.toolInvocationToken, token),
this.getOrInitializeWorkingDirectory(undefined, Promise.resolve(undefined), stream, request.toolInvocationToken, token),
this.getModelId(request, token), // prefer model in request, as we're delegating from another session here.
this.getAgent(undefined, undefined, token)
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
promptResolver,
itemProvider,
cloudProvider,
undefined,
git,
models as unknown as ICopilotCLIModels,
new NullCopilotCLIAgents(),
Expand Down Expand Up @@ -758,6 +759,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
promptResolver,
itemProvider,
cloudProvider,
undefined,
git,
models as unknown as ICopilotCLIModels,
new NullCopilotCLIAgents(),
Expand Down Expand Up @@ -1904,6 +1906,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
promptResolver,
itemProvider,
cloudProvider,
undefined,
git,
models as unknown as ICopilotCLIModels,
agents,
Expand Down Expand Up @@ -2036,6 +2039,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
promptResolver,
itemProvider,
cloudProvider,
undefined,
git,
models as unknown as ICopilotCLIModels,
new NullCopilotCLIAgents(),
Expand Down
1 change: 1 addition & 0 deletions src/platform/configuration/common/configurationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,7 @@ export namespace ConfigKey {
export const UseResponsesApiTruncation = defineAndMigrateSetting<boolean | undefined>('chat.advanced.useResponsesApiTruncation', 'chat.useResponsesApiTruncation', false);
export const OmitBaseAgentInstructions = defineAndMigrateSetting<boolean>('chat.advanced.omitBaseAgentInstructions', 'chat.omitBaseAgentInstructions', false);
export const CLIPlanExitModeEnabled = defineSetting<boolean>('chat.cli.planExitMode.enabled', ConfigType.Simple, false);
export const CLIAIGenerateBranchNames = defineSetting<boolean>('chat.cli.aiGenerateBranchNames.enabled', ConfigType.Simple, false);
export const CLIForkSessionsEnabled = defineSetting<boolean>('chat.cli.forkSessions.enabled', ConfigType.Simple, false);
export const CLIMCPServerEnabled = defineAndMigrateSetting<boolean | undefined>('chat.advanced.cli.mcp.enabled', 'chat.cli.mcp.enabled', true);
export const CLIBranchSupport = defineSetting<boolean>('chat.cli.branchSupport.enabled', ConfigType.Simple, false);
Expand Down
Loading