Refactor new chat view: rename to workspace, separate session type and isolation pickers (#302797)

* refactor: rename SessionProject to SessionWorkspace and update related references

* fix: update filter placeholder text in project picker to "Search Workspaces..."

* feat: add SessionWorkspace class to represent workspaces for sessions

* feat: implement ProjectPicker class for unified project selection

* fix: update SessionTargetType to use 'copilot-cli' instead of 'cli'

* refactor: rename targetMode to isolationMode in NewSession and related classes

* refactor: rename LocalNewSession to CopilotCLISession and update related references

* feat: set project in session type picker for local sessions

* fix: ensure project is set in session type picker for both remote and local sessions

* fix: reset isolation mode to worktree when isolation option is disabled

* delete file
This commit is contained in:
Sandeep Somavarapu
2026-03-18 14:43:31 +01:00
committed by GitHub
parent 819f0cd46c
commit 44264fdeae
12 changed files with 309 additions and 185 deletions

View File

@@ -58,7 +58,7 @@ import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../../workbench/
import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js';
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
import { IActiveSessionItem, ISessionsManagementService } from '../../sessions/browser/sessionsManagementService.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionWorkspace.js';
import { CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, PRReviewStateKind } from '../../codeReview/browser/codeReviewService.js';
import { IGitRepository, IGitService } from '../../../../workbench/contrib/git/common/gitService.js';
import { IGitHubService } from '../../github/browser/githubService.js';

View File

@@ -178,7 +178,7 @@
}
/* Prominent project picker button */
.sessions-chat-picker-slot.sessions-chat-project-picker .action-label {
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label {
height: auto;
padding: 8px 20px;
font-size: 15px;
@@ -188,16 +188,16 @@
border-radius: 6px;
}
.sessions-chat-picker-slot.sessions-chat-project-picker .action-label:hover {
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label:hover {
background-color: var(--vscode-button-secondaryHoverBackground);
}
.sessions-chat-picker-slot.sessions-chat-project-picker .action-label .codicon {
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .codicon {
font-size: 18px;
margin-right: 6px;
}
.sessions-chat-picker-slot.sessions-chat-project-picker .action-label .sessions-chat-dropdown-label {
.sessions-chat-picker-slot.sessions-chat-workspace-picker .action-label .sessions-chat-dropdown-label {
font-size: 15px;
}

View File

@@ -54,13 +54,12 @@ import { ContextMenuController } from '../../../../editor/contrib/contextmenu/br
import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js';
import { NewChatContextAttachments } from './newChatContextAttachments.js';
import { IGitService } from '../../../../workbench/contrib/git/common/gitService.js';
import { TargetPicker } from './sessionTargetPicker.js';
import { SessionTypePicker, IsolationPicker } from './sessionTargetPicker.js';
import { BranchPicker } from './branchPicker.js';
import { SyncIndicator } from './syncIndicator.js';
import { INewSession, ISessionOptionGroup, RemoteNewSession } from './newSession.js';
import { CloudModelPicker } from './modelPicker.js';
import { ProjectPicker } from './projectPicker.js';
import { SessionProject } from '../../sessions/common/sessionProject.js';
import { WorkspacePicker } from './workspacePicker.js';
import { SessionWorkspace } from '../../sessions/common/sessionWorkspace.js';
import { ModePicker } from './modePicker.js';
import { getErrorMessage } from '../../../../base/common/errors.js';
import { SlashCommandHandler } from './slashCommands.js';
@@ -99,10 +98,10 @@ interface INewChatWidgetOptions {
*/
class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
private readonly _projectPicker: ProjectPicker;
private readonly _targetPicker: TargetPicker;
private readonly _workspacePicker: WorkspacePicker;
private readonly _sessionTypePicker: SessionTypePicker;
private readonly _branchPicker: BranchPicker;
private readonly _syncIndicator: SyncIndicator;
private readonly _isolationPicker: IsolationPicker;
private readonly _options: INewChatWidgetOptions;
// IHistoryNavigationWidget
@@ -185,17 +184,17 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
super();
this._history = this._register(this.instantiationService.createInstance(ChatHistoryNavigator, ChatAgentLocation.Chat));
this._contextAttachments = this._register(this.instantiationService.createInstance(NewChatContextAttachments));
this._projectPicker = this._register(this.instantiationService.createInstance(ProjectPicker));
this._workspacePicker = this._register(this.instantiationService.createInstance(WorkspacePicker));
this._permissionPicker = this._register(this.instantiationService.createInstance(NewChatPermissionPicker));
this._cloudModelPicker = this._register(this.instantiationService.createInstance(CloudModelPicker));
this._modePicker = this._register(this.instantiationService.createInstance(ModePicker));
this._targetPicker = this._register(this.instantiationService.createInstance(TargetPicker));
this._sessionTypePicker = this._register(this.instantiationService.createInstance(SessionTypePicker));
this._branchPicker = this._register(this.instantiationService.createInstance(BranchPicker));
this._syncIndicator = this._register(this.instantiationService.createInstance(SyncIndicator));
this._isolationPicker = this._register(this.instantiationService.createInstance(IsolationPicker));
this._options = options;
// When a project is selected, infer the target and create a new session
this._register(this._projectPicker.onDidSelectProject(async (project) => {
this._register(this._workspacePicker.onDidSelectProject(async (project) => {
await this._onProjectSelected(project);
this._updateDraftState();
this._focusEditor();
@@ -208,15 +207,26 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._register(this._branchPicker.onDidChange((branch) => {
this._newSession.value?.setBranch(branch);
this._syncIndicator.setBranch(branch);
this._updateDraftState();
this._focusEditor();
}));
this._register(this._targetPicker.onDidChange((mode) => {
this._newSession.value?.setTargetMode(mode);
this._register(this._sessionTypePicker.onDidChange((target) => {
if (target === 'cloud') {
this._isolationPicker.setVisible(false);
this._branchPicker.setVisible(false);
} else {
this._newSession.value?.setIsolationMode(this._isolationPicker.isolationMode);
this._isolationPicker.setVisible(true);
this._branchPicker.setVisible(this._isolationPicker.isWorktree);
}
this._updateDraftState();
this._focusEditor();
}));
this._register(this._isolationPicker.onDidChange((mode) => {
this._newSession.value?.setIsolationMode(mode);
this._branchPicker.setVisible(mode === 'worktree');
this._syncIndicator.setVisible(mode === 'worktree');
this._updateDraftState();
this._focusEditor();
}));
@@ -281,12 +291,12 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
// Isolation mode and branch pickers (below the input, shown when Local target is selected)
const isolationContainer = dom.append(welcomeElement, dom.$('.chat-full-welcome-local-mode'));
this._targetPicker.render(isolationContainer);
this._sessionTypePicker.render(isolationContainer);
this._permissionPicker.render(isolationContainer);
dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-spacer'));
const branchContainer = dom.append(isolationContainer, dom.$('.sessions-chat-local-mode-right'));
this._isolationPicker.render(branchContainer);
this._branchPicker.render(branchContainer);
this._syncIndicator.render(branchContainer);
// Render project picker & extension pickers
this._renderOptionGroupPickers();
@@ -298,7 +308,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._restoreState();
// Create initial session
const restoredProject = this._projectPicker.selectedProject;
const restoredProject = this._workspacePicker.selectedProject;
if (restoredProject) {
this._onProjectSelected(restoredProject);
} else {
@@ -314,7 +324,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
}, { once: true }));
}
private async _createNewSession(project?: SessionProject): Promise<void> {
private async _createNewSession(project?: SessionWorkspace): Promise<void> {
const target = project?.isRepo ? AgentSessionProviders.Cloud : AgentSessionProviders.Background;
const resource = getResourceForNewChatSession({
@@ -358,8 +368,9 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
}
}));
this._sessionTypePicker.setProject(session.project);
if (session instanceof RemoteNewSession) {
this._targetPicker.setProject(session.project);
this._renderRemoteSessionPickers(session, true);
listeners.add(session.onDidChangeOptionGroups(() => {
this._renderRemoteSessionPickers(session);
@@ -382,7 +393,6 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._repositoryLoading = true;
this._updateInputLoadingState();
this._branchPicker.setRepository(undefined);
this._syncIndicator.setRepository(undefined);
this._modePicker.reset();
this.gitService.openRepository(folderUri).then(repository => {
@@ -397,11 +407,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
session.setProject(session.project.withRepository(repository));
}
this._targetPicker.setProject(session?.project);
this._sessionTypePicker.setProject(session?.project);
this._isolationPicker.setHasGitRepo(!!repository);
this._branchPicker.setRepository(repository);
this._branchPicker.setVisible(!!repository && this._targetPicker.isWorktree);
this._syncIndicator.setRepository(repository);
this._syncIndicator.setVisible(!!repository && this._targetPicker.isWorktree);
this._branchPicker.setVisible(!!repository && this._sessionTypePicker.isCli && this._isolationPicker.isWorktree);
this._modePicker.reset();
}).catch(e => {
if (cts.token.isCancellationRequested) {
@@ -410,11 +419,10 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this.logService.warn(`Failed to open repository at ${folderUri.toString()}`, getErrorMessage(e));
this._repositoryLoading = false;
this._updateInputLoadingState();
this._targetPicker.setProject(undefined);
this._sessionTypePicker.setProject(undefined);
this._isolationPicker.setHasGitRepo(false);
this._branchPicker.setRepository(undefined);
this._branchPicker.setVisible(false);
this._syncIndicator.setRepository(undefined);
this._syncIndicator.setVisible(false);
this._modePicker.reset();
});
}
@@ -677,7 +685,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
const pickersRow = dom.append(this._pickersContainer, dom.$('.chat-full-welcome-pickers'));
// Project picker (unified folder + repo picker)
this._projectPicker.render(pickersRow);
this._workspacePicker.render(pickersRow);
}
// --- Local session pickers ---
@@ -703,7 +711,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
this._modePicker.setVisible(false);
this._permissionPicker.setVisible(false);
this._branchPicker.setVisible(false);
this._syncIndicator.setVisible(false);
this._isolationPicker.setVisible(false);
this._cloudModelPicker.setSession(session);
this._cloudModelPicker.setVisible(true);
@@ -957,20 +965,20 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
}
private _openRepoOrFolderPicker(_sessionType: AgentSessionProviders): void {
this._projectPicker.showPicker();
this._workspacePicker.showPicker();
}
private async _requestFolderTrust(folderUri: URI, previousProject?: SessionProject): Promise<boolean> {
private async _requestFolderTrust(folderUri: URI, previousProject?: SessionWorkspace): Promise<boolean> {
const trusted = await this.workspaceTrustRequestService.requestResourcesTrust({
uri: folderUri,
message: localize('trustFolderMessage', "An agent session will be able to read files, run commands, and make changes in this folder."),
});
if (!trusted) {
this._projectPicker.removeFromRecents(folderUri);
this._workspacePicker.removeFromRecents(folderUri);
if (previousProject) {
this._projectPicker.setSelectedProject(previousProject, false);
this._workspacePicker.setSelectedProject(previousProject, false);
} else {
this._projectPicker.clearSelection();
this._workspacePicker.clearSelection();
}
}
return !!trusted;
@@ -996,8 +1004,8 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
}
if (draft.projectUri) {
try {
const project = new SessionProject(URI.revive(draft.projectUri));
this._projectPicker.setSelectedProject(project, false);
const project = new SessionWorkspace(URI.revive(draft.projectUri));
this._workspacePicker.setSelectedProject(project, false);
} catch { /* ignore */ }
}
}
@@ -1054,7 +1062,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
* Infers the session target from the selection kind, creates a new session,
* and shows/hides pickers accordingly.
*/
private async _onProjectSelected(project: SessionProject): Promise<void> {
private async _onProjectSelected(project: SessionWorkspace): Promise<void> {
// Cancel any in-flight project selection
this._projectSelectionCts.value?.cancel();
const cts = this._projectSelectionCts.value = new CancellationTokenSource();

View File

@@ -8,15 +8,15 @@ import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
import { TargetMode } from './sessionTargetPicker.js';
import { SessionProject } from '../../sessions/common/sessionProject.js';
import { IsolationMode } from './sessionTargetPicker.js';
import { SessionWorkspace } from '../../sessions/common/sessionWorkspace.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';
import { IChatMode } from '../../../../workbench/contrib/chat/common/chatModes.js';
export type NewSessionChangeType = 'repoUri' | 'targetMode' | 'branch' | 'options' | 'disabled' | 'agent';
export type NewSessionChangeType = 'repoUri' | 'isolationMode' | 'branch' | 'options' | 'disabled' | 'agent';
/**
* Represents a resolved option group with its current selected value.
@@ -28,14 +28,14 @@ export interface ISessionOptionGroup {
/**
* A new session represents a session being configured before the first
* request is sent. It holds the user's selections (repoUri, targetMode)
* request is sent. It holds the user's selections (repoUri, isolationMode)
* and fires a single event when any property changes.
*/
export interface INewSession extends IDisposable {
readonly resource: URI;
readonly target: AgentSessionProviders;
readonly project: SessionProject | undefined;
readonly targetMode: TargetMode;
readonly project: SessionWorkspace | undefined;
readonly isolationMode: IsolationMode | undefined;
readonly branch: string | undefined;
readonly modelId: string | undefined;
readonly mode: IChatMode | undefined;
@@ -44,8 +44,8 @@ export interface INewSession extends IDisposable {
readonly selectedOptions: ReadonlyMap<string, IChatSessionProviderOptionItem>;
readonly disabled: boolean;
readonly onDidChange: Event<NewSessionChangeType>;
setProject(project: SessionProject): void;
setTargetMode(mode: TargetMode): void;
setProject(project: SessionWorkspace): void;
setIsolationMode(mode: IsolationMode): void;
setBranch(branch: string | undefined): void;
setModelId(modelId: string | undefined): void;
setMode(mode: IChatMode | undefined): void;
@@ -61,14 +61,14 @@ const AGENT_OPTION_ID = 'agent';
/**
* Local new session for Background agent sessions.
* Fires `onDidChange` for both `repoUri` and `targetMode` changes.
* Fires `onDidChange` for both `repoUri` and `isolationMode` changes.
* Notifies the extension service with session options for each property change.
*/
export class LocalNewSession extends Disposable implements INewSession {
export class CopilotCLISession extends Disposable implements INewSession {
private _repoUri: URI | undefined;
private _project: SessionProject | undefined;
private _targetMode: TargetMode;
private _project: SessionWorkspace | undefined;
private _isolationMode: IsolationMode;
private _branch: string | undefined;
private _modelId: string | undefined;
private _mode: IChatMode | undefined;
@@ -81,8 +81,8 @@ export class LocalNewSession extends Disposable implements INewSession {
readonly target = AgentSessionProviders.Background;
readonly selectedOptions = new Map<string, IChatSessionProviderOptionItem>();
get project(): SessionProject | undefined { return this._project; }
get targetMode(): TargetMode { return this._targetMode; }
get project(): SessionWorkspace | undefined { return this._project; }
get isolationMode(): IsolationMode { return this._isolationMode; }
get branch(): string | undefined { return this._branch; }
get modelId(): string | undefined { return this._modelId; }
get mode(): IChatMode | undefined { return this._mode; }
@@ -92,7 +92,7 @@ export class LocalNewSession extends Disposable implements INewSession {
if (!this._repoUri) {
return true;
}
if (this._targetMode === 'worktree' && !this._branch) {
if (this._isolationMode === 'worktree' && !this._branch) {
return true;
}
return false;
@@ -109,24 +109,24 @@ export class LocalNewSession extends Disposable implements INewSession {
this._repoUri = defaultRepoUri;
this.setOption(REPOSITORY_OPTION_ID, defaultRepoUri.fsPath);
}
this._targetMode = 'worktree';
this._isolationMode = 'worktree';
this.setOption(ISOLATION_OPTION_ID, 'worktree');
}
setProject(project: SessionProject): void {
setProject(project: SessionWorkspace): void {
this._project = project;
this._repoUri = project.uri;
this.setTargetMode('worktree');
this.setIsolationMode('worktree');
this._branch = undefined;
this._onDidChange.fire('repoUri');
this._onDidChange.fire('disabled');
this.setOption(REPOSITORY_OPTION_ID, project.uri.fsPath);
}
setTargetMode(mode: TargetMode): void {
if (this._targetMode !== mode) {
this._targetMode = mode;
this._onDidChange.fire('targetMode');
setIsolationMode(mode: IsolationMode): void {
if (this._isolationMode !== mode) {
this._isolationMode = mode;
this._onDidChange.fire('isolationMode');
this._onDidChange.fire('disabled');
this.setOption(ISOLATION_OPTION_ID, mode);
}
@@ -183,7 +183,7 @@ export class LocalNewSession extends Disposable implements INewSession {
export class RemoteNewSession extends Disposable implements INewSession {
private _repoUri: URI | undefined;
private _project: SessionProject | undefined;
private _project: SessionWorkspace | undefined;
private _modelId: string | undefined;
private _query: string | undefined;
private _attachedContext: IChatRequestVariableEntry[] | undefined;
@@ -196,8 +196,8 @@ export class RemoteNewSession extends Disposable implements INewSession {
readonly selectedOptions = new Map<string, IChatSessionProviderOptionItem>();
get project(): SessionProject | undefined { return this._project; }
get targetMode(): TargetMode { return 'cloud'; }
get project(): SessionWorkspace | undefined { return this._project; }
get isolationMode(): undefined { return undefined; }
get branch(): string | undefined { return undefined; }
get modelId(): string | undefined { return this._modelId; }
get mode(): IChatMode | undefined { return undefined; }
@@ -232,7 +232,7 @@ export class RemoteNewSession extends Disposable implements INewSession {
}));
}
setProject(project: SessionProject): void {
setProject(project: SessionWorkspace): void {
this._project = project;
this._repoUri = project.uri;
this._onDidChange.fire('repoUri');
@@ -241,7 +241,7 @@ export class RemoteNewSession extends Disposable implements INewSession {
this.setOption('repositories', { id, name: id });
}
setTargetMode(_mode: TargetMode): void {
setIsolationMode(_mode: IsolationMode): void {
// No-op for remote sessions
}

View File

@@ -12,49 +12,173 @@ import { localize } from '../../../../nls.js';
import { IActionWidgetService } from '../../../../platform/actionWidget/browser/actionWidget.js';
import { ActionListItemKind, IActionListDelegate, IActionListItem } from '../../../../platform/actionWidget/browser/actionList.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { SessionProject } from '../../sessions/common/sessionProject.js';
import { SessionWorkspace } from '../../sessions/common/sessionWorkspace.js';
// #region --- Types ---
export type SessionTargetType = 'copilot-cli' | 'cloud';
export type IsolationMode = 'worktree' | 'workspace';
// #endregion
// #region --- Target Picker ---
export type TargetMode = 'worktree' | 'workspace' | 'cloud';
/**
* A self-contained widget for selecting the session target mode.
* A self-contained widget for selecting the session target type.
*
* Options:
* - **Worktree** (`worktree`) — shown when a folder with a git repo is selected
* - **Folder** (`workspace`) — shown only when isolation option is enabled
* - **Cloud** (`cloud`) — shown and auto-selected when a repository is picked; disabled
* - **Copilot CLI** (`cli`) — local agent session
* - **Cloud** (`cloud`) — remote/cloud agent session
*
* Emits `onDidChange` with the selected `TargetMode` when the user picks an option.
* The target is determined by the project type (folder → CLI, repo → Cloud).
* Emits `onDidChange` with the selected `SessionTargetType` when the target changes.
*/
export class TargetPicker extends Disposable {
export class SessionTypePicker extends Disposable {
private _targetMode: TargetMode = 'worktree';
private _project: SessionProject | undefined;
private _isolationOptionEnabled: boolean = true;
private _sessionTarget: SessionTargetType = 'copilot-cli';
private _project: SessionWorkspace | undefined;
private readonly _onDidChange = this._register(new Emitter<TargetMode>());
readonly onDidChange: Event<TargetMode> = this._onDidChange.event;
private readonly _onDidChange = this._register(new Emitter<SessionTargetType>());
readonly onDidChange: Event<SessionTargetType> = this._onDidChange.event;
private readonly _renderDisposables = this._register(new DisposableStore());
private _slotElement: HTMLElement | undefined;
private _triggerElement: HTMLElement | undefined;
get targetMode(): TargetMode {
return this._targetMode;
get sessionTarget(): SessionTargetType {
return this._sessionTarget;
}
get isWorktree(): boolean {
return this._targetMode === 'worktree';
}
get isFolder(): boolean {
return this._targetMode === 'workspace';
get isCli(): boolean {
return this._sessionTarget === 'copilot-cli';
}
get isCloud(): boolean {
return this._targetMode === 'cloud';
return this._sessionTarget === 'cloud';
}
constructor(
) {
super();
}
/**
* Sets the current project context. Determines the target type:
* - Repo project → cloud
* - Folder project → cli
* - No project → retains current target
*/
setProject(project: SessionWorkspace | undefined): void {
this._project = project;
this._updateTarget();
this._updateTriggerLabel();
}
private _updateTarget(): void {
if (this._project?.isRepo) {
this._setTarget('cloud');
return;
}
if (this._project?.isFolder) {
this._setTarget('copilot-cli');
return;
}
}
render(container: HTMLElement): void {
this._renderDisposables.clear();
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot'));
this._slotElement = slot;
this._renderDisposables.add({ dispose: () => slot.remove() });
const trigger = dom.append(slot, dom.$('a.action-label'));
trigger.tabIndex = -1;
trigger.role = 'button';
trigger.setAttribute('aria-disabled', 'true');
this._triggerElement = trigger;
this._updateTriggerLabel();
}
private _setTarget(target: SessionTargetType): void {
if (this._sessionTarget !== target) {
this._sessionTarget = target;
this._updateTriggerLabel();
this._onDidChange.fire(target);
}
}
private _updateTriggerLabel(): void {
if (!this._triggerElement) {
return;
}
dom.clearNode(this._triggerElement);
let modeIcon;
let modeLabel: string;
switch (this._sessionTarget) {
case 'cloud':
modeIcon = Codicon.cloud;
modeLabel = localize('sessionTarget.cloud', "Cloud");
break;
case 'copilot-cli':
default:
modeIcon = Codicon.worktree;
modeLabel = localize('sessionTarget.cli', "Copilot CLI");
break;
}
dom.append(this._triggerElement, renderIcon(modeIcon));
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
labelSpan.textContent = modeLabel;
this._slotElement?.classList.toggle('disabled', true);
}
}
// #endregion
// #region --- Isolation Picker ---
/**
* A self-contained widget for selecting the isolation mode.
*
* Options:
* - **Worktree** (`worktree`) — run in a git worktree
* - **Folder** (`workspace`) — run directly in the folder
*
* Only visible when isolation option is enabled, project has a git repo,
* and the target is CLI.
*
* Emits `onDidChange` with the selected `IsolationMode` when the user picks an option.
*/
export class IsolationPicker extends Disposable {
private _isolationMode: IsolationMode = 'worktree';
private _hasGitRepo = false;
private _visible = true;
private _isolationOptionEnabled: boolean;
private readonly _onDidChange = this._register(new Emitter<IsolationMode>());
readonly onDidChange: Event<IsolationMode> = this._onDidChange.event;
private readonly _renderDisposables = this._register(new DisposableStore());
private _slotElement: HTMLElement | undefined;
private _triggerElement: HTMLElement | undefined;
get isolationMode(): IsolationMode {
return this._isolationMode;
}
get isWorktree(): boolean {
return this._isolationMode === 'worktree';
}
get isFolder(): boolean {
return this._isolationMode === 'workspace';
}
constructor(
@@ -67,35 +191,37 @@ export class TargetPicker extends Disposable {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('github.copilot.chat.cli.isolationOption.enabled')) {
this._isolationOptionEnabled = this.configurationService.getValue<boolean>('github.copilot.chat.cli.isolationOption.enabled') !== false;
this._updateMode();
if (!this._isolationOptionEnabled) {
// Reset to worktree when isolation option is disabled
this._setMode('worktree');
}
this._updateVisibility();
this._updateTriggerLabel();
}
}));
}
/**
* Sets the current project context. Determines the available target modes:
* - Repo project → cloud mode (disabled picker)
* - Folder with git repo → retains current local mode (worktree/folder)
* - Folder without git repo → folder mode only
* - No project → retains current mode
* Sets whether the project has a git repository.
* Resets isolation mode to the appropriate default.
*/
setProject(project: SessionProject | undefined): void {
this._project = project;
this._updateMode();
setHasGitRepo(hasRepo: boolean): void {
this._hasGitRepo = hasRepo;
if (!hasRepo) {
this._setMode('workspace');
} else {
this._setMode('worktree');
}
this._updateVisibility();
this._updateTriggerLabel();
}
private _updateMode(): void {
if (this._project?.isRepo) {
this._setMode('cloud');
return;
}
if (this._project?.isFolder) {
this._setMode(this._project.repository ? 'worktree' : 'workspace');
return;
}
/**
* Sets external visibility (e.g. hidden when target is Cloud).
*/
setVisible(visible: boolean): void {
this._visible = visible;
this._updateVisibility();
}
render(container: HTMLElement): void {
@@ -110,6 +236,7 @@ export class TargetPicker extends Disposable {
trigger.role = 'button';
this._triggerElement = trigger;
this._updateTriggerLabel();
this._updateVisibility();
this._renderDisposables.add(dom.addDisposableListener(trigger, dom.EventType.CLICK, (e) => {
dom.EventHelper.stop(e, true);
@@ -125,35 +252,31 @@ export class TargetPicker extends Disposable {
}
private _showPicker(): void {
if (!this._triggerElement || this.actionWidgetService.isVisible || this._targetMode === 'cloud') {
if (!this._triggerElement || this.actionWidgetService.isVisible) {
return;
}
// No picker when there's no git repo — only Folder mode is available
if (!this._project?.repository) {
if (!this._hasGitRepo || !this._isolationOptionEnabled) {
return;
}
const items: IActionListItem<TargetMode>[] = [
const items: IActionListItem<IsolationMode>[] = [
{
kind: ActionListItemKind.Action,
label: localize('targetMode.worktree', "Worktree"),
label: localize('isolationMode.worktree', "Worktree"),
group: { title: '', icon: Codicon.worktree },
item: 'worktree',
},
];
if (this._isolationOptionEnabled) {
items.push({
{
kind: ActionListItemKind.Action,
label: localize('targetMode.folder', "Folder"),
label: localize('isolationMode.folder', "Folder"),
group: { title: '', icon: Codicon.folder },
item: 'workspace',
});
}
},
];
const triggerElement = this._triggerElement;
const delegate: IActionListDelegate<TargetMode> = {
const delegate: IActionListDelegate<IsolationMode> = {
onSelect: (mode) => {
this.actionWidgetService.hide();
this._setMode(mode);
@@ -161,8 +284,8 @@ export class TargetPicker extends Disposable {
onHide: () => { triggerElement.focus(); },
};
this.actionWidgetService.show<TargetMode>(
'targetPicker',
this.actionWidgetService.show<IsolationMode>(
'isolationPicker',
false,
items,
delegate,
@@ -171,19 +294,27 @@ export class TargetPicker extends Disposable {
[],
{
getAriaLabel: (item) => item.label ?? '',
getWidgetAriaLabel: () => localize('targetPicker.ariaLabel', "Target"),
getWidgetAriaLabel: () => localize('isolationPicker.ariaLabel', "Isolation Mode"),
},
);
}
private _setMode(mode: TargetMode): void {
if (this._targetMode !== mode) {
this._targetMode = mode;
private _setMode(mode: IsolationMode): void {
if (this._isolationMode !== mode) {
this._isolationMode = mode;
this._updateTriggerLabel();
this._onDidChange.fire(mode);
}
}
private _updateVisibility(): void {
if (!this._slotElement) {
return;
}
const shouldShow = this._visible && this._hasGitRepo && this._isolationOptionEnabled;
this._slotElement.style.display = shouldShow ? '' : 'none';
}
private _updateTriggerLabel(): void {
if (!this._triggerElement) {
return;
@@ -193,25 +324,16 @@ export class TargetPicker extends Disposable {
let modeIcon;
let modeLabel: string;
let isDisabled: boolean = true;
if (this._project?.isFolder && this._project.repository) {
isDisabled = !this._isolationOptionEnabled;
}
switch (this._targetMode) {
case 'cloud':
modeIcon = Codicon.cloud;
modeLabel = localize('targetMode.cloud', "Cloud");
break;
switch (this._isolationMode) {
case 'workspace':
modeIcon = Codicon.folder;
modeLabel = localize('targetMode.folder', "Folder");
modeLabel = localize('isolationMode.folder', "Folder");
break;
case 'worktree':
default:
modeIcon = Codicon.worktree;
modeLabel = localize('targetMode.worktree', "Worktree");
modeLabel = localize('isolationMode.worktree', "Worktree");
break;
}
@@ -219,12 +341,6 @@ export class TargetPicker extends Disposable {
const labelSpan = dom.append(this._triggerElement, dom.$('span.sessions-chat-dropdown-label'));
labelSpan.textContent = modeLabel;
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
this._slotElement?.classList.toggle('disabled', isDisabled);
if (this._triggerElement) {
this._triggerElement.tabIndex = isDisabled ? -1 : 0;
this._triggerElement.setAttribute('aria-disabled', String(isDisabled));
}
}
}

View File

@@ -17,7 +17,7 @@ import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { GITHUB_REMOTE_FILE_SCHEME, SessionProject } from '../../sessions/common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME, SessionWorkspace } from '../../sessions/common/sessionWorkspace.js';
const OPEN_REPO_COMMAND = 'github.copilot.chat.cloudSessions.openRepository';
const STORAGE_KEY_LAST_PROJECT = 'sessions.lastPickedProject';
@@ -51,18 +51,18 @@ interface IStoredProject {
* - "Browse Folders..." opens a folder dialog
* - "Browse Repositories..." runs the cloud repository picker command
*/
export class ProjectPicker extends Disposable {
export class WorkspacePicker extends Disposable {
private readonly _onDidSelectProject = this._register(new Emitter<SessionProject>());
readonly onDidSelectProject: Event<SessionProject> = this._onDidSelectProject.event;
private readonly _onDidSelectProject = this._register(new Emitter<SessionWorkspace>());
readonly onDidSelectProject: Event<SessionWorkspace> = this._onDidSelectProject.event;
private _selectedProject: SessionProject | undefined;
private _selectedProject: SessionWorkspace | undefined;
private _recentProjects: IStoredProject[] = [];
private _triggerElement: HTMLElement | undefined;
private readonly _renderDisposables = this._register(new DisposableStore());
get selectedProject(): SessionProject | undefined {
get selectedProject(): SessionWorkspace | undefined {
return this._selectedProject;
}
@@ -134,7 +134,7 @@ export class ProjectPicker extends Disposable {
try {
const lastFolder = this.storageService.get(LEGACY_STORAGE_KEY_LAST_FOLDER, StorageScope.PROFILE);
if (lastFolder) {
this._selectedProject = new SessionProject(URI.parse(lastFolder));
this._selectedProject = new SessionWorkspace(URI.parse(lastFolder));
return;
}
} catch { /* ignore */ }
@@ -143,7 +143,7 @@ export class ProjectPicker extends Disposable {
const lastRepo = this.storageService.get(LEGACY_STORAGE_KEY_LAST_REPO, StorageScope.PROFILE);
if (lastRepo) {
const repo: { id: string; name: string } = JSON.parse(lastRepo);
this._selectedProject = new SessionProject(URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', path: `/${repo.id}/HEAD` }));
this._selectedProject = new SessionWorkspace(URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', path: `/${repo.id}/HEAD` }));
}
} catch { /* ignore */ }
}
@@ -155,7 +155,7 @@ export class ProjectPicker extends Disposable {
render(container: HTMLElement): HTMLElement {
this._renderDisposables.clear();
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot.sessions-chat-project-picker'));
const slot = dom.append(container, dom.$('.sessions-chat-picker-slot.sessions-chat-workspace-picker'));
this._renderDisposables.add({ dispose: () => slot.remove() });
const trigger = dom.append(slot, dom.$('a.action-label'));
@@ -207,10 +207,10 @@ export class ProjectPicker extends Disposable {
onHide: () => { triggerElement.focus(); },
};
const listOptions = showFilter ? { showFilter: true, filterPlaceholder: localize('projectPicker.filter', "Filter projects...") } : undefined;
const listOptions = showFilter ? { showFilter: true, filterPlaceholder: localize('workspacePicker.filter', "Search Workspaces...") } : undefined;
this.actionWidgetService.show<IStoredProject>(
'projectPicker',
'workspacePicker',
false,
items,
delegate,
@@ -219,7 +219,7 @@ export class ProjectPicker extends Disposable {
[],
{
getAriaLabel: (item) => item.label ?? '',
getWidgetAriaLabel: () => localize('projectPicker.ariaLabel', "Project Picker"),
getWidgetAriaLabel: () => localize('workspacePicker.ariaLabel', "Workspace Picker"),
},
listOptions,
);
@@ -229,7 +229,7 @@ export class ProjectPicker extends Disposable {
* Programmatically set the selected project.
* @param fireEvent Whether to fire the onDidSelectProject event. Defaults to true.
*/
setSelectedProject(project: SessionProject, fireEvent = true): void {
setSelectedProject(project: SessionWorkspace, fireEvent = true): void {
this._selectProject(project, fireEvent);
}
@@ -254,7 +254,7 @@ export class ProjectPicker extends Disposable {
}
}
private _selectProject(project: SessionProject, fireEvent = true): void {
private _selectProject(project: SessionWorkspace, fireEvent = true): void {
this._selectedProject = project;
const stored = this._toStored(project);
this._addToRecents(stored);
@@ -274,7 +274,7 @@ export class ProjectPicker extends Disposable {
title: localize('selectFolder', "Select Folder"),
});
if (selected?.[0]) {
this._selectProject(new SessionProject(selected[0]));
this._selectProject(new SessionWorkspace(selected[0]));
}
} catch {
// dialog was cancelled or failed
@@ -285,7 +285,7 @@ export class ProjectPicker extends Disposable {
try {
const result: string | undefined = await this.commandService.executeCommand(OPEN_REPO_COMMAND);
if (result) {
this._selectProject(new SessionProject(URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', path: `/${result}/HEAD` })));
this._selectProject(new SessionWorkspace(URI.from({ scheme: GITHUB_REMOTE_FILE_SCHEME, authority: 'github', path: `/${result}/HEAD` })));
}
} catch {
// command was cancelled or failed
@@ -386,7 +386,7 @@ export class ProjectPicker extends Disposable {
dom.clearNode(this._triggerElement);
const project = this._selectedProject;
const label = project ? this._getProjectLabel(project) : localize('pickProject', "Pick a Project");
const label = project ? this._getProjectLabel(project) : localize('pickWorkspace', "Pick a Workspace");
const icon = project ? (project.isFolder ? Codicon.folder : Codicon.repo) : Codicon.project;
dom.append(this._triggerElement, renderIcon(icon));
@@ -395,7 +395,7 @@ export class ProjectPicker extends Disposable {
dom.append(this._triggerElement, renderIcon(Codicon.chevronDown));
}
private _getProjectLabel(project: SessionProject): string {
private _getProjectLabel(project: SessionWorkspace): string {
return this._getStoredProjectLabel({ uri: project.uri.toJSON() });
}
@@ -408,14 +408,14 @@ export class ProjectPicker extends Disposable {
return uri.path.substring(1).replace(/\/HEAD$/, '');
}
private _toStored(project: SessionProject): IStoredProject {
private _toStored(project: SessionWorkspace): IStoredProject {
return {
uri: project.uri.toJSON(),
};
}
private _fromStored(stored: IStoredProject): SessionProject {
return new SessionProject(URI.revive(stored.uri));
private _fromStored(stored: IStoredProject): SessionWorkspace {
return new SessionWorkspace(URI.revive(stored.uri));
}
private _projectKey(project: IStoredProject): string {

View File

@@ -8,7 +8,7 @@ import { IFileService } from '../../../../platform/files/common/files.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
import { GitHubFileSystemProvider } from './githubFileSystemProvider.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionWorkspace.js';
// --- View registration is currently disabled in favor of the "Add Context" picker.
// The Files view will be re-enabled once we finalize the sessions auxiliary bar layout.

View File

@@ -44,7 +44,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet
import { ILogService } from '../../../../platform/log/common/log.js';
import { ISessionsManagementService, IActiveSessionItem } from '../../sessions/browser/sessionsManagementService.js';
import { getGitHubRemoteFileDisplayName } from './githubFileSystemProvider.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionWorkspace.js';
import { basename } from '../../../../base/common/path.js';
import { isEqual } from '../../../../base/common/resources.js';

View File

@@ -12,7 +12,7 @@ import { IAuthenticationService } from '../../../../workbench/services/authentic
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../sessions/common/sessionWorkspace.js';
/**
* Derives a display name from a github-remote-file URI.

View File

@@ -10,7 +10,7 @@ import { NullLogService, ILogService } from '../../../../../platform/log/common/
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { GitHubService } from '../../browser/githubService.js';
import { URI } from '../../../../../base/common/uri.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../../sessions/common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../../../sessions/common/sessionWorkspace.js';
import { IActiveSessionItem } from '../../../sessions/browser/sessionsManagementService.js';
suite('GitHubService', () => {

View File

@@ -21,12 +21,12 @@ import { IAgentSession, isAgentSession } from '../../../../workbench/contrib/cha
import { IAgentSessionsService } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
import { INewSession, LocalNewSession, RemoteNewSession } from '../../chat/browser/newSession.js';
import { INewSession, CopilotCLISession, RemoteNewSession } from '../../chat/browser/newSession.js';
import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js';
import { isBuiltinChatMode } from '../../../../workbench/contrib/chat/common/chatModes.js';
import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
import { ILanguageModelToolsService } from '../../../../workbench/contrib/chat/common/tools/languageModelToolsService.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../common/sessionProject.js';
import { GITHUB_REMOTE_FILE_SCHEME } from '../common/sessionWorkspace.js';
import { IGitHubSessionContext } from '../../github/common/types.js';
import { ResourceSet } from '../../../../base/common/map.js';
@@ -267,7 +267,7 @@ export class SessionsManagementService extends Disposable implements ISessionsMa
let newSession: INewSession;
if (target === AgentSessionProviders.Background) {
newSession = this.instantiationService.createInstance(LocalNewSession, sessionResource, defaultRepoUri);
newSession = this.instantiationService.createInstance(CopilotCLISession, sessionResource, defaultRepoUri);
} else {
newSession = this.instantiationService.createInstance(RemoteNewSession, sessionResource, target);
}

View File

@@ -9,10 +9,10 @@ import { IGitRepository } from '../../../../workbench/contrib/git/common/gitServ
export const GITHUB_REMOTE_FILE_SCHEME = 'github-remote-file';
/**
* Represents a project (folder or repository) for a session.
* The project type (folder vs repo) is derived from the URI scheme.
* Represents a workspace (folder or repository) for a session.
* The workspace type (folder vs repo) is derived from the URI scheme.
*/
export class SessionProject {
export class SessionWorkspace {
readonly uri: URI;
readonly repository: IGitRepository | undefined;
@@ -22,18 +22,18 @@ export class SessionProject {
this.repository = repository;
}
/** Whether this is a local folder project. */
/** Whether this is a local folder workspace. */
get isFolder(): boolean {
return this.uri.scheme !== GITHUB_REMOTE_FILE_SCHEME;
}
/** Whether this is a remote repository project. */
/** Whether this is a remote repository workspace. */
get isRepo(): boolean {
return this.uri.scheme === GITHUB_REMOTE_FILE_SCHEME;
}
/** Returns a new SessionProject with the repository updated. */
withRepository(repository: IGitRepository | undefined): SessionProject {
return new SessionProject(this.uri, repository);
/** Returns a new SessionWorkspace with the repository updated. */
withRepository(repository: IGitRepository | undefined): SessionWorkspace {
return new SessionWorkspace(this.uri, repository);
}
}