From 3be5332ed4364173400fdad0fbc773bca3e5bb6b Mon Sep 17 00:00:00 2001 From: srikanthananthula Date: Wed, 19 Aug 2026 12:03:15 +0530 Subject: [PATCH 1/6] Fix section checkbox state when all items are unchecked (#331419) The migration group checkbox was only computed once at render time, so unchecking an individual item never updated it: the group checkbox stayed checked even after every child item was unchecked. Thread an onSelectionChange callback from each item's checkbox down through renderItem, so renderGroup can resync its own group checkbox whenever a child selection changes. Fixes #331330 --- .../aiCustomizationManagementEditor.ts | 12 ++-- .../aiCustomizationManagementEditor.test.ts | 71 +++++++++++++++++++ 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts index 510b77c94bd198..23b6d39d4c789d 100644 --- a/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts @@ -1357,7 +1357,7 @@ export class AICustomizationManagementEditor extends EditorPane { isWorkspaceFile, ); }; - const renderSelectionCheckbox = (row: HTMLElement, customization: IPromptPath): void => { + const renderSelectionCheckbox = (row: HTMLElement, customization: IPromptPath, onSelectionChange?: () => void): void => { const checkboxContainer = DOM.append(row, $('.item-sync-checkbox.prompt-migration-checkbox')); const checkboxTitle = localize('customizationMigrationSelectAriaLabel', "Select {0}", customization.name ?? basename(customization.uri)); const checkbox = this.migrationPageDisposables.add(new Checkbox(checkboxTitle, this.isCustomizationSelectedForMigration(customization), defaultCheckboxStyles)); @@ -1365,12 +1365,13 @@ export class AICustomizationManagementEditor extends EditorPane { this.migrationPageDisposables.add(checkbox.onChange(() => { this.setCustomizationSelectedForMigration(customization, checkbox.checked); this.updateCustomizationMigrationActionState(); + onSelectionChange?.(); })); }; - const renderItem = (container: HTMLElement, customization: IPromptPath): void => { + const renderItem = (container: HTMLElement, customization: IPromptPath, onSelectionChange?: () => void): void => { const row = DOM.append(container, $('div.ai-customization-list-item.prompt-migration-item')); - renderSelectionCheckbox(row, customization); + renderSelectionCheckbox(row, customization, onSelectionChange); const itemLeft = DOM.append(row, $('span.item-left')); const displayName = customization.name ?? basename(customization.uri); @@ -1421,6 +1422,9 @@ export class AICustomizationManagementEditor extends EditorPane { } this.renderCustomizationMigrationPage(); })); + const updateGroupCheckboxState = (): void => { + groupCheckbox.checked = customizations.every(customization => this.isCustomizationSelectedForMigration(customization)); + }; const groupToggle = DOM.append(groupHeader, $('button.prompt-migration-group-toggle')) as HTMLButtonElement; groupToggle.type = 'button'; const groupId = `prompt-migration-group-${category.id}-${groupKey}`; @@ -1455,7 +1459,7 @@ export class AICustomizationManagementEditor extends EditorPane { })); for (const customization of customizations) { - renderItem(groupItems, customization); + renderItem(groupItems, customization, updateGroupCheckboxState); } }; diff --git a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts index ebc8f3ae0b6e30..b6b8d2561a3e9d 100644 --- a/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/aiCustomization/aiCustomizationManagementEditor.test.ts @@ -510,4 +510,75 @@ suite('aiCustomizationManagementEditor', () => { editor.editorPreviewDisposables.dispose(); } }); + + test('unchecking every item in a migration group unchecks the group checkbox', () => { + const editor = createTestEditor(undefined, createConfigurationServiceStub({ + [ChatConfiguration.ChatCustomizationsPromptMigrationEnabled]: true, + })); + const promptFiles = [ + { + uri: URI.file('/workspace/.github/prompts/workspace-a.prompt.md'), + name: 'workspace-a.prompt.md', + storage: PromptsStorage.local, + type: PromptsType.prompt, + source: PromptFileSource.GitHubWorkspace, + } as IPromptPath, + { + uri: URI.file('/workspace/.github/prompts/workspace-b.prompt.md'), + name: 'workspace-b.prompt.md', + storage: PromptsStorage.local, + type: PromptsType.prompt, + source: PromptFileSource.GitHubWorkspace, + } as IPromptPath, + ]; + editor.customizationsByMigrationCategory = new Map([[CustomizationMigrationCategoryId.PromptFiles, promptFiles]]); + editor.activeMigrationCategoryId = CustomizationMigrationCategoryId.PromptFiles; + for (const promptFile of promptFiles) { + editor.setCustomizationSelectedForMigration(promptFile, true); + } + editor.migrationListContainer = document.createElement('div'); + editor.migrationTitleElement = document.createElement('h2'); + editor.migrationDescriptionElement = document.createElement('p'); + editor.migrationLinkElement = document.createElement('a'); + editor.migrationMigrateButton = { enabled: false, label: '' }; + document.body.appendChild(editor.migrationListContainer); + + try { + editor.renderCustomizationMigrationPage(); + + const groupCheckbox = editor.migrationListContainer.querySelector('.prompt-migration-group-checkbox [role="checkbox"]'); + const itemCheckboxes = [...editor.migrationListContainer.querySelectorAll('.prompt-migration-group-items .prompt-migration-checkbox [role="checkbox"]')]; + const readGroupChecked = () => groupCheckbox?.getAttribute('aria-checked'); + + const initiallyChecked = readGroupChecked(); + // Unchecking only one item already breaks "all selected", so the group checkbox should clear. + itemCheckboxes[0].click(); + const afterFirstUncheck = readGroupChecked(); + // Unchecking the last remaining item must keep the group checkbox cleared (issue #331330). + itemCheckboxes[1].click(); + const afterLastUncheck = readGroupChecked(); + // Re-checking every item should re-select the group checkbox. + itemCheckboxes[0].click(); + itemCheckboxes[1].click(); + const afterRecheckingAll = readGroupChecked(); + + assert.deepStrictEqual({ + itemCount: itemCheckboxes.length, + initiallyChecked, + afterFirstUncheck, + afterLastUncheck, + afterRecheckingAll, + }, { + itemCount: 2, + initiallyChecked: 'true', + afterFirstUncheck: 'false', + afterLastUncheck: 'false', + afterRecheckingAll: 'true', + }); + } finally { + editor.migrationListContainer.remove(); + editor.migrationPageDisposables.dispose(); + editor.editorPreviewDisposables.dispose(); + } + }); }); From b29a9b36a048c48107af0341ad6b361973870f50 Mon Sep 17 00:00:00 2001 From: Danyal Ahmed <58849388+danyalahmed1995@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:54:03 +0500 Subject: [PATCH 2/6] Fix case-insensitive aggregated basename glob matching (#316387) * Fix case-insensitive aggregated basename glob matching * Added the requested review test * Preserve expression glob casing with cached patterns * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add reverse cache collision regression test --------- Co-authored-by: Dmitriy Vasyura Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/vs/base/common/glob.ts | 14 +++++------ src/vs/base/test/common/glob.test.ts | 36 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/vs/base/common/glob.ts b/src/vs/base/common/glob.ts index 8b750c0924f979..78255e02110251 100644 --- a/src/vs/base/common/glob.ts +++ b/src/vs/base/common/glob.ts @@ -334,7 +334,7 @@ export function isEmptyPattern(pattern: ParsedPattern | ParsedExpression): patte return false; } -function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): ParsedStringPattern { +function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions, cacheKey?: string): ParsedStringPattern { if (!arg1) { return NULL; } @@ -359,7 +359,7 @@ function parsePattern(arg1: string | IRelativePattern, options: IGlobOptions): P }; // Check cache - const patternKey = `${ignoreCase ? pattern.toLowerCase() : pattern}_${!!options.trimForExclusions}_${ignoreCase}`; + const patternKey = `${cacheKey === undefined ? `default:${ignoreCase ? pattern.toLowerCase() : pattern}` : `custom:${cacheKey}`}_${!!options.trimForExclusions}_${ignoreCase}`; let parsedPattern = CACHE.get(patternKey); if (parsedPattern) { return wrapRelativePattern(parsedPattern, arg1, internalOptions); @@ -462,7 +462,7 @@ function trivia3(pattern: string, options: IGlobOptionsInternal): ParsedStringPa const parsedPatterns = aggregateBasenameMatches(pattern.slice(1, -1) .split(',') .map(pattern => parsePattern(pattern, options)) - .filter(pattern => pattern !== NULL), pattern); + .filter(pattern => pattern !== NULL), pattern, options.ignoreCase); const patternsLength = parsedPatterns.length; if (!patternsLength) { @@ -617,7 +617,7 @@ export function getPathTerms(patternOrExpression: ParsedPattern | ParsedExpressi function parsedExpression(expression: IExpression, options: IGlobOptions): ParsedExpression { const parsedPatterns = aggregateBasenameMatches(Object.getOwnPropertyNames(expression) .map(pattern => parseExpressionPattern(pattern, expression[pattern], options)) - .filter(pattern => pattern !== NULL)); + .filter(pattern => pattern !== NULL), undefined, options.ignoreCase); const patternsLength = parsedPatterns.length; if (!patternsLength) { @@ -750,7 +750,7 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, return NULL; // pattern is disabled } - const parsedPattern = parsePattern(pattern, options); + const parsedPattern = parsePattern(pattern, options, pattern); if (parsedPattern === NULL) { return NULL; } @@ -786,7 +786,7 @@ function parseExpressionPattern(pattern: string, value: boolean | SiblingClause, return parsedPattern; } -function aggregateBasenameMatches(parsedPatterns: Array, result?: string): Array { +function aggregateBasenameMatches(parsedPatterns: Array, result?: string, ignoreCase?: boolean): Array { const basenamePatterns = parsedPatterns.filter(parsedPattern => !!(parsedPattern).basenames); if (basenamePatterns.length < 2) { return parsedPatterns; @@ -830,7 +830,7 @@ function aggregateBasenameMatches(parsedPatterns: Array equalsIgnoreCase(candidate, basename)) : basenames.indexOf(basename); return index !== -1 ? patterns[index] : null; }; diff --git a/src/vs/base/test/common/glob.test.ts b/src/vs/base/test/common/glob.test.ts index 3021b924988f05..7f0bd234845fb8 100644 --- a/src/vs/base/test/common/glob.test.ts +++ b/src/vs/base/test/common/glob.test.ts @@ -779,6 +779,40 @@ suite('Glob', () => { assert.strictEqual(glob.match(expr, 'foo/foo'), null); }); + test('expression with two basename globs ignores case', function () { + const expr = { + '**/BAR': true, + '**/BAZ': true + }; + + assert.strictEqual(glob.match(expr, 'bar', { ignoreCase: true }), '**/BAR'); + assert.strictEqual(glob.match(expr, 'baz', { ignoreCase: true }), '**/BAZ'); + assert.strictEqual(glob.match(expr, 'src/bar', { ignoreCase: true }), '**/BAR'); + assert.strictEqual(glob.match(expr, 'bar'), null); + }); + + test('expression with cached basename globs ignores case', function () { + glob.parse('**/bar', { ignoreCase: true }); + + const expr = { + '**/BAR': true, + '**/BAZ': true + }; + + assert.strictEqual(glob.match(expr, 'BaR', { ignoreCase: true }), '**/BAR'); + }); + + test('expression cache does not collide with string pattern cache', function () { + glob.parse('**/BAR', { ignoreCase: true }); + + const expr = { + '**/bar': true, + '**/baz': true + }; + + assert.strictEqual(glob.match(expr, 'bar', { ignoreCase: true }), '**/bar'); + }); + test('expression with two basename globs and a siblings expression', function () { const expr = { '**/bar': true, @@ -1185,6 +1219,8 @@ suite('Glob', () => { assertNoGlobMatch('{**/*.JS,**/*.TS}', 'bar/foo.js'); assertGlobMatch('{**/*.JS,**/*.TS}', 'bar/foo.ts', true); assertGlobMatch('{**/*.JS,**/*.TS}', 'bar/foo.js', true); + assertNoGlobMatch('{**/BAR,**/BAZ}', 'bar'); + assertGlobMatch('{**/BAR,**/BAZ}', 'bar', true); // T4 assertNoGlobMatch('**/FOO/Bar', 'bar/foo/bar'); assertGlobMatch('**/FOO/Bar', 'bar/foo/bar', true); From d11fd428e3dea9d19083fd8f0b02e61aae5c4fde Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Tue, 18 Aug 2026 23:55:24 -0700 Subject: [PATCH 3/6] Fix double-click behavior in editor window sessions list (#331516) * Fix new chat from session list double-click Route empty-space double-clicks through the Chat view's action context so an active chat editor is not cleared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix new chat from welcome sessions Provide the Welcome editor session context when empty-space double-click creates a new chat. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../agentSessions/agentSessionsControl.ts | 6 +- .../widgetHosts/viewPane/chatViewPane.ts | 4 +- .../agentSessionsControl.test.ts | 71 +++++++++++++++++++ .../browser/agentSessionsWelcome.ts | 7 ++ 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsControl.test.ts diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 39cd98139ffd90..92015440707ceb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -19,8 +19,6 @@ import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; import { FuzzyScore } from '../../../../../base/common/filters.js'; import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IChatSessionsService } from '../../common/chatSessionsService.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { ACTION_ID_NEW_CHAT } from '../actions/chatActions.js'; import { Emitter, Event } from '../../../../../base/common/event.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; import { Throttler } from '../../../../../base/common/async.js'; @@ -60,6 +58,7 @@ export interface IAgentSessionsControlOptions { readonly itemHeight?: number; readonly sectionHeight?: number; + createNewChat(): void; getHoverPosition(): HoverPosition; trackActiveEditorSession(): boolean; collapseOlderSections?(): boolean; @@ -116,7 +115,6 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IChatSessionsService private readonly chatSessionsService: IChatSessionsService, - @ICommandService private readonly commandService: ICommandService, @IMenuService private readonly menuService: IMenuService, @IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService, @ITelemetryService private readonly telemetryService: ITelemetryService, @@ -566,7 +564,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this._register(list.onMouseDblClick(({ element }) => { if (element === null) { - this.commandService.executeCommand(ACTION_ID_NEW_CHAT); + this.options.createNewChat(); } })); diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts index 20bf2261de97aa..c1ee439b3e1ade 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/chatViewPane.ts @@ -916,7 +916,8 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const newSessionButtonContainer = this.sessionsNewButtonContainer = append(sessionsContainer, $('.agent-sessions-new-button-container')); const newSessionButton = this._register(new Button(newSessionButtonContainer, { ...defaultButtonStyles, secondary: true })); newSessionButton.label = localize('newSession', "New Session"); - this._register(newSessionButton.onDidClick(() => this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.getActionsContext()))); + const createNewChat = () => this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.getActionsContext()); + this._register(newSessionButton.onDidClick(createNewChat)); // Sessions Control this.sessionsControlContainer = append(sessionsContainer, $('.agent-sessions-control-container')); @@ -924,6 +925,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { source: 'chatViewPane', filter: sessionsFilter, overrideStyles: this.getLocationBasedColors().listOverrideStyles, + createNewChat, getHoverPosition: () => this.getSessionHoverPosition(), trackActiveEditorSession: () => { return !this._widget || this._widget.isEmpty(); // only track and reveal if chat widget is empty diff --git a/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsControl.test.ts b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsControl.test.ts new file mode 100644 index 00000000000000..e15a07bc51ec5d --- /dev/null +++ b/src/vs/workbench/contrib/chat/test/browser/agentSessions/agentSessionsControl.test.ts @@ -0,0 +1,71 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { EventType } from '../../../../../../base/browser/dom.js'; +import { Event } from '../../../../../../base/common/event.js'; +import { mock } from '../../../../../../base/test/common/mock.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js'; +import { AgentSessionsControl } from '../../../browser/agentSessions/agentSessionsControl.js'; +import { IAgentSessionsModel } from '../../../browser/agentSessions/agentSessionsModel.js'; +import { IAgentSessionsService } from '../../../browser/agentSessions/agentSessionsService.js'; +import { IAgentSessionsFilter } from '../../../browser/agentSessions/agentSessionsViewer.js'; +import { IChatSessionsService } from '../../../common/chatSessionsService.js'; +import { IVoicePlaybackService } from '../../../common/voicePlaybackService.js'; +import { workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js'; + +suite('AgentSessionsControl', () => { + + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + test('creates a new chat when double-clicking empty list space', () => { + const model: IAgentSessionsModel = { + sessions: [], + resolved: true, + getSession: () => undefined, + observeSession: () => { throw new Error('Not implemented'); }, + onWillResolve: Event.None, + onDidResolve: Event.None, + onDidChangeSessions: Event.None, + onDidChangeSessionArchivedState: Event.None, + resolve: async () => { }, + }; + const filter: IAgentSessionsFilter = { + onDidChange: Event.None, + exclude: () => false, + getExcludes: () => ({ providers: [], states: [], archived: false, read: false, repositoryGroupCapped: true }), + isDefault: () => true, + reset: () => { }, + }; + const instantiationService = workbenchInstantiationService(undefined, store); + instantiationService.stub(IChatSessionsService, new class extends mock() { }); + instantiationService.stub(IVoicePlaybackService, new class extends mock() { }); + instantiationService.stub(IAgentSessionsService, new class extends mock() { + override readonly model = model; + override readonly onDidChangeSessionArchivedState = Event.None; + override getSession = () => undefined; + }); + + const container = document.createElement('div'); + let newChatCount = 0; + const control = store.add(instantiationService.createInstance(AgentSessionsControl, container, { + overrideStyles: {}, + filter, + source: 'test', + createNewChat: () => newChatCount++, + getHoverPosition: () => HoverPosition.BELOW, + trackActiveEditorSession: () => false, + })); + + control.element?.querySelector('.monaco-list')?.dispatchEvent(new MouseEvent(EventType.DBLCLICK, { + bubbles: true, + button: 0, + detail: 2, + })); + + assert.strictEqual(newChatCount, 1); + }); +}); diff --git a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts index ac3207098565ea..c54d5111764e0d 100644 --- a/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts +++ b/src/vs/workbench/contrib/welcomeAgentSessions/browser/agentSessionsWelcome.ts @@ -11,6 +11,7 @@ import { Toggle } from '../../../../base/browser/ui/toggle/toggle.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { DisposableStore, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; +import { MarshalledId } from '../../../../base/common/marshallingIds.js'; import { Emitter } from '../../../../base/common/event.js'; import { ScrollbarVisibility } from '../../../../base/common/scrollable.js'; import { basename } from '../../../../base/common/resources.js'; @@ -35,7 +36,9 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js'; import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../chat/common/constants.js'; import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; +import { IChatViewTitleActionContext } from '../../chat/common/actions/chatActions.js'; import { ChatWidget } from '../../chat/browser/widget/chatWidget.js'; +import { ACTION_ID_NEW_CHAT } from '../../chat/browser/actions/chatActions.js'; import { IAgentSessionsService } from '../../chat/browser/agentSessions/agentSessionsService.js'; import { AgentSessionProviders, AgentSessionTarget } from '../../chat/browser/agentSessions/agentSessions.js'; import { IAgentSession } from '../../chat/browser/agentSessions/agentSessionsModel.js'; @@ -563,6 +566,10 @@ export class AgentSessionsWelcomePage extends EditorPane { limitResults: () => MAX_SESSIONS, overrideExclude: (session) => session.isArchived() ? true : undefined, })), + createNewChat: () => this.commandService.executeCommand(ACTION_ID_NEW_CHAT, this.chatWidget?.viewModel ? { + $mid: MarshalledId.ChatViewContext, + sessionResource: this.chatWidget.viewModel.sessionResource, + } satisfies IChatViewTitleActionContext : undefined), getHoverPosition: () => HoverPosition.BELOW, trackActiveEditorSession: () => false, source: 'welcomeView', From 474a349ad5b745e512ef86b864d1c74f7264dd7a Mon Sep 17 00:00:00 2001 From: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:33:33 +0200 Subject: [PATCH 4/6] Agents - move section chevron to the left and reveal it on hover (#331545) --- src/vs/sessions/SESSIONS_LIST.md | 2 +- .../sessions/browser/media/sessionsList.css | 20 ++++++++-- .../sessions/browser/views/sessionsList.ts | 40 ++++++++++++++----- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/vs/sessions/SESSIONS_LIST.md b/src/vs/sessions/SESSIONS_LIST.md index 76bf6c9a3ebd7f..4ffaf85d97226d 100644 --- a/src/vs/sessions/SESSIONS_LIST.md +++ b/src/vs/sessions/SESSIONS_LIST.md @@ -195,7 +195,7 @@ The Open Pull Request action shared by the session context menu and header uses | Menu | Constant | Where it appears | Use for | |------|----------|------------------|---------| -| `SessionSectionToolbar` | `SessionSectionToolbarMenuId` | Toolbar on section headers (Pinned, workspace groups, Done) | Section-scoped actions like the workspace `DropdownWithPrimaryActionViewItem` whose fixed primary action is "New Session" and whose dropdown contains actions contributed to `Menus.SessionSectionNewSession`, including the GitHub-backed "Create Session from Pull Request" action. When that menu is empty, the toolbar renders the ordinary "New Session" action. The toolbar also contains the selected "Archive All"/"Mark All as Done" action. The Done section restores/unarchives sessions individually (or via multi-selection) rather than with a section-wide action. Section headers also show a collapsible chevron on hover/focus; while a section action dropdown is open, both the toolbar and chevron remain visible. The chevron uses the same ghost icon hover background token as toolbar icon buttons. | +| `SessionSectionToolbar` | `SessionSectionToolbarMenuId` | Toolbar on section headers (Pinned, workspace groups, Done) | Section-scoped actions like the workspace `DropdownWithPrimaryActionViewItem` whose fixed primary action is "New Session" and whose dropdown contains actions contributed to `Menus.SessionSectionNewSession`, including the GitHub-backed "Create Session from Pull Request" action. When that menu is empty, the toolbar renders the ordinary "New Session" action. The toolbar also contains the selected "Archive All"/"Mark All as Done" action. The Done section restores/unarchives sessions individually (or via multi-selection) rather than with a section-wide action. Section headers also show a collapsible chevron on hover or keyboard focus; while a section action dropdown is open, both the toolbar and chevron remain visible. The chevron uses the same ghost icon hover background token as toolbar icon buttons. | ### Group Header Menu diff --git a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css index 9dfb5575735ff6..a276f88703beab 100644 --- a/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css +++ b/src/vs/sessions/contrib/sessions/browser/media/sessionsList.css @@ -67,6 +67,10 @@ right: 0; top: 0; } + + .actions-container { + gap: var(--vscode-spacing-size40); + } } .monaco-list-row .session-pending-voice-indicator { @@ -522,6 +526,10 @@ display: none; } + .session-section-toolbar .actions-container { + gap: var(--vscode-spacing-size40); + } + .session-section-toolbar .monaco-action-bar .action-label { border-radius: var(--vscode-cornerRadius-small); } @@ -531,8 +539,8 @@ display: none; color: var(--vscode-descriptionForeground); font-size: var(--vscode-codiconFontSize-compact, 12px); - width: 22px; height: 22px; + margin-right: 6px; border-radius: var(--vscode-cornerRadius-small); justify-content: center; } @@ -563,13 +571,19 @@ } .monaco-list-row:hover .session-section .session-section-chevron.collapsible, -.monaco-list-row.focused .session-section .session-section-chevron.collapsible, +.sessions-list-control:not(.session-section-focus-from-pointer) .monaco-list:focus-within .monaco-list-row.focused .session-section .session-section-chevron.collapsible, .monaco-list-row .session-section.dropdown-active .session-section-chevron.collapsible { display: flex; align-items: center; } -.monaco-list-row .session-section .session-section-chevron.collapsible:hover { +.monaco-list-row:hover .session-section .session-section-chevron.collapsible + .session-section-icon, +.sessions-list-control:not(.session-section-focus-from-pointer) .monaco-list:focus-within .monaco-list-row.focused .session-section .session-section-chevron.collapsible + .session-section-icon, +.monaco-list-row .session-section.dropdown-active .session-section-chevron.collapsible + .session-section-icon { + display: none; +} + +.session-section.dropdown-active .session-section-toolbar .monaco-dropdown-with-primary { background-color: var(--vscode-toolbar-hoverBackground); } diff --git a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts index 8ee3001aa5760c..23ec2e66308ee2 100644 --- a/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts +++ b/src/vs/sessions/contrib/sessions/browser/views/sessionsList.ts @@ -181,6 +181,27 @@ function isSessionSection(item: SessionListItem): item is ISessionSection { return !isSessionGroupItem(item) && 'sessions' in item && Array.isArray((item as ISessionSection).sessions); } +function getSessionSectionIcon(sectionId: string): ThemeIcon | undefined { + switch (sectionId) { + case QUICK_CHATS_SECTION_ID: + return Codicon.commentDiscussion; + case 'pinned': + return Codicon.pinned; + case AUTOMATIONS_SECTION_ID: + return Codicon.watch; + case 'archived': + return Codicon.archive; + case 'recent': + return Codicon.history; + case 'older': + return Codicon.calendar; + default: + return sectionId.startsWith('workspace:') + ? Codicon.folder + : undefined; + } +} + function isSessionShowMore(item: SessionListItem): item is ISessionShowMore { return 'showMore' in item && (item as ISessionShowMore).showMore === true; } @@ -970,6 +991,8 @@ export class SessionSectionRenderer implements ITreeRenderer Date: Wed, 19 Aug 2026 10:44:00 +0100 Subject: [PATCH 5/6] Modern UI: Adjust padding for status bar items (#331496) style(statusBar): adjust padding for first and last visible items for symmetry Co-authored-by: mrleemurray --- .../contrib/modernUI/browser/media/statusBar.css | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/modernUI/browser/media/statusBar.css b/src/vs/workbench/contrib/modernUI/browser/media/statusBar.css index 9784fab6002731..11acf295d7d9af 100644 --- a/src/vs/workbench/contrib/modernUI/browser/media/statusBar.css +++ b/src/vs/workbench/contrib/modernUI/browser/media/statusBar.css @@ -60,10 +60,12 @@ background-image: linear-gradient(var(--vscode-statusBarItem-compactHoverBackground), var(--vscode-statusBarItem-compactHoverBackground)); } +/* + * The items at either end of the status bar attach to the corner in the base + * styles, which pads them asymmetrically. As pills they need even padding on + * both sides, so give both end items the same symmetric inset. + */ +.modern-ui .part.statusbar > .items-container > .statusbar-item.left.first-visible-item > .statusbar-item-label, .modern-ui .part.statusbar > .items-container > .statusbar-item.right.last-visible-item > .statusbar-item-label { padding: 0 var(--vscode-spacing-size60); } - -.modern-ui .part.statusbar > .items-container > .statusbar-item.left.first-visible-item > .statusbar-item-label { - padding-right: var(--vscode-spacing-size40); -} From 0dac2a8dfdad732e464a588028ac89cdb55cb988 Mon Sep 17 00:00:00 2001 From: "vs-code-engineering[bot]" <122617954+vs-code-engineering[bot]@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:03:42 +0000 Subject: [PATCH 6/6] [cherry-pick] Fix New Session Prompt Description Color (#331358) Co-authored-by: vs-code-engineering[bot] --- .../contrib/chat/browser/media/newSessionPromptOptions.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/sessions/contrib/chat/browser/media/newSessionPromptOptions.css b/src/vs/sessions/contrib/chat/browser/media/newSessionPromptOptions.css index 9b6f8ea08b45a0..a8c8d007fc1ec5 100644 --- a/src/vs/sessions/contrib/chat/browser/media/newSessionPromptOptions.css +++ b/src/vs/sessions/contrib/chat/browser/media/newSessionPromptOptions.css @@ -101,7 +101,6 @@ } .new-session-prompt-option-title { - color: var(--vscode-descriptionForeground); display: flex; font-size: var(--vscode-agents-fontSize-body1); font-weight: var(--vscode-agents-fontWeight-semiBold); @@ -147,7 +146,7 @@ } .new-session-prompt-option-description { - color: var(--vscode-agentsChatInput-placeholderForeground, var(--vscode-descriptionForeground)); + color: var(--vscode-descriptionForeground); font-size: var(--vscode-agents-fontSize-label1); font-weight: var(--vscode-agents-fontWeight-regular); grid-column: 1 / -1;