Skip to content

Commit 8b88310

Browse files
Enhance Copilot CLI session management and permissions handling (#310495)
* Enhance Copilot CLI session management and permissions handling * Update src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * updates * Fixes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent d0eea83 commit 8b88310

File tree

6 files changed

+54
-6
lines changed

6 files changed

+54
-6
lines changed

extensions/copilot/src/extension/chatSessions/copilotcli/node/copilotcliSession.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Attachment, SendOptions, Session, SessionOptions } from '@github/c
77
import * as l10n from '@vscode/l10n';
88
import type * as vscode from 'vscode';
99
import type { ChatParticipantToolToken } from 'vscode';
10+
import { IRunCommandExecutionService } from '../../../../platform/commands/common/runCommandExecutionService';
1011
import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService';
1112
import { ILogService } from '../../../../platform/log/common/logService';
1213
import { GenAiMetrics } from '../../../../platform/otel/common/genAiMetrics';
@@ -29,6 +30,7 @@ import { IChatSessionMetadataStore } from '../../common/chatSessionMetadataStore
2930
import { ExternalEditTracker } from '../../common/externalEditTracker';
3031
import { getWorkingDirectory, isIsolationEnabled, IWorkspaceInfo } from '../../common/workspaceInfo';
3132
import { enrichToolInvocationWithSubagentMetadata, isCopilotCliEditToolCall, isCopilotCLIToolThatCouldRequirePermissions, processToolExecutionComplete, processToolExecutionStart, ToolCall, updateTodoList } from '../common/copilotCLITools';
33+
import { SessionIdForCLI } from '../common/utils';
3234
import type { CopilotCliBridgeSpanProcessor } from './copilotCliBridgeSpanProcessor';
3335
import { ICopilotCLIImageSupport } from './copilotCLIImageSupport';
3436
import { handleExitPlanMode } from './exitPlanModeHandler';
@@ -93,6 +95,12 @@ export interface ICopilotCLISession extends IDisposable {
9395
addUserMessage(content: string): void;
9496
addUserAssistantMessage(content: string): void;
9597
getSelectedModelId(): Promise<string | undefined>;
98+
/**
99+
* Temporarily sets the session resource.
100+
* Only for non-controller code paths.
101+
* @deprecated
102+
*/
103+
setSessionResource(resource: vscode.Uri): IDisposable;
96104
}
97105

