Sync all agent sessions filters (#291887)

This commit is contained in:
Osvaldo Ortega 2026-02-03 18:17:47 +01:00 committed by GitHub
parent 1adb77d0f4
commit 69e53bd021
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 68 additions and 66 deletions

View File

@ -22,7 +22,7 @@ export enum AgentSessionsGrouping {
export interface IAgentSessionsFilterOptions extends Partial<IAgentSessionsFilter> {
readonly filterMenuId: MenuId;
readonly filterMenuId?: MenuId;
readonly limitResults?: () => number | undefined;
notifyResults?(count: number): void;
@ -41,7 +41,7 @@ const DEFAULT_EXCLUDES: IAgentSessionsFilterExcludes = Object.freeze({
export class AgentSessionsFilter extends Disposable implements Required<IAgentSessionsFilter> {
private readonly STORAGE_KEY: string;
private readonly STORAGE_KEY = `agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu`;
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
@ -61,8 +61,6 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
) {
super();
this.STORAGE_KEY = `agentSessions.filterExcludes.${this.options.filterMenuId.id.toLowerCase()}`;
this.updateExcludes(false);
this.registerListeners();
@ -116,14 +114,19 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
private updateFilterActions(): void {
this.actionDisposables.clear();
this.registerProviderActions(this.actionDisposables);
this.registerStateActions(this.actionDisposables);
this.registerArchivedActions(this.actionDisposables);
this.registerReadActions(this.actionDisposables);
this.registerResetAction(this.actionDisposables);
const menuId = this.options.filterMenuId;
if (!menuId) {
return;
}
this.registerProviderActions(this.actionDisposables, menuId);
this.registerStateActions(this.actionDisposables, menuId);
this.registerArchivedActions(this.actionDisposables, menuId);
this.registerReadActions(this.actionDisposables, menuId);
this.registerResetAction(this.actionDisposables, menuId);
}
private registerProviderActions(disposables: DisposableStore): void {
private registerProviderActions(disposables: DisposableStore, menuId: MenuId): void {
const providers: { id: string; label: string }[] = Object.values(AgentSessionProviders).map(provider => ({
id: provider,
label: getAgentSessionProviderName(provider)
@ -143,10 +146,10 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: `agentSessions.filter.toggleExclude:${provider.id}.${that.options.filterMenuId.id.toLowerCase()}`,
id: `agentSessions.filter.toggleExclude:${provider.id}.${menuId.id.toLowerCase()}`,
title: provider.label,
menu: {
id: that.options.filterMenuId,
id: menuId,
group: '1_providers',
order: counter++,
},
@ -165,7 +168,7 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
}
}
private registerStateActions(disposables: DisposableStore): void {
private registerStateActions(disposables: DisposableStore, menuId: MenuId): void {
const states: { id: AgentSessionStatus; label: string }[] = [
{ id: AgentSessionStatus.Completed, label: localize('agentSessionStatus.completed', "Completed") },
{ id: AgentSessionStatus.InProgress, label: localize('agentSessionStatus.inProgress', "In Progress") },
@ -179,10 +182,10 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: `agentSessions.filter.toggleExcludeState:${state.id}.${that.options.filterMenuId.id.toLowerCase()}`,
id: `agentSessions.filter.toggleExcludeState:${state.id}.${menuId.id.toLowerCase()}`,
title: state.label,
menu: {
id: that.options.filterMenuId,
id: menuId,
group: '2_states',
order: counter++,
},
@ -201,15 +204,15 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
}
}
private registerArchivedActions(disposables: DisposableStore): void {
private registerArchivedActions(disposables: DisposableStore, menuId: MenuId): void {
const that = this;
disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: `agentSessions.filter.toggleExcludeArchived.${that.options.filterMenuId.id.toLowerCase()}`,
id: `agentSessions.filter.toggleExcludeArchived.${menuId.id.toLowerCase()}`,
title: localize('agentSessions.filter.archived', 'Archived'),
menu: {
id: that.options.filterMenuId,
id: menuId,
group: '3_props',
order: 1000,
},
@ -222,15 +225,15 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
}));
}
private registerReadActions(disposables: DisposableStore): void {
private registerReadActions(disposables: DisposableStore, menuId: MenuId): void {
const that = this;
disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: `agentSessions.filter.toggleExcludeRead.${that.options.filterMenuId.id.toLowerCase()}`,
id: `agentSessions.filter.toggleExcludeRead.${menuId.id.toLowerCase()}`,
title: localize('agentSessions.filter.read', 'Read'),
menu: {
id: that.options.filterMenuId,
id: menuId,
group: '3_props',
order: 0,
},
@ -243,15 +246,15 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
}));
}
private registerResetAction(disposables: DisposableStore): void {
private registerResetAction(disposables: DisposableStore, menuId: MenuId): void {
const that = this;
disposables.add(registerAction2(class extends Action2 {
constructor() {
super({
id: `agentSessions.filter.resetExcludes.${that.options.filterMenuId.id.toLowerCase()}`,
id: `agentSessions.filter.resetExcludes.${menuId.id.toLowerCase()}`,
title: localize('agentSessions.filter.reset', "Reset"),
menu: {
id: that.options.filterMenuId,
id: menuId,
group: '4_reset',
order: 0,
},

View File

@ -16,6 +16,7 @@ import { IAgentSession, isLocalAgentSessionItem } from './agentSessionsModel.js'
import { IAgentSessionsService } from './agentSessionsService.js';
import { AgentSessionsSorter, groupAgentSessionsByDate, sessionDateFromNow } from './agentSessionsViewer.js';
import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID, getAgentSessionTime } from './agentSessions.js';
import { AgentSessionsFilter } from './agentSessionsFilter.js';
interface ISessionPickItem extends IQuickPickItem {
readonly session: IAgentSession;
@ -75,8 +76,9 @@ export class AgentSessionsPicker {
async pickAgentSession(): Promise<void> {
const disposables = new DisposableStore();
const picker = disposables.add(this.quickInputService.createQuickPick<ISessionPickItem>({ useSeparators: true }));
const filter = disposables.add(this.instantiationService.createInstance(AgentSessionsFilter, {}));
picker.items = this.createPickerItems();
picker.items = this.createPickerItems(filter);
picker.canAcceptInBackground = true;
picker.placeholder = localize('chatAgentPickerPlaceholder', "Search agent sessions by name");
@ -116,7 +118,7 @@ export class AgentSessionsPicker {
await this.agentSessionsService.model.resolve(session.providerType);
this.pickAgentSession();
} else {
picker.items = this.createPickerItems();
picker.items = this.createPickerItems(filter);
}
}));
@ -124,8 +126,10 @@ export class AgentSessionsPicker {
picker.show();
}
private createPickerItems(): (ISessionPickItem | IQuickPickSeparator)[] {
const sessions = this.agentSessionsService.model.sessions.sort(this.sorter.compare.bind(this.sorter));
private createPickerItems(filter: AgentSessionsFilter): (ISessionPickItem | IQuickPickSeparator)[] {
const sessions = this.agentSessionsService.model.sessions
.filter(session => !filter.exclude(session))
.sort(this.sorter.compare.bind(this.sorter));
const items: (ISessionPickItem | IQuickPickSeparator)[] = [];
const groupedSessions = groupAgentSessionsByDate(sessions);

View File

@ -16,12 +16,14 @@ import { openSession } from './agentSessionsOpener.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { AGENT_SESSION_DELETE_ACTION_ID, AGENT_SESSION_RENAME_ACTION_ID } from './agentSessions.js';
import { archiveButton, deleteButton, getSessionButtons, getSessionDescription, renameButton, unarchiveButton } from './agentSessionsPicker.js';
import { AgentSessionsFilter } from './agentSessionsFilter.js';
export const AGENT_SESSIONS_QUICK_ACCESS_PREFIX = 'agent ';
export class AgentSessionsQuickAccessProvider extends PickerQuickAccessProvider<IPickerQuickAccessItem> {
private readonly sorter = new AgentSessionsSorter();
private readonly filter: AgentSessionsFilter;
constructor(
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
@ -34,12 +36,16 @@ export class AgentSessionsQuickAccessProvider extends PickerQuickAccessProvider<
label: localize('noAgentSessionResults', "No matching agent sessions")
}
});
this.filter = this._register(this.instantiationService.createInstance(AgentSessionsFilter, {}));
}
protected async _getPicks(filter: string): Promise<(IQuickPickSeparator | IPickerQuickAccessItem)[]> {
const picks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = [];
const sessions = this.agentSessionsService.model.sessions.sort(this.sorter.compare.bind(this.sorter));
const sessions = this.agentSessionsService.model.sessions
.filter(session => !this.filter.exclude(session))
.sort(this.sorter.compare.bind(this.sorter));
const groupedSessions = groupAgentSessionsByDate(sessions);
for (const group of groupedSessions.values()) {

View File

@ -750,6 +750,7 @@ suite('AgentSessions', () => {
suite('AgentSessionsFilter', () => {
const disposables = new DisposableStore();
const storageKey = 'agentSessions.filterExcludes.agentsessionsviewerfiltersubmenu';
let mockChatSessionsService: MockChatSessionsService;
let instantiationService: TestInstantiationService;
@ -788,7 +789,7 @@ suite('AgentSessions', () => {
{ filterMenuId: MenuId.ViewTitle }
));
// Default: archived sessions should NOT be excluded (archived: false by default)
// Default: archived sessions should NOT be excluded unless grouped by capped
const archivedSession = createSession({
isArchived: () => true
});
@ -827,7 +828,7 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// After excluding type-1, session1 should be filtered but not session2
assert.strictEqual(filter.exclude(session1), true);
@ -851,14 +852,14 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
assert.strictEqual(filter.exclude(session1), true);
assert.strictEqual(filter.exclude(session2), true);
assert.strictEqual(filter.exclude(session3), false);
});
test('should filter not out archived sessions', () => {
test('should not exclude archived sessions when not capped', () => {
const storageService = instantiationService.get(IStorageService);
const filter = disposables.add(instantiationService.createInstance(
AgentSessionsFilter,
@ -875,7 +876,7 @@ suite('AgentSessions', () => {
isArchived: () => false
});
// By default, archived sessions should NOT be filtered (archived: false in default excludes)
// By default, archived sessions should NOT be filtered when not capped
assert.strictEqual(filter.exclude(archivedSession), false);
assert.strictEqual(filter.exclude(activeSession), false);
@ -885,9 +886,9 @@ suite('AgentSessions', () => {
states: [],
archived: true
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// After excluding archived, only archived session should be filtered
// Archived exclusion only applies when grouped by capped
assert.strictEqual(filter.exclude(archivedSession), false);
assert.strictEqual(filter.exclude(activeSession), false);
});
@ -925,7 +926,7 @@ suite('AgentSessions', () => {
states: [ChatSessionStatus.Failed],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// After excluding failed status, only failedSession should be filtered
assert.strictEqual(filter.exclude(failedSession), true);
@ -950,7 +951,7 @@ suite('AgentSessions', () => {
states: [ChatSessionStatus.Failed, ChatSessionStatus.InProgress],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
assert.strictEqual(filter.exclude(failedSession), true);
assert.strictEqual(filter.exclude(completedSession), false);
@ -982,7 +983,7 @@ suite('AgentSessions', () => {
states: [ChatSessionStatus.Failed],
archived: true
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// session1 should be excluded for multiple reasons
assert.strictEqual(filter.exclude(session1), true);
@ -1008,7 +1009,7 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
assert.strictEqual(changeEventFired, true);
});
@ -1031,7 +1032,7 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// Should now be excluded
assert.strictEqual(filter.exclude(session), true);
@ -1096,7 +1097,7 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// Nothing should be excluded
assert.strictEqual(filter.exclude(session), false);
@ -1117,7 +1118,7 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
assert.strictEqual(filter.exclude(session), false);
});
@ -1144,19 +1145,19 @@ suite('AgentSessions', () => {
states: [],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// filter1 should exclude the session
assert.strictEqual(filter1.exclude(session), true);
// filter2 should not exclude the session (different storage key)
assert.strictEqual(filter2.exclude(session), false);
// filter2 should also exclude the session (shared storage key)
assert.strictEqual(filter2.exclude(session), true);
});
test('should handle malformed storage data gracefully', () => {
const storageService = instantiationService.get(IStorageService);
// Store malformed JSON
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, 'invalid json', StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, 'invalid json', StorageScope.PROFILE, StorageTarget.USER);
// Filter should still be created with default excludes
const filter = disposables.add(instantiationService.createInstance(
@ -1188,7 +1189,7 @@ suite('AgentSessions', () => {
states: [ChatSessionStatus.Completed],
archived: true
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
// Should be excluded due to archived (checked first)
assert.strictEqual(filter.exclude(session), true);
@ -1211,7 +1212,7 @@ suite('AgentSessions', () => {
states: [ChatSessionStatus.Completed, ChatSessionStatus.InProgress, ChatSessionStatus.Failed],
archived: false
};
storageService.store(`agentSessions.filterExcludes.${MenuId.ViewTitle.id.toLowerCase()}`, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
storageService.store(storageKey, JSON.stringify(excludes), StorageScope.PROFILE, StorageTarget.USER);
assert.strictEqual(filter.exclude(completedSession), true);
assert.strictEqual(filter.exclude(inProgressSession), true);
@ -1393,7 +1394,7 @@ suite('AgentSessions', () => {
instantiationService.stub(IChatSessionsService, mockChatSessionsService);
instantiationService.stub(ILifecycleService, disposables.add(new TestLifecycleService()));
const storageService = instantiationService.get(IStorageService);
storageService.store('agentSessions.readDateBaseline', 1, StorageScope.WORKSPACE, StorageTarget.MACHINE);
storageService.store('agentSessions.readDateBaseline2', 1, StorageScope.WORKSPACE, StorageTarget.MACHINE);
});
teardown(() => {

View File

@ -46,7 +46,7 @@ import { ChatViewId, IChatWidgetService, ISessionTypePickerDelegate, IWorkspaceP
import { ChatSessionPosition, getResourceForNewChatSession } from '../../chat/browser/chatSessions/chatSessions.contribution.js';
import { IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js';
import { AgentSessionsControl, IAgentSessionsControlOptions } from '../../chat/browser/agentSessions/agentSessionsControl.js';
import { IAgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsViewer.js';
import { AgentSessionsFilter } from '../../chat/browser/agentSessions/agentSessionsFilter.js';
import { HoverPosition } from '../../../../base/browser/ui/hover/hoverWidget.js';
import { IResolvedWalkthrough, IWalkthroughsService } from '../../welcomeGettingStarted/browser/gettingStartedService.js';
import { GettingStartedEditorOptions, GettingStartedInput } from '../../welcomeGettingStarted/browser/gettingStartedInput.js';
@ -566,25 +566,13 @@ export class AgentSessionsWelcomePage extends EditorPane {
// Hide the control initially until loading completes
this.sessionsControlContainer.style.display = 'none';
// Create a filter that limits results and excludes archived sessions
const onDidChangeEmitter = this.sessionsControlDisposables.add(new Emitter<void>());
const filter: IAgentSessionsFilter = {
onDidChange: onDidChangeEmitter.event,
limitResults: () => MAX_SESSIONS,
exclude: (session: IAgentSession) => session.isArchived(),
getExcludes: () => ({
providers: [],
states: [],
archived: true,
read: false,
}),
};
const options: IAgentSessionsControlOptions = {
overrideStyles: getListStyles({
listBackground: editorBackground,
}),
filter,
filter: this.sessionsControlDisposables.add(this.instantiationService.createInstance(AgentSessionsFilter, {
limitResults: () => MAX_SESSIONS,
})),
getHoverPosition: () => HoverPosition.BELOW,
trackActiveEditorSession: () => false,
source: 'welcomeView',