Skip to content

Commit c772f67

Browse files
chrmartiCopilot
andauthored
Agent Host: Open Dev Container workspaces in Agents (#334249)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0f2eb0c commit c772f67

19 files changed

Lines changed: 495 additions & 41 deletions

File tree

src/vs/sessions/browser/parts/chatGroupView.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ChatCompositeBar, IChatCompositeBarDelegate } from './chatCompositeBar.
2525
import { type IRemoteHostUnavailableEmptyStateContent, RemoteHostUnavailableEmptyState } from './remoteHostUnavailableEmptyState.js';
2626
import { SessionRemoteConnection } from './sessionRemoteConnection.js';
2727
import { ISessionReadOnlyBannerContent, SessionReadOnlyBanner } from './sessionReadOnlyBanner.js';
28-
import { AbstractChatView, ChatViewKind, IChatViewOptions } from './chatView.js';
28+
import { AbstractChatView, ChatViewKind, IChatViewOptions, ISelectWorkspaceOptions } from './chatView.js';
2929

3030
/**
3131
* The data + callbacks a {@link ChatGroupView} needs from its owning
@@ -347,8 +347,8 @@ export class ChatGroupView extends Disposable implements ISerializableView {
347347
return this._currentView.value?.submitInput() ?? Promise.resolve(false);
348348
}
349349

350-
selectWorkspace(folderUri: URI, providerId?: string): void {
351-
this._currentView.value?.selectWorkspace(folderUri, providerId);
350+
selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void {
351+
this._currentView.value?.selectWorkspace(folderUri, options);
352352
}
353353

354354
prefillInput(text: string): void {

src/vs/sessions/browser/parts/chatGroupsView.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { agentsPanelBorder } from '../../common/theme.js';
1919
import { IChat } from '../../services/sessions/common/session.js';
2020
import { IActiveSession } from '../../services/sessions/common/sessionsManagement.js';
2121
import { ISessionsService } from '../../services/sessions/browser/sessionsService.js';
22-
import { IChatViewOptions } from './chatView.js';
22+
import { IChatViewOptions, ISelectWorkspaceOptions } from './chatView.js';
2323
import { ChatGroupView, IChatGroupContext } from './chatGroupView.js';
2424
import { ChatDropZone, ChatGroupDropTarget, IChatGroupDropTargetDelegate } from './chatGroupDropTarget.js';
2525
import { IDraggedSessionChat, isSessionChatDrag } from '../dnd.js';
@@ -805,8 +805,8 @@ export class ChatGroupsView extends Themable {
805805
return this._activeGroup?.view.submitInput() ?? Promise.resolve(false);
806806
}
807807

808-
selectWorkspace(folderUri: URI, providerId?: string): void {
809-
this._activeGroup?.view.selectWorkspace(folderUri, providerId);
808+
selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void {
809+
this._activeGroup?.view.selectWorkspace(folderUri, options);
810810
}
811811

812812
prefillInput(text: string): void {

src/vs/sessions/browser/parts/chatView.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export type ChatViewKind = 'newSession' | 'newChatInSession' | 'chat';
2626
export interface IChatViewOptions {
2727
}
2828

29+
export interface ISelectWorkspaceOptions {
30+
readonly providerId?: string;
31+
readonly preferDevContainer?: boolean;
32+
}
33+
2934
/**
3035
* Base class for a view that lives inside the {@link SessionsPart} internal grid.
3136
* Each instance occupies a single grid leaf. Subclasses populate {@link element}
@@ -81,7 +86,7 @@ export abstract class AbstractChatView extends Disposable implements ISerializab
8186
* implementation is a no-op; subclasses that host a workspace picker
8287
* (e.g. `NewChatView`) override this to forward the selection.
8388
*/
84-
selectWorkspace(_folderUri: URI, _providerId?: string): void {
89+
selectWorkspace(_folderUri: URI, _options?: ISelectWorkspaceOptions): void {
8590
// no-op by default
8691
}
8792

src/vs/sessions/browser/parts/sessionView.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { IContextKey, IContextKeyService } from '../../../platform/contextkey/co
1515
import { IThemeService } from '../../../platform/theme/common/themeService.js';
1616
import { IActiveSession } from '../../services/sessions/common/sessionsManagement.js';
1717
import { IChat } from '../../services/sessions/common/session.js';
18-
import { AbstractChatView, IChatViewOptions } from './chatView.js';
18+
import { AbstractChatView, IChatViewOptions, ISelectWorkspaceOptions } from './chatView.js';
1919
import { ChatGroupsView } from './chatGroupsView.js';
2020
import { SessionHeader, SessionViewFloatingToolbar } from './sessionHeader.js';
2121
import { ISessionContext, SessionContext } from '../../services/sessions/browser/sessionContext.js';
@@ -293,9 +293,9 @@ export class SessionView extends Disposable implements ISerializableView {
293293
return this._currentSession;
294294
}
295295

296-
selectWorkspace(folderUri: URI, providerId?: string): void {
296+
selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void {
297297
const standaloneView = this._standaloneView.value;
298-
standaloneView ? standaloneView.selectWorkspace(folderUri, providerId) : this._groupsView.selectWorkspace(folderUri, providerId);
298+
standaloneView ? standaloneView.selectWorkspace(folderUri, options) : this._groupsView.selectWorkspace(folderUri, options);
299299
}
300300

301301
/** Opens the given chat in a group beside the active one ("open to the side"). */

src/vs/sessions/common/agentHostSessionsProvider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ export interface IAgentHostSessionsProvider extends ISessionsProvider {
171171
isDevContainerEnabled?(sessionId: string): boolean;
172172
/** Set whether this draft should run on a Dev Container Agent Host. */
173173
setDevContainerEnabled?(sessionId: string, enabled: boolean): void;
174+
/** Enable Dev Container execution once availability resolves for this draft. */
175+
preferDevContainer?(sessionId: string): void;
174176

175177
// -- Dynamic Session Config --
176178

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { decodeHex } from '../../../../base/common/buffer.js';
7+
import { Schemas } from '../../../../base/common/network.js';
8+
import { URI } from '../../../../base/common/uri.js';
9+
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
10+
import { DevContainerAgentHostEnabledSettingId } from '../../../common/devContainerAgentHostService.js';
11+
12+
const DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX = 'dev-container+';
13+
14+
export interface IAgentsWindowFolderIntent {
15+
readonly folderUri: URI | undefined;
16+
readonly preferDevContainer: boolean;
17+
}
18+
19+
export function resolveAgentsWindowFolderIntent(workspaceUri: URI | undefined, configurationService: IConfigurationService): IAgentsWindowFolderIntent {
20+
if (workspaceUri?.scheme === Schemas.file) {
21+
return { folderUri: workspaceUri, preferDevContainer: false };
22+
}
23+
if (workspaceUri?.scheme !== Schemas.vscodeRemote || !workspaceUri.authority.startsWith(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX)) {
24+
return { folderUri: undefined, preferDevContainer: false };
25+
}
26+
try {
27+
return {
28+
folderUri: URI.file(decodeHex(workspaceUri.authority.slice(DEV_CONTAINER_REMOTE_AUTHORITY_PREFIX.length)).toString()),
29+
preferDevContainer: configurationService.getValue<boolean>(DevContainerAgentHostEnabledSettingId) === true,
30+
};
31+
} catch (error) {
32+
if (error instanceof SyntaxError) {
33+
return { folderUri: undefined, preferDevContainer: false };
34+
}
35+
throw error;
36+
}
37+
}

src/vs/sessions/contrib/chat/browser/chatView.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { IChatModel } from '../../../../workbench/contrib/chat/common/model/chat
3636
import { ChatAgentLocation, ChatModeKind } from '../../../../workbench/contrib/chat/common/constants.js';
3737
import { getChatSessionType } from '../../../../workbench/contrib/chat/common/model/chatUri.js';
3838
import { IChatSessionsService, localChatSessionType } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
39-
import { AbstractChatView, ChatViewKind, IChatViewOptions } from '../../../browser/parts/chatView.js';
39+
import { AbstractChatView, ChatViewKind, IChatViewOptions, ISelectWorkspaceOptions } from '../../../browser/parts/chatView.js';
4040
import { ChatInteractivity, getSessionStatusMessage, IChat, isActiveSessionStatus, ISession, SessionStatus } from '../../../services/sessions/common/session.js';
4141
import { IChatViewFactory } from '../../../services/chatView/browser/chatViewFactory.js';
4242
import { NewChatWidget } from './newChatWidget.js';
@@ -104,9 +104,9 @@ export class NewChatView extends AbstractChatView {
104104
this._widget.focusInput();
105105
}
106106

107-
override selectWorkspace(folderUri: URI, providerId?: string): void {
107+
override selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void {
108108
if (this._widget instanceof NewChatWidget) {
109-
this._widget.selectWorkspace(folderUri, providerId);
109+
this._widget.selectWorkspace(folderUri, options);
110110
}
111111
}
112112

src/vs/sessions/contrib/chat/browser/newChatWidget.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import { localize } from '../../../../nls.js';
2525
import { IActiveSession, ICreateNewSessionOptions, ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
2626
import { ISession, SESSION_WORKSPACE_GROUP_GITHUB } from '../../../services/sessions/common/session.js';
2727
import { IOpenNewSessionResult, ISessionsService } from '../../../services/sessions/browser/sessionsService.js';
28+
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
2829
import { isAllowSignedOutWhenUsableEnabled, shouldShowGitHubWorkspaceGroupSignIn } from '../../../browser/sessionsAuthGate.js';
2930
import { AGENTIC_SIGN_IN_COMMAND_ID } from '../../../common/sessionCommands.js';
31+
import { isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js';
3032
import { IAquariumService, IMountedToggleHandle } from '../../aquarium/browser/aquariumOverlay.js';
3133
import { IWorkspacePickerNoWorkspaceOption, IWorkspacePickerTrigger, WorkspacePicker } from './sessionWorkspacePicker.js';
3234
import { WebWorkspacePicker } from './webWorkspacePicker.js';
@@ -35,7 +37,7 @@ import { NewChatInputWidget } from './newChatInput.js';
3537
import { NoAgentHostEmptyState } from './noAgentHostEmptyState.js';
3638
import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';
3739
import { IAgentHostFilterService } from '../../../services/agentHostFilter/common/agentHostFilter.js';
38-
import { IChatViewOptions } from '../../../browser/parts/chatView.js';
40+
import { IChatViewOptions, ISelectWorkspaceOptions } from '../../../browser/parts/chatView.js';
3941
import { SessionWorkspacePickerVisibleContext } from '../../../common/contextkeys.js';
4042
import { AGENT_FEEDBACK_NEW_SESSION_RESOURCE, AgentFeedbackState, IAgentFeedback, IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js';
4143
import { buildNewSessionPrompt } from '../../agentFeedback/browser/agentFeedbackAttachmentEntry.js';
@@ -70,6 +72,7 @@ export class NewChatWidget extends Disposable {
7072
/** Recreates the draft once a better/late-registering provider can serve the folder (see {@link _createNewSession}). */
7173
private readonly _pendingPreferredUpgrade = new MutableDisposable<IDisposable>();
7274
private readonly _newSessionCreation = new MutableDisposable<IDisposable>();
75+
private _preferredDevContainerFolderUri: URI | undefined;
7376

7477
/**
7578
* The currently mounted no-agent-host empty state, if any. Set by
@@ -114,6 +117,7 @@ export class NewChatWidget extends Disposable {
114117
@ILogService private readonly logService: ILogService,
115118
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
116119
@ISessionsService private readonly sessionsService: ISessionsService,
120+
@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,
117121
@IAquariumService private readonly aquariumService: IAquariumService,
118122
@IAgentHostFilterService private readonly agentHostFilterService: IAgentHostFilterService,
119123
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@@ -618,13 +622,16 @@ export class NewChatWidget extends Disposable {
618622
} else {
619623
return result;
620624
}
625+
this._applyPreferredDevContainer(result.session, folderUri);
621626
if (result.trustDeclined) {
627+
this._preferredDevContainerFolderUri = undefined;
622628
// The user explicitly declined trust: don't schedule a retry, which
623629
// would silently recreate (and possibly re-prompt) the draft once a
624630
// provider registers/changes without any further user action.
625631
this._pendingPreferredUpgrade.clear();
626632
return result;
627633
}
634+
628635
// Keep the draft in sync with late-registering providers. Agent hosts
629636
// connect lazily, so there is no timeout — the listener lives until the
630637
// draft is sent or replaced. We watch when:
@@ -640,6 +647,18 @@ export class NewChatWidget extends Disposable {
640647
return result;
641648
}
642649

650+
private _applyPreferredDevContainer(session: ISession | undefined, folderUri: URI): void {
651+
if (!session || !this._preferredDevContainerFolderUri || !this.uriIdentityService.extUri.isEqual(this._preferredDevContainerFolderUri, folderUri)) {
652+
return;
653+
}
654+
const provider = this.sessionsProvidersService.getProvider(session.providerId);
655+
if (!provider || !isAgentHostProvider(provider) || !provider.preferDevContainer) {
656+
return;
657+
}
658+
provider.preferDevContainer(session.sessionId);
659+
this._preferredDevContainerFolderUri = undefined;
660+
}
661+
643662
private async _createSessionNow(folderUri: URI, userPick: IPreferredSessionType | undefined, token: CancellationToken): Promise<IOpenNewSessionResult> {
644663
// Prefer the user's explicit pick when its provider can serve the
645664
// folder; otherwise fall back to the preferred (first) session type.
@@ -997,6 +1016,9 @@ export class NewChatWidget extends Disposable {
9971016
private async _onWorkspaceSelected(folderUri: URI | undefined): Promise<void> {
9981017
// Cancel any in-flight upgrade for a previous selection.
9991018
this._pendingPreferredUpgrade.clear();
1019+
if (!folderUri || !this._preferredDevContainerFolderUri || !this.uriIdentityService.extUri.isEqual(this._preferredDevContainerFolderUri, folderUri)) {
1020+
this._preferredDevContainerFolderUri = undefined;
1021+
}
10001022
const currentFolderUri = this._session.get()?.workspace.get()?.folders[0]?.root;
10011023
const refreshingPromptOptions = !!currentFolderUri
10021024
&& (!folderUri || !this.uriIdentityService.extUri.isEqual(currentFolderUri, folderUri))
@@ -1054,8 +1076,9 @@ export class NewChatWidget extends Disposable {
10541076
this._newChatInput.attach(uris);
10551077
}
10561078

1057-
selectWorkspace(folderUri: URI, providerId?: string): void {
1058-
this._workspacePicker.setSelectedWorkspace(folderUri, { providerId });
1079+
selectWorkspace(folderUri: URI, options?: ISelectWorkspaceOptions): void {
1080+
this._preferredDevContainerFolderUri = options?.preferDevContainer ? folderUri : undefined;
1081+
this._workspacePicker.setSelectedWorkspace(folderUri, { providerId: options?.providerId });
10591082
}
10601083
}
10611084

0 commit comments

Comments
 (0)