From 373571f674638d235917de6b795419d30392c801 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 25 Mar 2026 22:15:56 +0100 Subject: [PATCH 1/2] improve sessions workspace picker --- .../actionWidget/browser/actionList.ts | 94 +++++++++---- src/vs/sessions/SESSIONS_PROVIDER.md | 2 +- .../chat/browser/sessionWorkspacePicker.ts | 125 +++++++++++++----- .../browser/copilotChatSessionsProvider.ts | 4 +- .../remoteAgentHostSessionsProvider.ts | 2 +- .../browser/defaultCopilotSessionsProvider.ts | 4 - .../sessions/browser/media/sessionsList.css | 10 ++ .../sessions/browser/sessionsListControl.ts | 4 - .../sessionsProvidersServiceInterface.ts | 4 - .../sessions/browser/views/sessionsList.ts | 34 ++++- .../browser/views/sessionsViewActions.ts | 121 +++++++++++++++-- .../browser/views/sessionsViewPane.ts | 4 - 12 files changed, 311 insertions(+), 97 deletions(-) delete mode 100644 src/vs/sessions/contrib/sessions/browser/defaultCopilotSessionsProvider.ts delete mode 100644 src/vs/sessions/contrib/sessions/browser/sessionsListControl.ts delete mode 100644 src/vs/sessions/contrib/sessions/browser/sessionsProvidersServiceInterface.ts delete mode 100644 src/vs/sessions/contrib/sessions/browser/views/sessionsViewPane.ts diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index fc09cb44b394e..d5a9be44e3cc2 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -191,7 +191,8 @@ class ActionItemRenderer implements IListRenderer, IAction constructor( private readonly _supportsPreview: boolean, private readonly _onRemoveItem: ((item: IActionListItem) => void) | undefined, - private _hasAnySubmenuActions: boolean, + private readonly _onShowSubmenu: ((item: IActionListItem) => void) | undefined, + private readonly _hasAnySubmenuActions: boolean, private readonly _linkHandler: ((uri: URI, item: IActionListItem) => void) | undefined, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IOpenerService private readonly _openerService: IOpenerService, @@ -342,17 +343,22 @@ class ActionItemRenderer implements IListRenderer, IAction actionBar.push(toolbarActions, { icon: true, label: false }); } - // Show submenu indicator for items with submenu actions - const hasSubmenu = !!element.submenuActions?.length; - if (hasSubmenu) { + // Show submenu indicator only for items with submenu actions + if (element.submenuActions?.length) { data.submenuIndicator.className = 'action-list-submenu-indicator has-submenu ' + ThemeIcon.asClassName(Codicon.chevronRight); data.submenuIndicator.style.display = ''; + data.submenuIndicator.style.visibility = ''; + data.elementDisposables.add(dom.addDisposableListener(data.submenuIndicator, dom.EventType.CLICK, (e) => { + e.stopPropagation(); + this._onShowSubmenu?.(element); + })); } else if (this._hasAnySubmenuActions) { // Reserve space for alignment when other items have submenus data.submenuIndicator.className = 'action-list-submenu-indicator'; data.submenuIndicator.style.display = ''; + data.submenuIndicator.style.visibility = 'hidden'; } else { - // No items have submenu actions — hide completely + data.submenuIndicator.className = 'action-list-submenu-indicator'; data.submenuIndicator.style.display = 'none'; } } @@ -431,6 +437,12 @@ export interface IActionListOptions { * When true and filtering is enabled, focuses the filter input when the list opens. */ readonly focusFilterOnOpen?: boolean; + + /** + * When false, non-submenu items do not reserve space for the submenu chevron. + * Defaults to true for alignment consistency. + */ + readonly reserveSubmenuSpace?: boolean; } /** @@ -534,10 +546,11 @@ export class ActionListWidget extends Disposable { }; - const hasAnySubmenuActions = items.some(item => !!item.submenuActions?.length); + const reserveSubmenuSpace = this._options?.reserveSubmenuSpace ?? true; + const hasAnySubmenuActions = reserveSubmenuSpace && items.some(item => !!item.submenuActions?.length); this._list = this._register(new List(user, this.domNode, virtualDelegate, [ - new ActionItemRenderer(preview, (item) => this._removeItem(item), hasAnySubmenuActions, this._options?.linkHandler, this._keybindingService, this._openerService), + new ActionItemRenderer(preview, (item) => this._removeItem(item), (item) => this._showSubmenuForItem(item), hasAnySubmenuActions, this._options?.linkHandler, this._keybindingService, this._openerService), new HeaderRenderer(), new SeparatorRenderer(), ], { @@ -1106,10 +1119,10 @@ export class ActionListWidget extends Disposable { this._list.setSelection([]); return; } - // Don't select when clicking the submenu indicator - if (element.submenuActions?.length && dom.isMouseEvent(e.browserEvent)) { + // Don't select when clicking the toolbar or submenu indicator + if (dom.isMouseEvent(e.browserEvent)) { const target = e.browserEvent.target; - if (dom.isHTMLElement(target) && target.closest('.action-list-submenu-indicator')) { + if (dom.isHTMLElement(target) && (target.closest('.action-list-item-toolbar') || target.closest('.action-list-submenu-indicator'))) { this._list.setSelection([]); return; } @@ -1201,6 +1214,16 @@ export class ActionListWidget extends Disposable { }, { groupId: `actionListHover` }); } + private _showSubmenuForItem(item: IActionListItem): void { + const index = this._allMenuItems.indexOf(item); + if (index >= 0) { + const rowElement = this._getRowElement(index); + if (rowElement) { + this._showSubmenuForElement(item, rowElement); + } + } + } + private _showSubmenuForElement(element: IActionListItem, anchor: HTMLElement): void { this._submenuDisposables.clear(); this._hover.clear(); @@ -1209,26 +1232,38 @@ export class ActionListWidget extends Disposable { // Convert submenu actions into ActionListWidget items const submenuItems: IActionListItem[] = []; + const submenuGroups = element.submenuActions!.filter((a): a is SubmenuAction => a instanceof SubmenuAction); + const groupsWithActions = submenuGroups.filter(g => g.actions.length > 0); + for (let gi = 0; gi < groupsWithActions.length; gi++) { + const group = groupsWithActions[gi]; + for (let ci = 0; ci < group.actions.length; ci++) { + const child = group.actions[ci]; + submenuItems.push({ + item: child, + kind: ActionListItemKind.Action, + label: child.label, + description: ci === 0 && group.label ? group.label : (child.tooltip || undefined), + group: { title: '', icon: ThemeIcon.fromId(child.checked ? Codicon.check.id : Codicon.blank.id) }, + hideIcon: false, + hover: {}, + }); + } + if (gi < groupsWithActions.length - 1) { + submenuItems.push({ kind: ActionListItemKind.Separator, label: '' }); + } + } + // Also include non-SubmenuAction items directly for (const action of element.submenuActions!) { - if (action instanceof SubmenuAction) { - // Add header for the group + if (!(action instanceof SubmenuAction)) { submenuItems.push({ - kind: ActionListItemKind.Header, - group: { title: action.label }, + item: action, + kind: ActionListItemKind.Action, label: action.label, + description: action.tooltip || undefined, + group: { title: '' }, + hideIcon: false, + hover: {}, }); - // Add each child action as a selectable item - for (const child of action.actions) { - submenuItems.push({ - item: child, - kind: ActionListItemKind.Action, - label: child.label, - description: child.tooltip || undefined, - group: { title: '', icon: ThemeIcon.fromId(child.checked ? Codicon.check.id : Codicon.blank.id) }, - hideIcon: false, - hover: {}, - }); - } } } @@ -1365,10 +1400,13 @@ export class ActionListWidget extends Disposable { if (element && element.item && this.focusCondition(element)) { // Check if the hover target is inside a toolbar - if so, skip the splice - // to avoid re-rendering which would destroy the element mid-hover + // to avoid re-rendering which would destroy the element mid-hover. + // But still maintain submenu state for items with submenu actions. const isHoveringToolbar = dom.isHTMLElement(e.browserEvent.target) && e.browserEvent.target.closest('.action-list-item-toolbar') !== null; if (isHoveringToolbar) { - this._cancelSubmenuShow(); + if (!element.submenuActions?.length) { + this._cancelSubmenuShow(); + } this._list.setFocus([]); return; } diff --git a/src/vs/sessions/SESSIONS_PROVIDER.md b/src/vs/sessions/SESSIONS_PROVIDER.md index c655708c08c2b..596803cd782ac 100644 --- a/src/vs/sessions/SESSIONS_PROVIDER.md +++ b/src/vs/sessions/SESSIONS_PROVIDER.md @@ -11,7 +11,7 @@ This design allows new compute environments (remote agent hosts, cloud backends, ``` ┌─────────────────────────────────────────────────────────────────┐ │ UI Components │ -│ (SessionsView, TitleBar, NewSession, ChatWidget) │ +│ (SessionsView, TitleBar, NewSession, Changes | Terminal) │ └───────────────────────────┬─────────────────────────────────────┘ │ ┌───────────▼────────────┐ diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index b279cc8b2546c..5a65bbfffde59 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../base/browser/dom.js'; +import { SubmenuAction, toAction } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; @@ -88,22 +89,26 @@ export class WorkspacePicker extends Disposable { // Restore selected workspace from storage this._selectedWorkspace = this._restoreSelectedWorkspace(); - // If restore failed (providers not yet registered), retry when providers appear - if (!this._selectedWorkspace && this._hasStoredWorkspace()) { - const providerListener = this._register(this.sessionsProvidersService.onDidChangeProviders(() => { - if (!this._selectedWorkspace) { - const restored = this._restoreSelectedWorkspace(); - if (restored) { - this._selectedWorkspace = restored; - this._updateTriggerLabel(); - this._onDidSelectWorkspace.fire(restored); - } + // React to provider registrations/removals: re-validate the current + // selection and attempt to restore a stored workspace when none is active. + this._register(this.sessionsProvidersService.onDidChangeProviders(() => { + if (this._selectedWorkspace) { + // Validate that the selected workspace's provider is still registered + const providers = this.sessionsProvidersService.getProviders(); + if (!providers.some(p => p.id === this._selectedWorkspace!.providerId)) { + this._selectedWorkspace = undefined; + this._updateTriggerLabel(); } - if (this._selectedWorkspace) { - providerListener.dispose(); + } + if (!this._selectedWorkspace) { + const restored = this._restoreSelectedWorkspace(); + if (restored) { + this._selectedWorkspace = restored; + this._updateTriggerLabel(); + this._onDidSelectWorkspace.fire(restored); } - })); - } + } + })); } /** @@ -162,7 +167,7 @@ export class WorkspacePicker extends Disposable { onHide: () => { triggerElement.focus(); }, }; - const listOptions = showFilter ? { showFilter: true, filterPlaceholder: localize('workspacePicker.filter', "Search Workspaces...") } : undefined; + const listOptions = showFilter ? { showFilter: true, filterPlaceholder: localize('workspacePicker.filter', "Search Workspaces..."), reserveSubmenuSpace: false } : { reserveSubmenuSpace: false }; this.actionWidgetService.show( 'workspacePicker', @@ -267,28 +272,27 @@ export class WorkspacePicker extends Disposable { const hasMultipleProviders = allProviders.length > 1; if (hasMultipleProviders) { - // Group workspaces by provider - for (const provider of allProviders) { + // Group workspaces by provider, showing provider name as description on the first entry + const providersWithWorkspaces = allProviders.filter(p => recentWorkspaces.some(w => w.providerId === p.id)); + for (let pi = 0; pi < providersWithWorkspaces.length; pi++) { + const provider = providersWithWorkspaces[pi]; const providerWorkspaces = recentWorkspaces.filter(w => w.providerId === provider.id); - if (providerWorkspaces.length === 0) { - continue; - } - items.push({ - kind: ActionListItemKind.Header, - label: provider.label, - group: { title: provider.label, icon: provider.icon }, - item: {}, - }); - for (const { workspace, providerId } of providerWorkspaces) { + for (let i = 0; i < providerWorkspaces.length; i++) { + const { workspace, providerId } = providerWorkspaces[i]; const selection: IWorkspaceSelection = { providerId, workspace }; const selected = this._isSelectedWorkspace(selection); items.push({ kind: ActionListItemKind.Action, label: workspace.label, + description: i === 0 ? provider.label : undefined, group: { title: '', icon: workspace.icon }, item: { selection, checked: selected || undefined }, + onRemove: () => this._removeRecentWorkspace(selection), }); } + if (pi < providersWithWorkspaces.length - 1) { + items.push({ kind: ActionListItemKind.Separator, label: '' }); + } } } else { for (const { workspace, providerId } of recentWorkspaces) { @@ -299,6 +303,7 @@ export class WorkspacePicker extends Disposable { label: workspace.label, group: { title: '', icon: workspace.icon }, item: { selection, checked: selected || undefined }, + onRemove: () => this._removeRecentWorkspace(selection), }); } } @@ -308,14 +313,48 @@ export class WorkspacePicker extends Disposable { if (items.length > 0 && allBrowseActions.length > 0) { items.push({ kind: ActionListItemKind.Separator, label: '' }); } - for (let i = 0; i < allBrowseActions.length; i++) { - const action = allBrowseActions[i]; + if (hasMultipleProviders && allBrowseActions.length > 1) { + // Show a single "Browse..." entry with provider-grouped submenu actions + const providerMap = new Map(); + allBrowseActions.forEach((action, i) => { + let entry = providerMap.get(action.providerId); + if (!entry) { + const provider = allProviders.find(p => p.id === action.providerId); + if (!provider) { return; } + entry = { provider, actions: [] }; + providerMap.set(action.providerId, entry); + } + entry.actions.push({ action, index: i }); + }); + const submenuActions = [...providerMap.values()].map(({ provider, actions }) => + new SubmenuAction( + `workspacePicker.browse.${provider.id}`, + provider.label, + actions.map(({ action, index }) => toAction({ + id: `workspacePicker.browse.${index}`, + label: localize(`workspacePicker.browse`, "{0}...", action.label), + tooltip: '', + run: () => this._executeBrowseAction(index), + })), + ) + ); items.push({ kind: ActionListItemKind.Action, - label: action.label, - group: { title: '', icon: action.icon }, - item: { browseActionIndex: i }, + label: localize('workspacePicker.browse', "Select..."), + group: { title: '', icon: Codicon.folderOpened }, + item: {}, + submenuActions, }); + } else { + for (let i = 0; i < allBrowseActions.length; i++) { + const action = allBrowseActions[i]; + items.push({ + kind: ActionListItemKind.Action, + label: localize(`workspacePicker.browse`, "Select {0}...", action.label), + group: { title: '', icon: action.icon }, + item: { browseActionIndex: i }, + }); + } } return items; @@ -353,10 +392,6 @@ export class WorkspacePicker extends Disposable { this._addRecentWorkspace(selection.providerId, selection.workspace, true); } - private _hasStoredWorkspace(): boolean { - return this._getStoredRecentWorkspaces().length > 0; - } - private _restoreSelectedWorkspace(): IWorkspaceSelection | undefined { try { const providers = this._getActiveProviders(); @@ -469,6 +504,24 @@ export class WorkspacePicker extends Disposable { }); } + private _removeRecentWorkspace(selection: IWorkspaceSelection): void { + const uri = selection.workspace.repositories[0]?.uri; + if (!uri) { + return; + } + const recents = this._getStoredRecentWorkspaces(); + const updated = recents.filter(p => + !(p.providerId === selection.providerId && this.uriIdentityService.extUri.isEqual(URI.revive(p.uri), uri)) + ); + this.storageService.store(STORAGE_KEY_RECENT_WORKSPACES, JSON.stringify(updated), StorageScope.PROFILE, StorageTarget.MACHINE); + + // Clear current selection if it was the removed workspace + if (this._isSelectedWorkspace(selection)) { + this._selectedWorkspace = undefined; + this._updateTriggerLabel(); + } + } + private _getStoredRecentWorkspaces(): IStoredRecentWorkspace[] { const raw = this.storageService.get(STORAGE_KEY_RECENT_WORKSPACES, StorageScope.PROFILE); if (!raw) { diff --git a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts index 41f12d816bcbf..4916e8da83f9d 100644 --- a/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts +++ b/src/vs/sessions/contrib/copilotChatSessions/browser/copilotChatSessionsProvider.ts @@ -755,13 +755,13 @@ export class CopilotChatSessionsProvider extends Disposable implements ISessions this.browseActions = [ { - label: 'Browse Folders...', + label: localize('folders', "Folders"), icon: Codicon.folderOpened, providerId: this.id, execute: () => this._browseForFolder(), }, { - label: 'Browse Repositories...', + label: localize('repositories', "Repositories"), icon: Codicon.repo, providerId: this.id, execute: () => this._browseForRepo(), diff --git a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts index 47280c291fbf3..18ab6b36765e8 100644 --- a/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts +++ b/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts @@ -59,7 +59,7 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess this.sessionTypes = [CopilotCLISessionType]; this.browseActions = [{ - label: localize('browseRemote', "Browse Remote Folders..."), + label: localize('folders', "Folders"), icon: Codicon.remote, providerId: this.id, execute: () => this._browseForFolder(), diff --git a/src/vs/sessions/contrib/sessions/browser/defaultCopilotSessionsProvider.ts b/src/vs/sessions/contrib/sessions/browser/defaultCopilotSessionsProvider.ts deleted file mode 100644 index a4a092d83492f..0000000000000 --- a/src/vs/sessions/contrib/sessions/browser/defaultCopilotSessionsProvider.ts +++ /dev/null @@ -1,4 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index 490b929758f68..75a78b550c99d 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -323,6 +323,16 @@ opacity: 0.7; margin-right: 4px; } + + .session-section-toolbar { + margin-left: auto; + display: none; + } +} + +.monaco-list-row:hover .session-section .session-section-toolbar, +.monaco-list-row.focused .session-section .session-section-toolbar { + display: block; } .sessions-list-control { diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsListControl.ts b/src/vs/sessions/contrib/sessions/browser/sessionsListControl.ts deleted file mode 100644 index a4a092d83492f..0000000000000 --- a/src/vs/sessions/contrib/sessions/browser/sessionsListControl.ts +++ /dev/null @@ -1,4 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/sessions/contrib/sessions/browser/sessionsProvidersServiceInterface.ts b/src/vs/sessions/contrib/sessions/browser/sessionsProvidersServiceInterface.ts deleted file mode 100644 index a4a092d83492f..0000000000000 --- a/src/vs/sessions/contrib/sessions/browser/sessionsProvidersServiceInterface.ts +++ /dev/null @@ -1,4 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index fe7ec83606662..3a746194b8fd0 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -40,9 +40,11 @@ const $ = DOM.$; export const SessionItemToolbarMenuId = new MenuId('SessionItemToolbar'); export const SessionItemContextMenuId = new MenuId('SessionItemContextMenu'); +export const SessionSectionToolbarMenuId = new MenuId('SessionSectionToolbar'); export const IsSessionPinnedContext = new RawContextKey('sessionItem.isPinned', false); export const IsSessionArchivedContext = new RawContextKey('sessionItem.isArchived', false); export const IsSessionReadContext = new RawContextKey('sessionItem.isRead', true); +export const SessionSectionTypeContext = new RawContextKey('sessionSection.type', ''); //#region Types @@ -435,19 +437,36 @@ interface ISessionSectionTemplate { readonly container: HTMLElement; readonly label: HTMLElement; readonly count: HTMLElement; + readonly toolbar: MenuWorkbenchToolBar; + readonly contextKeyService: IContextKeyService; + readonly disposables: DisposableStore; } class SessionSectionRenderer implements ITreeRenderer { static readonly TEMPLATE_ID = 'session-section'; readonly templateId = SessionSectionRenderer.TEMPLATE_ID; - constructor(private readonly hideSectionCount: boolean) { } + constructor( + private readonly hideSectionCount: boolean, + private readonly instantiationService: IInstantiationService, + private readonly contextKeyService: IContextKeyService, + ) { } renderTemplate(container: HTMLElement): ISessionSectionTemplate { + const disposables = new DisposableStore(); + container.classList.add('session-section'); const label = DOM.append(container, $('span.session-section-label')); const count = DOM.append(container, $('span.session-section-count')); - return { container, label, count }; + const toolbarContainer = DOM.append(container, $('.session-section-toolbar')); + + const contextKeyService = disposables.add(this.contextKeyService.createScoped(container)); + const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService]))); + const toolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarContainer, SessionSectionToolbarMenuId, { + menuOptions: { shouldForwardArgs: true }, + })); + + return { container, label, count, toolbar, contextKeyService, disposables }; } renderElement(node: ITreeNode, _index: number, template: ISessionSectionTemplate): void { @@ -463,9 +482,16 @@ class SessionSectionRenderer implements ITreeRenderer { - if (!context || !isAgentSessionSection(context) || context.sessions.length === 0) { + async run(accessor: ServicesAccessor, context?: ISessionSection): Promise { + if (!context || !context.sessions || context.sessions.length === 0) { return; } const sessionsManagementService = accessor.get(ISessionsManagementService); @@ -265,6 +265,109 @@ registerAction2(class NewSessionForRepositoryAction extends Action2 { } }); +const ConfirmArchiveStorageKey = 'sessions.confirmArchive'; + +registerAction2(class ArchiveSectionAction extends Action2 { + constructor() { + super({ + id: 'sessionsView.sectionArchive', + title: localize2('archiveSection', "Archive All"), + icon: Codicon.archive, + menu: [{ + id: SessionSectionToolbarMenuId, + group: 'navigation', + order: 1, + when: ContextKeyExpr.notEquals(SessionSectionTypeContext.key, 'archived'), + }] + }); + } + async run(accessor: ServicesAccessor, context?: ISessionSection): Promise { + if (!context || !context.sessions || context.sessions.length === 0) { + return; + } + + const sessionsManagementService = accessor.get(ISessionsManagementService); + const dialogService = accessor.get(IDialogService); + const storageService = accessor.get(IStorageService); + + const skipConfirmation = storageService.getBoolean(ConfirmArchiveStorageKey, StorageScope.PROFILE, false); + if (!skipConfirmation) { + const confirmed = await dialogService.confirm({ + message: context.sessions.length === 1 + ? localize('archiveSectionSessions.confirmSingle', "Are you sure you want to archive 1 session from '{0}'?", context.label) + : localize('archiveSectionSessions.confirm', "Are you sure you want to archive {0} sessions from '{1}'?", context.sessions.length, context.label), + detail: localize('archiveSectionSessions.detail', "You can unarchive sessions later if needed from the sessions view."), + primaryButton: localize('archiveSectionSessions.archive', "Archive All"), + checkbox: { + label: localize('doNotAskAgain', "Do not ask me again") + } + }); + + if (!confirmed.confirmed) { + return; + } + + if (confirmed.checkboxChecked) { + storageService.store(ConfirmArchiveStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); + } + } + + for (const session of context.sessions) { + await sessionsManagementService.archiveSession(session); + } + } +}); + +registerAction2(class UnarchiveSectionAction extends Action2 { + constructor() { + super({ + id: 'sessionsView.sectionUnarchive', + title: localize2('unarchiveSection', "Unarchive All"), + icon: Codicon.unarchive, + menu: [{ + id: SessionSectionToolbarMenuId, + group: 'navigation', + order: 1, + when: ContextKeyExpr.equals(SessionSectionTypeContext.key, 'archived'), + }] + }); + } + async run(accessor: ServicesAccessor, context?: ISessionSection): Promise { + if (!context || !context.sessions || context.sessions.length === 0) { + return; + } + + const sessionsManagementService = accessor.get(ISessionsManagementService); + const dialogService = accessor.get(IDialogService); + const storageService = accessor.get(IStorageService); + + if (context.sessions.length > 1) { + const skipConfirmation = storageService.getBoolean(ConfirmArchiveStorageKey, StorageScope.PROFILE, false); + if (!skipConfirmation) { + const confirmed = await dialogService.confirm({ + message: localize('unarchiveSectionSessions.confirm', "Are you sure you want to unarchive {0} sessions?", context.sessions.length), + primaryButton: localize('unarchiveSectionSessions.unarchive', "Unarchive All"), + checkbox: { + label: localize('doNotAskAgain2', "Do not ask me again") + } + }); + + if (!confirmed.confirmed) { + return; + } + + if (confirmed.checkboxChecked) { + storageService.store(ConfirmArchiveStorageKey, true, StorageScope.PROFILE, StorageTarget.USER); + } + } + } + + for (const session of context.sessions) { + await sessionsManagementService.unarchiveSession(session); + } + } +}); + // Session Item Actions registerAction2(class PinSessionAction extends Action2 { diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewPane.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsViewPane.ts deleted file mode 100644 index a4a092d83492f..0000000000000 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsViewPane.ts +++ /dev/null @@ -1,4 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ From 3035d01db444250c225f2108d909b57a4f1cbf88 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 25 Mar 2026 22:30:35 +0100 Subject: [PATCH 2/2] feedback --- src/vs/platform/actionWidget/browser/actionList.ts | 2 +- .../contrib/chat/browser/sessionWorkspacePicker.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index d5a9be44e3cc2..43610b427512d 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -1215,7 +1215,7 @@ export class ActionListWidget extends Disposable { } private _showSubmenuForItem(item: IActionListItem): void { - const index = this._allMenuItems.indexOf(item); + const index = this._list.indexOf(item); if (index >= 0) { const rowElement = this._getRowElement(index); if (rowElement) { diff --git a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts index 5a65bbfffde59..d23a2d5c8f547 100644 --- a/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts +++ b/src/vs/sessions/contrib/chat/browser/sessionWorkspacePicker.ts @@ -380,8 +380,12 @@ export class WorkspacePicker extends Disposable { if (!this._selectedWorkspace) { return false; } - return this._selectedWorkspace.providerId === selection.providerId - && this._selectedWorkspace.workspace.label === selection.workspace.label; + if (this._selectedWorkspace.providerId !== selection.providerId) { + return false; + } + const selectedUri = this._selectedWorkspace.workspace.repositories[0]?.uri; + const candidateUri = selection.workspace.repositories[0]?.uri; + return this.uriIdentityService.extUri.isEqual(selectedUri, candidateUri); } private _persistSelectedWorkspace(selection: IWorkspaceSelection): void {