98106
export class CopilotCLISession extends DisposableStore implements ICopilotCLISession {
@@ -147,6 +155,8 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
147155
setSdkTraceContextUpdater(updater: ((traceparent?: string, tracestate?: string) => void) | undefined): void {
148156
this._updateSdkTraceContext = updater;
149157
}
158+
private _sessionResource: Uri;
159+
150160
constructor(
151161
private readonly _workspaceInfo: IWorkspaceInfo,
152162
private readonly _agentName: string | undefined,
@@ -162,11 +172,19 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
162172
@IUserQuestionHandler private readonly _userQuestionHandler: IUserQuestionHandler,
163173
@IConfigurationService private readonly configurationService: IConfigurationService,
164174
@IOTelService private readonly _otelService: IOTelService,
175+
@IRunCommandExecutionService private readonly _commandExecutionService: IRunCommandExecutionService,
165176
) {
166177
super();
167178
this.sessionId = _sdkSession.sessionId;
179+
this._sessionResource = SessionIdForCLI.getResource(this.sessionId);
168180
}
169181

182+
setSessionResource(resource: vscode.Uri): IDisposable {
183+
this._sessionResource = resource;
184+
return this.add(toDisposable(() => {
185+
this._sessionResource = SessionIdForCLI.getResource(this.sessionId);
186+
}));
187+
}
170188
attachStream(stream: vscode.ChatResponseStream): IDisposable {
171189
this._stream = stream;
172190
return toDisposable(() => {
@@ -517,6 +535,16 @@ export class CopilotCLISession extends DisposableStore implements ICopilotCLISes
517535
token,
518536
);
519537
flushPendingInvocationMessages();
538+
539+
// Update the permission picker dropdown based on the selected action
540+
if (response.approved && response.selectedAction !== 'exit_only') {
541+
const autopilotSelected = response.selectedAction === 'autopilot' || response.selectedAction === 'autopilot_fleet';
542+
const commandId = 'workbench.action.chat.copilotcli.approval';
543+
this._commandExecutionService.executeCommand(commandId, this._sessionResource, autopilotSelected).catch(error => {
544+
this.logService.error(error, '[CopilotCLISession] Failed to update permission picker');
545+
});
546+
}
547+
520548
this._sdkSession.respondToExitPlanMode(event.data.requestId, response);
521549
} catch (error) {
522550
this.logService.error(error, '[CopilotCLISession] Error handling exit plan mode');

extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotCliSessionService.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { CopilotCLIMCPHandler } from '../mcpHandler';
4242
import { IQuestion, IQuestionAnswer, IUserQuestionHandler } from '../userInputHelpers';
4343
import { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullICopilotCLIImageSupport } from './testHelpers';
4444
import { MockPromptsService } from '../../../../../platform/promptFiles/test/common/mockPromptsService';
45+
import { MockRunCommandExecutionService } from '../../../../../platform/commands/common/mockRunCommandExecutionService';
4546

4647
// Re-export for backward compatibility with other spec files
4748
export { MockCliSdkSession, MockCliSdkSessionManager, MockSkillLocations, NullCopilotCLIAgents, NullICopilotCLIImageSupport } from './testHelpers';
@@ -56,7 +57,7 @@ class MockLocalSession {
5657
}
5758
}
5859

59-
class NullAgentSessionsWorkspace implements IAgentSessionsWorkspace {
60+
export class NullAgentSessionsWorkspace implements IAgentSessionsWorkspace {
6061
_serviceBrand: undefined;
6162
readonly isAgentSessionsWorkspace = false;
6263
}
@@ -147,7 +148,7 @@ describe('CopilotCLISessionService', () => {
147148
}
148149
}();
149150
}
150-
return disposables.add(new CopilotCLISession(workspaceInfo, agentName, sdkSession, [], logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' }))));
151+
return disposables.add(new CopilotCLISession(workspaceInfo, agentName, sdkSession, [], logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new MockRunCommandExecutionService()));
151152
}
152153
} as unknown as IInstantiationService;
153154
const configurationService = accessor.get(IConfigurationService);

extensions/copilot/src/extension/chatSessions/copilotcli/node/test/copilotcliSession.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { CopilotCLISession } from '../copilotcliSession';
2929
import { PermissionRequest } from '../permissionHelpers';
3030
import { IQuestion, IQuestionAnswer, IUserQuestionHandler } from '../userInputHelpers';
3131
import { NullICopilotCLIImageSupport } from './testHelpers';
32+
import { MockRunCommandExecutionService } from '../../../../../platform/commands/common/mockRunCommandExecutionService';
3233

3334
vi.mock('../cliHelpers', async (importOriginal) => ({
3435
...(await importOriginal<typeof import('../cliHelpers')>()),
@@ -246,6 +247,7 @@ describe('CopilotCLISession', () => {
246247
new FakeUserQuestionHandler(),
247248
configurationService,
248249
new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })),
250+
new MockRunCommandExecutionService()
249251
));
250252
}
251253

extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,7 @@ export class CopilotCLIChatSessionParticipant extends Disposable {
13891389
// Previously we just updated it with details of the folder.
13901390
// If user has selected a repo, then update with repo information (right icons, etc).
13911391
if (isUntitled) {
1392+
disposables.add(session.object.setSessionResource(resource));
13921393
void this.lockRepoOptionForSession(context, token);
13931394
this.customSessionTitleService.generateSessionTitle(session.object.sessionId, request, token).catch(ex => this.logService.error(ex, 'Failed to generate custom session title'));
13941395
}

extensions/copilot/src/extension/chatSessions/vscode-node/test/copilotCLIChatSessionParticipant.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { CopilotCLIChatSessionContentProvider, CopilotCLIChatSessionItemProvider
5757
import { CopilotCloudSessionsProvider } from '../copilotCloudSessionsProvider';
5858
import { CopilotCLIFolderRepositoryManager } from '../folderRepositoryManagerImpl';
5959
import { MockPromptsService } from '../../../../platform/promptFiles/test/common/mockPromptsService';
60+
import { MockRunCommandExecutionService } from '../../../../platform/commands/common/mockRunCommandExecutionService';
6061

6162
// Mock terminal integration to avoid importing PowerShell asset (.ps1) which Vite cannot parse during tests
6263
vi.mock('../copilotCLITerminalIntegration', () => {
@@ -392,7 +393,7 @@ describe('CopilotCLIChatSessionParticipant.handleRequest', () => {
392393
}
393394
}();
394395
}
395-
const session = new TestCopilotCLISession(workspaceInfo, agentName, sdkSession, [], logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })));
396+
const session = new TestCopilotCLISession(workspaceInfo, agentName, sdkSession, [], logService, workspaceService, new MockChatSessionMetadataStore(), instantiationService, new NullRequestLogger(), new NullICopilotCLIImageSupport(), new FakeToolsService(), new FakeUserQuestionHandler(), accessor.get(IConfigurationService), new NoopOTelService(resolveOTelConfig({ env: {}, extensionVersion: '0.0.0', sessionId: 'test' })), new MockRunCommandExecutionService());
396397
cliSessions.push(session);
397398
return disposables.add(session);
398399
}

