mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-02 08:15:56 +01:00
improve sessions workspace picker (#304907)
* improve sessions workspace picker * feedback
This commit is contained in:
committed by
GitHub
parent
ae7b6654c6
commit
f6218ecb33
@@ -191,7 +191,8 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
|
||||
constructor(
|
||||
private readonly _supportsPreview: boolean,
|
||||
private readonly _onRemoveItem: ((item: IActionListItem<T>) => void) | undefined,
|
||||
private _hasAnySubmenuActions: boolean,
|
||||
private readonly _onShowSubmenu: ((item: IActionListItem<T>) => void) | undefined,
|
||||
private readonly _hasAnySubmenuActions: boolean,
|
||||
private readonly _linkHandler: ((uri: URI, item: IActionListItem<T>) => void) | undefined,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@@ -342,17 +343,22 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -528,10 +540,11 @@ export class ActionListWidget<T> 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<T>(preview, (item) => this._removeItem(item), hasAnySubmenuActions, this._options?.linkHandler, this._keybindingService, this._openerService),
|
||||
new ActionItemRenderer<T>(preview, (item) => this._removeItem(item), (item) => this._showSubmenuForItem(item), hasAnySubmenuActions, this._options?.linkHandler, this._keybindingService, this._openerService),
|
||||
new HeaderRenderer(),
|
||||
new SeparatorRenderer(),
|
||||
], {
|
||||
@@ -1100,10 +1113,10 @@ export class ActionListWidget<T> 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;
|
||||
}
|
||||
@@ -1195,6 +1208,16 @@ export class ActionListWidget<T> extends Disposable {
|
||||
}, { groupId: `actionListHover` });
|
||||
}
|
||||
|
||||
private _showSubmenuForItem(item: IActionListItem<T>): void {
|
||||
const index = this._list.indexOf(item);
|
||||
if (index >= 0) {
|
||||
const rowElement = this._getRowElement(index);
|
||||
if (rowElement) {
|
||||
this._showSubmenuForElement(item, rowElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _showSubmenuForElement(element: IActionListItem<T>, anchor: HTMLElement): void {
|
||||
this._submenuDisposables.clear();
|
||||
this._hover.clear();
|
||||
@@ -1203,26 +1226,38 @@ export class ActionListWidget<T> extends Disposable {
|
||||
|
||||
// Convert submenu actions into ActionListWidget items
|
||||
const submenuItems: IActionListItem<IAction>[] = [];
|
||||
for (const action of element.submenuActions!) {
|
||||
if (action instanceof SubmenuAction) {
|
||||
// Add header for the group
|
||||
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({
|
||||
kind: ActionListItemKind.Header,
|
||||
group: { title: action.label },
|
||||
label: action.label,
|
||||
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)) {
|
||||
submenuItems.push({
|
||||
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: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,10 +1394,13 @@ export class ActionListWidget<T> 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;
|
||||
}
|
||||
|
||||
@@ -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) │
|
||||
└───────────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌───────────▼────────────┐
|
||||
|
||||
@@ -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<IWorkspacePickerItem>(
|
||||
'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<string, { provider: typeof allProviders[0]; actions: { action: ISessionsBrowseAction; index: number }[] }>();
|
||||
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;
|
||||
@@ -341,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 {
|
||||
@@ -353,10 +396,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 +508,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) {
|
||||
|
||||
@@ -781,13 +781,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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
@@ -319,6 +319,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 {
|
||||
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
@@ -42,9 +42,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<boolean>('sessionItem.isPinned', false);
|
||||
export const IsSessionArchivedContext = new RawContextKey<boolean>('sessionItem.isArchived', false);
|
||||
export const IsSessionReadContext = new RawContextKey<boolean>('sessionItem.isRead', true);
|
||||
export const SessionSectionTypeContext = new RawContextKey<string>('sessionSection.type', '');
|
||||
|
||||
//#region Types
|
||||
|
||||
@@ -439,19 +441,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<SessionListItem, FuzzyScore, ISessionSectionTemplate> {
|
||||
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<SessionListItem, FuzzyScore>, _index: number, template: ISessionSectionTemplate): void {
|
||||
@@ -467,9 +486,16 @@ class SessionSectionRenderer implements ITreeRenderer<SessionListItem, FuzzyScor
|
||||
template.count.textContent = String(element.sessions.length);
|
||||
template.count.style.display = '';
|
||||
}
|
||||
|
||||
// Set context key for section type so toolbar actions can use when clauses
|
||||
const sectionType = element.id.startsWith('repo:') ? 'repository' : element.id;
|
||||
SessionSectionTypeContext.bindTo(template.contextKeyService).set(sectionType);
|
||||
template.toolbar.context = element;
|
||||
}
|
||||
|
||||
disposeTemplate(_template: ISessionSectionTemplate): void { }
|
||||
disposeTemplate(template: ISessionSectionTemplate): void {
|
||||
template.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -635,7 +661,7 @@ export class SessionsList extends Disposable implements ISessionsList {
|
||||
new SessionsTreeDelegate(approvalModel),
|
||||
[
|
||||
sessionRenderer,
|
||||
new SessionSectionRenderer(true /* hideSectionCount */),
|
||||
new SessionSectionRenderer(true /* hideSectionCount */, instantiationService, contextKeyService),
|
||||
showMoreRenderer,
|
||||
],
|
||||
{
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
|
||||
import { localize2 } from '../../../../../nls.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IViewsService } from '../../../../../workbench/services/views/common/viewsService.js';
|
||||
import { EditorsVisibleContext, IsAuxiliaryWindowContext } from '../../../../../workbench/common/contextkeys.js';
|
||||
import { AgentSessionSection, IAgentSessionSection, isAgentSessionSection } from '../../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js';
|
||||
import { ChatContextKeys } from '../../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';
|
||||
import { IChatWidgetService } from '../../../../../workbench/contrib/chat/browser/chat.js';
|
||||
import { AUX_WINDOW_GROUP } from '../../../../../workbench/services/editor/common/editorService.js';
|
||||
import { SessionsCategories } from '../../../../common/categories.js';
|
||||
import { SessionItemToolbarMenuId, SessionItemContextMenuId, IsSessionPinnedContext, IsSessionArchivedContext, IsSessionReadContext, SessionsGrouping, SessionsSorting } from './sessionsList.js';
|
||||
import { SessionItemToolbarMenuId, SessionItemContextMenuId, SessionSectionToolbarMenuId, SessionSectionTypeContext, IsSessionPinnedContext, IsSessionArchivedContext, IsSessionReadContext, SessionsGrouping, SessionsSorting, ISessionSection } from './sessionsList.js';
|
||||
import { ISessionsManagementService, IsNewChatSessionContext } from '../sessionsManagementService.js';
|
||||
import { ISessionData, SessionStatus } from '../../common/sessionData.js';
|
||||
import { IsRepositoryGroupCappedContext, SessionsViewFilterOptionsSubMenu, SessionsViewFilterSubMenu, SessionsViewGroupingContext, SessionsViewId, SessionsView, SessionsViewSortingContext } from './sessionsView.js';
|
||||
@@ -243,19 +243,19 @@ registerAction2(class FindSessionAction extends Action2 {
|
||||
registerAction2(class NewSessionForRepositoryAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'agentSessionSection.newSession',
|
||||
id: 'sessionsView.sectionNewSession',
|
||||
title: localize2('newSessionForRepo', "New Session"),
|
||||
icon: Codicon.newSession,
|
||||
menu: [{
|
||||
id: MenuId.AgentSessionSectionToolbar,
|
||||
id: SessionSectionToolbarMenuId,
|
||||
group: 'navigation',
|
||||
order: 0,
|
||||
when: ChatContextKeys.agentSessionSection.isEqualTo(AgentSessionSection.Repository),
|
||||
when: ContextKeyExpr.equals(SessionSectionTypeContext.key, 'repository'),
|
||||
}]
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor, context?: IAgentSessionSection): Promise<void> {
|
||||
if (!context || !isAgentSessionSection(context) || context.sessions.length === 0) {
|
||||
async run(accessor: ServicesAccessor, context?: ISessionSection): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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 {
|
||||
|
||||
@@ -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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Reference in New Issue
Block a user