Skip to content

Commit 2b553d1

Browse files
lszomoruCopilot
andauthored
Agents - persist working sets in storage (#310724)
* Agents - persist working sets in storage Co-authored-by: Copilot <copilot@github.com> * Pull request feedback Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
1 parent bf8fdae commit 2b553d1

File tree

1 file changed

+130
-29
lines changed

1 file changed

+130
-29
lines changed

src/vs/sessions/contrib/workingSet/browser/workingSet.ts

Lines changed: 130 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { mainWindow } from '../../../../base/browser/window.js';
67
import { Sequencer } from '../../../../base/common/async.js';
78
import { Disposable } from '../../../../base/common/lifecycle.js';
89
import { ResourceMap } from '../../../../base/common/map.js';
9-
import { autorun, derivedOpts, IObservable, runOnChange } from '../../../../base/common/observable.js';
10+
import { autorun, derivedObservableWithCache, IObservable, observableFromEvent, runOnChange } from '../../../../base/common/observable.js';
1011
import { isEqual } from '../../../../base/common/resources.js';
1112
import { URI } from '../../../../base/common/uri.js';
1213
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
1314
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
15+
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
1416
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
1517
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
1618
import { IEditorGroupsService, IEditorWorkingSet } from '../../../../workbench/services/editor/common/editorGroupsService.js';
@@ -19,30 +21,61 @@ import { IWorkbenchLayoutService, Parts } from '../../../../workbench/services/l
1921
import { SessionStatus } from '../../../services/sessions/common/session.js';
2022
import { IActiveSession, ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
2123

24+
type ISessionSerializedWorkingSet = {
25+
readonly sessionResource: string;
26+
readonly editorWorkingSet: IEditorWorkingSet;
27+
};
28+
2229
export class SessionWorkingSetController extends Disposable implements IWorkbenchContribution {
2330

2431
static readonly ID = 'workbench.contrib.sessionsWorkingSetController';
32+
private static readonly STORAGE_KEY = 'sessions.workingSets';
2533

2634
private readonly _useModalConfigObs: IObservable<'off' | 'some' | 'all'>;
27-
private readonly _workingSets = new ResourceMap<IEditorWorkingSet>();
35+
private readonly _workingSets: ResourceMap<IEditorWorkingSet>;
2836
private readonly _workingSetSequencer = new Sequencer();
2937

3038
constructor(
3139
@IConfigurationService private readonly _configurationService: IConfigurationService,
3240
@ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService,
33-
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
3441
@IEditorService private readonly _editorService: IEditorService,
42+
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
43+
@IStorageService private readonly _storageService: IStorageService,
3544
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
3645
@IWorkbenchLayoutService private readonly _layoutService: IWorkbenchLayoutService,
3746
) {
3847
super();
3948

49+
this._workingSets = this._loadWorkingSets();
50+
4051
this._useModalConfigObs = observableConfigValue<'off' | 'some' | 'all'>('workbench.editor.useModal', 'all', this._configurationService);
4152

42-
const activeSession = derivedOpts<IActiveSession | undefined>({
43-
equalsFn: ((a, b) => isEqual(a?.resource, b?.resource))
44-
}, reader => {
45-
return this._sessionManagementService.activeSession.read(reader);
53+
// Workspace folders
54+
const workspaceFoldersObs = observableFromEvent(
55+
this._workspaceContextService.onDidChangeWorkspaceFolders,
56+
() => this._workspaceContextService.getWorkspace().folders);
57+
58+
const activeSession = derivedObservableWithCache<IActiveSession | undefined>(this, (reader, lastValue) => {
59+
const workspaceFolders = workspaceFoldersObs.read(reader);
60+
const activeSession = this._sessionManagementService.activeSession.read(reader);
61+
const activeSessionWorkspace = activeSession?.workspace.read(reader)?.repositories[0];
62+
const activeSessionWorkspaceUri = activeSessionWorkspace?.workingDirectory ?? activeSessionWorkspace?.uri;
63+
64+
// The active session is updated before the workspace folders are updated. We
65+
// need to wait until the workspace folders are updated before considering the
66+
// active session.
67+
if (
68+
activeSessionWorkspaceUri &&
69+
!workspaceFolders.some(folder => isEqual(folder.uri, activeSessionWorkspaceUri))
70+
) {
71+
return lastValue;
72+
}
73+
74+
if (isEqual(activeSession?.resource, lastValue?.resource)) {
75+
return lastValue;
76+
}
77+
78+
return activeSession;
4679
});
4780

4881
this._register(autorun(reader => {
@@ -51,54 +84,122 @@ export class SessionWorkingSetController extends Disposable implements IWorkbenc
5184
return;
5285
}
5386

54-
// Session changed (save)
55-
reader.store.add(runOnChange(activeSession, (_, previousSession) => {
56-
if (!previousSession || previousSession.status.read(undefined) === SessionStatus.Untitled) {
87+
// Session changed (save, apply)
88+
reader.store.add(runOnChange(activeSession, (session, previousSession) => {
89+
if (!session || !previousSession) {
5790
return;
5891
}
5992

60-
this._saveWorkingSet(previousSession.resource);
93+
// Save working set for previous session (skip for untitled sessions)
94+
if (previousSession.status.read(undefined) !== SessionStatus.Untitled) {
95+
this._saveWorkingSet(previousSession.resource);
96+
}
97+
98+
// Apply working set for current session
99+
void this._applyWorkingSet(session.resource);
61100
}));
62101

63-
// Workspace folders changes (apply)
64-
reader.store.add(this._workspaceContextService.onDidChangeWorkspaceFolders(() => {
65-
const activeSessionResource = activeSession.read(undefined)?.resource;
66-
if (!activeSessionResource) {
67-
return;
102+
// Session state changed (archive, delete)
103+
reader.store.add(this._sessionManagementService.onDidChangeSessions(e => {
104+
const archivedSessions = e.changed.filter(session => session.isArchived.read(undefined));
105+
for (const session of [...e.removed, ...archivedSessions]) {
106+
this._deleteWorkingSet(session.resource);
107+
}
108+
}));
109+
110+
// Save working sets to storage
111+
reader.store.add(this._storageService.onWillSaveState(() => {
112+
const activeSession = this._sessionManagementService.activeSession.read(undefined);
113+
114+
// Save working set for previous session (skip for untitled sessions)
115+
if (activeSession && activeSession.status.read(undefined) !== SessionStatus.Untitled) {
116+
this._saveWorkingSet(activeSession.resource);
68117
}
69118

70-
void this._applyWorkingSet(activeSessionResource);
119+
this._storeWorkingSets();
71120
}));
72121
}));
73122
}
74123

75-
private _saveWorkingSet(sessionResource: URI): void {
76-
const existingWorkingSet = this._workingSets.get(sessionResource);
77-
if (existingWorkingSet) {
78-
this._editorGroupsService.deleteWorkingSet(existingWorkingSet);
124+
private _loadWorkingSets(): ResourceMap<IEditorWorkingSet> {
125+
const workingSets = new ResourceMap<IEditorWorkingSet>();
126+
const workingSetsRaw = this._storageService.get(SessionWorkingSetController.STORAGE_KEY, StorageScope.WORKSPACE);
127+
if (!workingSetsRaw) {
128+
return workingSets;
79129
}
80130

81-
const workingSet = this._editorGroupsService.saveWorkingSet(`session-working-set:${sessionResource.toString()}`);
82-
this._workingSets.set(sessionResource, workingSet);
131+
for (const serializedWorkingSet of JSON.parse(workingSetsRaw) as ISessionSerializedWorkingSet[]) {
132+
const sessionResource = URI.parse(serializedWorkingSet.sessionResource);
133+
workingSets.set(sessionResource, serializedWorkingSet.editorWorkingSet);
134+
}
135+
136+
return workingSets;
137+
}
138+
139+
private _storeWorkingSets(): void {
140+
if (this._workingSets.size === 0) {
141+
this._storageService.remove(SessionWorkingSetController.STORAGE_KEY, StorageScope.WORKSPACE);
142+
return;
143+
}
144+
145+
const serializedWorkingSets: ISessionSerializedWorkingSet[] = [];
146+
for (const [sessionResource, editorWorkingSet] of this._workingSets) {
147+
serializedWorkingSets.push({ sessionResource: sessionResource.toString(), editorWorkingSet });
148+
}
149+
150+
this._storageService.store(SessionWorkingSetController.STORAGE_KEY, JSON.stringify(serializedWorkingSets), StorageScope.WORKSPACE, StorageTarget.MACHINE);
83151
}
84152

85153
private async _applyWorkingSet(sessionResource: URI): Promise<void> {
86154
const workingSet: IEditorWorkingSet | 'empty' = this._workingSets.get(sessionResource) ?? 'empty';
87155
const preserveFocus = this._layoutService.hasFocus(Parts.PANEL_PART);
88156

89157
return this._workingSetSequencer.queue(async () => {
90-
const applied = await this._editorGroupsService.applyWorkingSet(workingSet, { preserveFocus });
91-
if (applied && this._editorService.visibleEditors.length > 0) {
158+
if (workingSet === 'empty') {
159+
// Applying an empty working set closes all editors, and we already have an
160+
// event listener that listens to the editor close event to hide the editor
161+
// part if there are no visible editors
162+
await this._editorGroupsService.applyWorkingSet(workingSet, { preserveFocus });
163+
return;
164+
}
165+
166+
if (!this._layoutService.isVisible(Parts.EDITOR_PART, mainWindow)) {
167+
// Applying the working set requires the editor part to be visible
168+
this._layoutService.setPartHidden(false, Parts.EDITOR_PART);
169+
}
170+
171+
// Applying the working set closes all editors which triggers the event listener
172+
// to close the editor part. After we apply the working set we need to show the
173+
// editor part
174+
const result = await this._editorGroupsService.applyWorkingSet(workingSet, { preserveFocus });
175+
if (result && !this._layoutService.isVisible(Parts.EDITOR_PART, mainWindow)) {
92176
this._layoutService.setPartHidden(false, Parts.EDITOR_PART);
93177
}
94178
});
95179
}
96180

97-
override dispose(): void {
98-
for (const [, workingSet] of this._workingSets) {
99-
this._editorGroupsService.deleteWorkingSet(workingSet);
181+
private _saveWorkingSet(sessionResource: URI): void {
182+
// Delete existing working set for session if any
183+
const existingWorkingSet = this._workingSets.get(sessionResource);
184+
if (existingWorkingSet) {
185+
this._workingSets.delete(sessionResource);
186+
this._editorGroupsService.deleteWorkingSet(existingWorkingSet);
187+
}
188+
189+
// Create new working set for session
190+
if (this._editorService.visibleEditors.length > 0) {
191+
const workingSet = this._editorGroupsService.saveWorkingSet(`session-working-set:${sessionResource.toString()}`);
192+
this._workingSets.set(sessionResource, workingSet);
193+
}
194+
}
195+
196+
private _deleteWorkingSet(sessionResource: URI): void {
197+
const existingWorkingSet = this._workingSets.get(sessionResource);
198+
if (!existingWorkingSet) {
199+
return;
100200
}
101201

102-
super.dispose();
202+
this._editorGroupsService.deleteWorkingSet(existingWorkingSet);
203+
this._workingSets.delete(sessionResource);
103204
}
104205
}

0 commit comments

Comments
 (0)