src/vs/workbench/contrib/chat/browser/chatSessions/chatSessions.contribution.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,21 @@ import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';
3232
import { IChatAgentAttachmentCapabilities, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';
3333
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
3434
import { ChatSessionOptionsMap, ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionCommitEvent, IChatSessionContentProvider, IChatSessionCustomizationItemGroup, IChatSessionCustomizationsProvider, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionOptionsChangeEvent, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus, ReadonlyChatSessionOptionsMap, ResolvedChatSessionsExtensionPoint } from '../../common/chatSessionsService.js';
35-
import { ChatAgentLocation, ChatModeKind } from '../../common/constants.js';
35+
import { ChatAgentLocation, ChatModeKind, ChatPermissionLevel } from '../../common/constants.js';
3636
import { CHAT_CATEGORY } from '../actions/chatActions.js';
3737
import { IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';
3838
import { IChatService, ResponseModelState } from '../../common/chatService/chatService.js';
3939
import { autorun, observableFromEvent } from '../../../../../base/common/observable.js';
4040
import { IChatRequestVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';
4141
import { IViewsService } from '../../../../services/views/common/viewsService.js';
42-
import { ChatViewId } from '../chat.js';
42+
import { ChatViewId, IChatWidgetService } from '../chat.js';
4343
import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';
4444
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderName } from '../agentSessions/agentSessions.js';
4545
import { BugIndicatingError, isCancellationError } from '../../../../../base/common/errors.js';
4646
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
4747
import { isUntitledChatSession, LocalChatSessionUri } from '../../common/model/chatUri.js';
4848
import { assertNever } from '../../../../../base/common/assert.js';
49-
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
49+
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
5050
import { Target } from '../../common/promptSyntax/promptTypes.js';
5151
import { slashReg } from '../../common/requestParser/chatRequestParser.js';
5252
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
@@ -1238,6 +1238,21 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
12381238

12391239
registerSingleton(IChatSessionsService, ChatSessionsService, InstantiationType.Delayed);
12401240

1241+
CommandsRegistry.registerCommand('workbench.action.chat.copilotcli.approval', (accessor: ServicesAccessor, sessionResource: UriComponents, setAutopilot: boolean) => {
1242+
const chatWidgetService = accessor.get(IChatWidgetService);
1243+
const widget = chatWidgetService.getWidgetBySessionResource(URI.revive(sessionResource));
1244+
if (!widget) {
1245+
return;
1246+
}
1247+
if (setAutopilot) {
1248+
widget.inputPart.setPermissionLevel(ChatPermissionLevel.Autopilot);
1249+
} else {
1250+
if (widget.inputPart.currentPermissionLevelObs.get() === ChatPermissionLevel.Autopilot) {
1251+
widget.inputPart.setPermissionLevel(ChatPermissionLevel.Default);
1252+
}
1253+
}
1254+
});
1255+
12411256

12421257

12431258
function registerNewSessionInPlaceAction(type: string, displayName: string): IDisposable {

0 commit comments

Comments
 (0)