From 77a1932b44dd04f80115b038d63cc22f8260c86d Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 16:08:16 +0100 Subject: [PATCH 1/7] Indicate when all sessions are filtered hidden (fix #296581) --- .../agentSessions/agentSessionsControl.ts | 50 +++++++++++++++++-- .../agentSessions/agentSessionsFilter.ts | 4 ++ .../agentSessions/agentSessionsViewer.ts | 20 +++++++- .../media/agentsessionsviewer.css | 17 +++++++ .../agentSessionsDataSource.test.ts | 34 +++++++------ 5 files changed, 102 insertions(+), 23 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 66977278aeb11e..48b3e13b909727 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -8,7 +8,8 @@ import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js'; import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; -import { $, append, EventHelper } from '../../../../../base/browser/dom.js'; +import { $, append, EventHelper, addDisposableListener, EventType, hide, setVisibility } from '../../../../../base/browser/dom.js'; +import { localize } from '../../../../../nls.js'; import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js'; @@ -71,6 +72,8 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo private sessionsContainer: HTMLElement | undefined; get element(): HTMLElement | undefined { return this.sessionsContainer; } + private emptyFilterMessage: HTMLElement | undefined; + private sessionsList: WorkbenchCompressibleAsyncDataTree | undefined; private sessionsListFindIsOpen = false; @@ -106,7 +109,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this.focusedAgentSessionTypeContextKey = ChatContextKeys.agentSessionType.bindTo(this.contextKeyService); this.hasMultipleAgentSessionsSelectedContextKey = ChatContextKeys.hasMultipleAgentSessionsSelected.bindTo(this.contextKeyService); - this.createList(this.container); + this.create(this.container); this.registerListeners(); } @@ -140,9 +143,27 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } } - private createList(container: HTMLElement): void { + private create(container: HTMLElement): void { this.sessionsContainer = append(container, $('.agent-sessions-viewer')); + this.createEmptyFilterMessage(this.sessionsContainer); + this.createList(this.sessionsContainer); + } + + private createEmptyFilterMessage(container: HTMLElement): void { + this.emptyFilterMessage = append(container, $('.agent-sessions-empty-filter-message')); + hide(this.emptyFilterMessage); + + const span = append(this.emptyFilterMessage, $('span')); + span.textContent = localize('agentSessions.noFilterResults', "No sessions match the current filter."); + + const link = append(this.emptyFilterMessage, $('a.reset-filter-link')); + link.textContent = localize('agentSessions.clearFilters', "Clear Filters"); + link.tabIndex = 0; + this._register(addDisposableListener(link, EventType.CLICK, () => this.options.filter.reset())); + } + + private createList(container: HTMLElement): void { const collapseByDefault = (element: unknown) => { if (isAgentSessionSection(element)) { if (element.section === AgentSessionSection.More && !this.options.filter.getExcludes().read) { @@ -168,16 +189,17 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo const sorter = new AgentSessionsSorter(this.options); const approvalModel = this.options.enableApprovalRow ? this._register(this.instantiationService.createInstance(AgentSessionApprovalModel)) : undefined; const sessionRenderer = this._register(this.instantiationService.createInstance(AgentSessionRenderer, this.options, approvalModel)); + const sessionFilter = this._register(new AgentSessionsDataSource(this.options.filter, sorter)); const list = this.sessionsList = this._register(this.instantiationService.createInstance(WorkbenchCompressibleAsyncDataTree, 'AgentSessionsView', - this.sessionsContainer, + container, new AgentSessionsListDelegate(approvalModel), new AgentSessionsCompressionDelegate(), [ sessionRenderer, this.instantiationService.createInstance(AgentSessionSectionRenderer), ], - new AgentSessionsDataSource(this.options.filter, sorter), + sessionFilter, { accessibilityProvider: new AgentSessionsAccessibilityProvider(), dnd: this.instantiationService.createInstance(AgentSessionsDragAndDrop), @@ -202,6 +224,10 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo } })); + this._register(sessionFilter.onDidGetChildren(count => { + this.updateEmptyFilterMessage(count); + })); + const model = this.agentSessionsService.model; this._register(this.options.filter.onDidChange(async () => { @@ -251,6 +277,20 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo })); } + private updateEmptyFilterMessage(visibleChildren: number): void { + if (!this.emptyFilterMessage || !this.sessionsList) { + return; + } + + const model = this.agentSessionsService.model; + const hasSessionsInModel = model.sessions.length > 0; + const hasVisibleChildren = visibleChildren > 0; + const isFilterActive = !this.options.filter.isDefault(); + + const showMessage = hasSessionsInModel && !hasVisibleChildren && isFilterActive; + setVisibility(showMessage, this.emptyFilterMessage); + } + private hasTodaySessions(): boolean { const startOfToday = new Date().setHours(0, 0, 0, 0); diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 65bfc186494c95..1848da132e9216 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -332,4 +332,8 @@ export class AgentSessionsFilter extends Disposable implements Required { +export class AgentSessionsDataSource extends Disposable implements IAsyncDataSource { private static readonly CAPPED_SESSIONS_LIMIT = 3; + private readonly _onDidGetChildren = this._register(new Emitter()); + readonly onDidGetChildren: Event = this._onDidGetChildren.event; + constructor( private readonly filter: IAgentSessionsFilter | undefined, private readonly sorter: ITreeSorter, - ) { } + ) { + super(); + } hasChildren(element: IAgentSessionsModel | AgentSessionListItem): boolean { @@ -739,6 +754,7 @@ export class AgentSessionsDataSource implements IAsyncDataSource { suite('AgentSessionsDataSource', () => { - ensureNoDisposablesAreLeakedInTestSuite(); + const disposables = ensureNoDisposablesAreLeakedInTestSuite(); const ONE_DAY = 24 * 60 * 60 * 1000; const WEEK_THRESHOLD = 7 * ONE_DAY; // 7 days @@ -152,7 +152,9 @@ suite('AgentSessionsDataSource', () => { onDidChange: Event.None, groupResults: () => options.groupBy, exclude: options.exclude ?? (() => false), - getExcludes: () => ({ providers: [], states: [], archived: false, read: options.excludeRead ?? false }) + getExcludes: () => ({ providers: [], states: [], archived: false, read: options.excludeRead ?? false }), + isDefault: () => true, + reset: () => { }, }; } @@ -182,7 +184,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: undefined }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -202,7 +204,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -223,7 +225,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -244,7 +246,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -263,7 +265,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -281,7 +283,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -299,7 +301,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -319,7 +321,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -353,7 +355,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -382,7 +384,7 @@ suite('AgentSessionsDataSource', () => { test('empty sessions returns empty result', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel([]); const result = Array.from(dataSource.getChildren(mockModel)); @@ -399,7 +401,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -422,7 +424,7 @@ suite('AgentSessionsDataSource', () => { const filter = createMockFilter({ groupBy: AgentSessionsGrouping.Date }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -456,7 +458,7 @@ suite('AgentSessionsDataSource', () => { excludeRead: true // Filtering to show only unread sessions }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); @@ -481,7 +483,7 @@ suite('AgentSessionsDataSource', () => { excludeRead: false // Not filtering to unread only }); const sorter = createMockSorter(); - const dataSource = new AgentSessionsDataSource(filter, sorter); + const dataSource = disposables.add(new AgentSessionsDataSource(filter, sorter)); const mockModel = createMockModel(sessions); const result = Array.from(dataSource.getChildren(mockModel)); From 98a23f963dc429877fe2e55d18842582369c84fc Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 16:27:52 +0100 Subject: [PATCH 2/7] . --- .../agentSessions/agentSessionsControl.ts | 16 +++++++++++++--- .../browser/agentSessions/agentSessionsFilter.ts | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 48b3e13b909727..4601f647ef2feb 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -9,6 +9,8 @@ import { IContextMenuService } from '../../../../../platform/contextview/browser import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { IOpenEvent, WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; import { $, append, EventHelper, addDisposableListener, EventType, hide, setVisibility } from '../../../../../base/browser/dom.js'; +import { StandardKeyboardEvent } from '../../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../../base/common/keyCodes.js'; import { localize } from '../../../../../nls.js'; import { AgentSessionSection, IAgentSession, IAgentSessionSection, IAgentSessionsModel, IMarshalledAgentSessionContext, isAgentSession, isAgentSessionSection } from './agentSessionsModel.js'; import { AgentSessionListItem, AgentSessionRenderer, AgentSessionsAccessibilityProvider, AgentSessionsCompressionDelegate, AgentSessionsDataSource, AgentSessionsDragAndDrop, AgentSessionsIdentityProvider, AgentSessionsKeyboardNavigationLabelProvider, AgentSessionsListDelegate, AgentSessionSectionRenderer, AgentSessionsSorter, IAgentSessionsFilter, IAgentSessionsSorterOptions } from './agentSessionsViewer.js'; @@ -155,12 +157,20 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo hide(this.emptyFilterMessage); const span = append(this.emptyFilterMessage, $('span')); - span.textContent = localize('agentSessions.noFilterResults', "No sessions match the current filter."); + span.textContent = localize('agentSessions.noFilterResults', "No matching sessions."); - const link = append(this.emptyFilterMessage, $('a.reset-filter-link')); - link.textContent = localize('agentSessions.clearFilters', "Clear Filters"); + const link = append(this.emptyFilterMessage, $('span.reset-filter-link')); + link.textContent = localize('agentSessions.clearFilters', "Clear Filter"); link.tabIndex = 0; + link.setAttribute('role', 'button'); this._register(addDisposableListener(link, EventType.CLICK, () => this.options.filter.reset())); + this._register(addDisposableListener(link, EventType.KEY_DOWN, (e) => { + const event = new StandardKeyboardEvent(e); + if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { + event.preventDefault(); + this.options.filter.reset(); + } + })); } private createList(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts index 1848da132e9216..c6cabc14a0d72c 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsFilter.ts @@ -287,7 +287,7 @@ export class AgentSessionsFilter extends Disposable implements Required Date: Tue, 3 Mar 2026 16:29:32 +0100 Subject: [PATCH 3/7] Update src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../browser/agentSessions/media/agentsessionsviewer.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 8c936cd234a8f0..3481a4a075dea9 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -360,6 +360,12 @@ &:hover { text-decoration: underline; } + + &:focus-visible { + text-decoration: underline; + outline: 1px solid var(--vscode-focusBorder); + outline-offset: 1px; + } } } } From 45bfade84277ffe6b3c8d1d1da43dbf24bfe6fe4 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 16:37:58 +0100 Subject: [PATCH 4/7] . --- .../browser/widgetHosts/viewPane/media/chatViewPane.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css index f57aca15300641..83799c15a7bc83 100644 --- a/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css +++ b/src/vs/workbench/contrib/chat/browser/widgetHosts/viewPane/media/chatViewPane.css @@ -130,6 +130,14 @@ padding: 0 6px 0 14px; } + /* Stacked: symmetric padding */ + &.sessions-control-orientation-stacked { + + .agent-sessions-empty-filter-message { + padding-left: 20px; + } + } + /* Right position: symmetric padding */ &.sessions-control-orientation-sidebyside.chat-view-position-right { From 91faf5e8e413c0a859b581dca1c6614f84624737 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 3 Mar 2026 16:47:03 +0100 Subject: [PATCH 5/7] Update src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../contrib/chat/browser/agentSessions/agentSessionsControl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts index 4601f647ef2feb..70fd5730de8966 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionsControl.ts @@ -167,7 +167,7 @@ export class AgentSessionsControl extends Disposable implements IAgentSessionsCo this._register(addDisposableListener(link, EventType.KEY_DOWN, (e) => { const event = new StandardKeyboardEvent(e); if (event.keyCode === KeyCode.Enter || event.keyCode === KeyCode.Space) { - event.preventDefault(); + EventHelper.stop(e, true); this.options.filter.reset(); } })); From a1b6189b64fa285e7dc0aa9c118ee712b6b25965 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:48:05 +0000 Subject: [PATCH 6/7] Initial plan From 2513e076320b7c1d61044ba4d2c084119d38c65d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:53:07 +0000 Subject: [PATCH 7/7] Fix layout: position empty filter message as absolute overlay Co-authored-by: bpasero <900690+bpasero@users.noreply.github.com> --- .../chat/browser/agentSessions/media/agentsessionsviewer.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css index 3481a4a075dea9..1d647a47f43310 100644 --- a/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css +++ b/src/vs/workbench/contrib/chat/browser/agentSessions/media/agentsessionsviewer.css @@ -5,6 +5,7 @@ .agent-sessions-viewer { + position: relative; flex: 1 1 auto; height: 100%; min-height: 0; @@ -347,6 +348,10 @@ } .agent-sessions-empty-filter-message { + position: absolute; + top: 0; + left: 0; + right: 0; padding: 8px 12px; font-size: 12px; color: var(--vscode-descriptionForeground);