diff --git a/build/gulpfile.vscode.ts b/build/gulpfile.vscode.ts index 147592d00a1..70b52c8a481 100644 --- a/build/gulpfile.vscode.ts +++ b/build/gulpfile.vscode.ts @@ -285,6 +285,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d const telemetry = gulp.src('.build/telemetry/**', { base: '.build/telemetry', dot: true }); const profiles = gulp.src('resources/profiles/**', { base: '.', dot: true }); + const workbenchModes = gulp.src('resources/workbenchModes/**', { base: '.', dot: true }); const jsFilter = util.filter(data => !data.isDirectory() && /\.js$/.test(data.path)); const root = path.resolve(path.join(import.meta.dirname, '..')); @@ -321,6 +322,7 @@ function packageTask(platform: string, arch: string, sourceFolderName: string, d api, telemetry, profiles, + workbenchModes, sources, deps ); diff --git a/resources/workbenchModes/agent-sessions.code-workbench-mode b/resources/workbenchModes/agent-sessions.code-workbench-mode new file mode 100644 index 00000000000..c6258ec61f2 --- /dev/null +++ b/resources/workbenchModes/agent-sessions.code-workbench-mode @@ -0,0 +1,27 @@ +{ + "name": "Agent Sessions", + "settings": { + "workbench.statusBar.visible": false, + "workbench.activityBar.location": "top", + "workbench.sideBar.location": "right", + "workbench.secondarySideBar.forceMaximized": true, + "chat.restoreLastPanelSession": true, + "workbench.startupEditor": "none", + "workbench.editor.restoreEditors": false, + "chat.agentsControl.enabled": true, + "chat.unifiedAgentsBar.enabled": true, + "files.autoSave": "afterDelay", + "window.title": "Agent Sessions", + "workbench.editor.showTabs": "single", + "chat.agentsControl.clickBehavior": "focus", + "workbench.tips.enabled": false, + "chat.agent.maxRequests": 1000, + "workbench.layoutControl.type": "toggles", + "inlineChat.affordance": "editor", + "inlineChat.renderMode": "hover", + "github.copilot.chat.claudeCode.enabled": true, + "github.copilot.chat.languageContext.typescript.enabled": true, + "diffEditor.renderSideBySide": false, + "diffEditor.hideUnchangedRegions.enabled": true + } +} diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index ddc44a1149a..4f96af80f08 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -221,6 +221,7 @@ class StandaloneEnvironmentService implements IEnvironmentService { readonly argvResource: URI = URI.from({ scheme: 'monaco', authority: 'argvResource' }); readonly untitledWorkspacesHome: URI = URI.from({ scheme: 'monaco', authority: 'untitledWorkspacesHome' }); readonly builtinProfilesHome: URI = URI.from({ scheme: 'monaco', authority: 'builtinProfilesHome' }); + readonly builtinWorkbenchModesHome: URI = URI.from({ scheme: 'monaco', authority: 'builtinWorkbenchModesHome' }); readonly workspaceStorageHome: URI = URI.from({ scheme: 'monaco', authority: 'workspaceStorageHome' }); readonly localHistoryHome: URI = URI.from({ scheme: 'monaco', authority: 'localHistoryHome' }); readonly cacheHome: URI = URI.from({ scheme: 'monaco', authority: 'cacheHome' }); diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index be0d2573d1d..0c64759067a 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -59,6 +59,7 @@ export interface IEnvironmentService { localHistoryHome: URI; cacheHome: URI; builtinProfilesHome: URI; + builtinWorkbenchModesHome: URI; // --- settings sync userDataSyncHome: URI; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 5a016b52bad..10d1781de82 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -110,6 +110,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron @memoize get builtinProfilesHome(): URI { return joinPath(URI.file(this.appRoot), 'resources', 'profiles'); } + @memoize + get builtinWorkbenchModesHome(): URI { return joinPath(URI.file(this.appRoot), 'resources', 'workbenchModes'); } + @memoize get builtinExtensionsPath(): string { const cliBuiltinExtensionsDir = this.args['builtin-extensions-dir']; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index ec726447d1c..048fb005145 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -98,6 +98,8 @@ import { INotificationService, Severity } from '../../platform/notification/comm import { IDefaultAccountService } from '../../platform/defaultAccount/common/defaultAccount.js'; import { DefaultAccountService } from '../services/accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../services/policies/common/accountPolicyService.js'; +import { WorkbenchModeService } from '../services/layout/browser/workbenchModeService.js'; +import { IWorkbenchModeService } from '../services/layout/common/workbenchModeService.js'; export class BrowserMain extends Disposable { @@ -387,6 +389,14 @@ export class BrowserMain extends Disposable { // // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // Layout Mode + const workbenchModeService: WorkbenchModeService = this._register(new WorkbenchModeService(configurationService, fileService, environmentService, uriIdentityService, logService)); + serviceCollection.set(IWorkbenchModeService, workbenchModeService); + try { + await workbenchModeService.initialize(); + } catch (error) { + logService.error('Error while initializing workbench mode service', error); + } // Workspace Trust Service const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); diff --git a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts index e1a8af5253e..e42ec4575cb 100644 --- a/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts +++ b/src/vs/workbench/contrib/chat/electron-browser/agentSessions/agentSessionsActions.ts @@ -11,7 +11,6 @@ import { ProductQualityContext } from '../../../../../platform/contextkey/common import { INativeEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; import { INativeHostService } from '../../../../../platform/native/common/native.js'; -import { IUserDataProfilesService } from '../../../../../platform/userDataProfile/common/userDataProfile.js'; import { ChatEntitlementContextKeys } from '../../../../services/chat/common/chatEntitlementService.js'; import { CHAT_CATEGORY } from '../../browser/actions/chatActions.js'; @@ -29,7 +28,6 @@ export class OpenAgentSessionsWindowAction extends Action2 { async run(accessor: ServicesAccessor) { const environmentService = accessor.get(INativeEnvironmentService); const nativeHostService = accessor.get(INativeHostService); - const userDataProfilesService = accessor.get(IUserDataProfilesService); const fileService = accessor.get(IFileService); // Create workspace file if it doesn't exist @@ -44,8 +42,6 @@ export class OpenAgentSessionsWindowAction extends Action2 { await fileService.writeFile(workspaceUri, VSBuffer.fromString(emptyWorkspaceContent)); } - const profile = await userDataProfilesService.createSystemProfile('agent-sessions'); - await userDataProfilesService.updateProfile(profile, { workspaces: [workspaceUri] }); await nativeHostService.openWindow([{ workspaceUri }], { forceNewWindow: true }); } } diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 5bb262b0f3e..f3a3836bf20 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -60,7 +60,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IURLService private readonly urlService: IURLService, - @IBrowserWorkbenchEnvironmentService private readonly environmentService: IBrowserWorkbenchEnvironmentService + @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService ) { super(); @@ -78,25 +78,22 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1); this._register(this.userDataProfilesService.onDidChangeProfiles(e => this.hasProfilesContext.set(this.userDataProfilesService.profiles.length > 1))); - if (!this.workspaceContextService.getWorkspace().isAgentSessionsWorkspace) { + this.registerEditor(); + this.registerActions(); - this.registerEditor(); - this.registerActions(); + this._register(this.urlService.registerHandler(this)); - this._register(this.urlService.registerHandler(this)); - - if (isWeb) { - lifecycleService.when(LifecyclePhase.Eventually).then(() => userDataProfilesService.cleanUp()); - } - - this.reportWorkspaceProfileInfo(); - - if (this.environmentService.options?.profileToPreview) { - lifecycleService.when(LifecyclePhase.Restored).then(() => this.handleURL(URI.revive(this.environmentService.options!.profileToPreview!))); - } - - this.registerDropHandler(); + if (isWeb) { + lifecycleService.when(LifecyclePhase.Eventually).then(() => userDataProfilesService.cleanUp()); } + + this.reportWorkspaceProfileInfo(); + + if (environmentService.options?.profileToPreview) { + lifecycleService.when(LifecyclePhase.Restored).then(() => this.handleURL(URI.revive(environmentService.options!.profileToPreview!))); + } + + this.registerDropHandler(); } async handleURL(uri: URI): Promise { diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 0533b94ffb9..9a4dfe294dc 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -65,6 +65,8 @@ import { IDefaultAccountService } from '../../platform/defaultAccount/common/def import { DefaultAccountService } from '../services/accounts/browser/defaultAccount.js'; import { AccountPolicyService } from '../services/policies/common/accountPolicyService.js'; import { MultiplexPolicyService } from '../services/policies/common/multiplexPolicyService.js'; +import { WorkbenchModeService } from '../services/layout/browser/workbenchModeService.js'; +import { IWorkbenchModeService } from '../services/layout/common/workbenchModeService.js'; export class DesktopMain extends Disposable { @@ -322,6 +324,16 @@ export class DesktopMain extends Disposable { }) ]); + // Workbench Mode + const workbenchModeService: WorkbenchModeService = this._register(new WorkbenchModeService(configurationService, fileService, environmentService, uriIdentityService, logService)); + serviceCollection.set(IWorkbenchModeService, workbenchModeService); + + try { + await workbenchModeService.initialize(); + } catch (error) { + logService.error('Error while initializing workbench mode service', error); + } + // Workspace Trust Service const workspaceTrustEnablementService = new WorkspaceTrustEnablementService(configurationService, environmentService); serviceCollection.set(IWorkspaceTrustEnablementService, workspaceTrustEnablementService); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 9435617d2f6..6b6b4fb4094 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -141,6 +141,9 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get builtinProfilesHome(): URI { return joinPath(this.userRoamingDataHome, 'builtinProfiles'); } + @memoize + get builtinWorkbenchModesHome(): URI { return joinPath(this.userRoamingDataHome, 'builtinWorkbenchModes'); } + @memoize get serviceMachineIdResource(): URI { return joinPath(this.userRoamingDataHome, 'machineid'); } diff --git a/src/vs/workbench/services/layout/browser/workbenchModeService.ts b/src/vs/workbench/services/layout/browser/workbenchModeService.ts new file mode 100644 index 00000000000..0d312ea5a0d --- /dev/null +++ b/src/vs/workbench/services/layout/browser/workbenchModeService.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { IWorkbenchModeConfiguration, IWorkbenchModeService } from '../common/workbenchModeService.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; +import { parse } from '../../../../base/common/json.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { Extensions, IConfigurationDefaults, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IStringDictionary } from '../../../../base/common/collections.js'; + +export class WorkbenchModeService extends Disposable implements IWorkbenchModeService { + + declare readonly _serviceBrand: undefined; + + private _workbenchMode: string | undefined; + get workbenchMode(): string | undefined { return this._workbenchMode; } + + private readonly _onDidChangeWorkbenchMode = this._register(new Emitter()); + readonly onDidChangeWorkbenchMode: Event = this._onDidChangeWorkbenchMode.event; + + private readonly workbenchModeFileWatcher = this._register(new MutableDisposable()); + private readonly configurationRegistry = Registry.as(Extensions.Configuration); + private configurationDefaults: IConfigurationDefaults | undefined; + + constructor( + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + @IFileService private readonly fileService: IFileService, + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @ILogService private readonly logService: ILogService + ) { + super(); + this._workbenchMode = workspaceContextService.getWorkspace().isAgentSessionsWorkspace ? 'agent-sessions' : undefined; + this.watchCurrentModeFile(); + } + + async initialize(): Promise { + return this.updateWorkbenchModeConfiguration(); + } + + private async updateWorkbenchModeConfiguration(): Promise { + if (!this._workbenchMode) { + return; + } + const workbenchModeConfiguration = await this.getWorkbenchModeConfiguration(this._workbenchMode); + this.updateConfigurationDefaults(workbenchModeConfiguration?.settings); + } + + private updateConfigurationDefaults(configurationDefaults: IStringDictionary | undefined): void { + if (this.configurationDefaults) { + this.configurationRegistry.deregisterDefaultConfigurations([this.configurationDefaults]); + } + if (configurationDefaults) { + this.configurationDefaults = { overrides: configurationDefaults }; + this.configurationRegistry.registerDefaultConfigurations([this.configurationDefaults]); + } else { + this.configurationDefaults = undefined; + } + } + + private watchCurrentModeFile(): void { + if (!this._workbenchMode) { + this.workbenchModeFileWatcher.clear(); + return; + } + + const workbenchModeFileUri = this.getWorkbenchModeFileUri(this._workbenchMode); + if (!workbenchModeFileUri) { + this.workbenchModeFileWatcher.clear(); + return; + } + + this.workbenchModeFileWatcher.value = this.fileService.watch(workbenchModeFileUri); + this._register(this.fileService.onDidFilesChange(e => { + if (e.affects(workbenchModeFileUri)) { + this.updateWorkbenchModeConfiguration(); + this._onDidChangeWorkbenchMode.fire(this._workbenchMode); + } + })); + } + + private getWorkbenchModeFileUri(layoutId: string): URI { + return this.uriIdentityService.extUri.joinPath(this.environmentService.builtinWorkbenchModesHome, `${layoutId}.code-workbench-mode`); + } + + async getWorkbenchModeConfiguration(id: string): Promise { + const resource = this.getWorkbenchModeFileUri(id); + return this.resolveWorkbenchModeConfiguration(resource); + } + + async getWorkbenchModeConfigurations(): Promise { + const result: IWorkbenchModeConfiguration[] = []; + const workbenchModesFolder = this.environmentService.builtinWorkbenchModesHome; + try { + const stat = await this.fileService.resolve(workbenchModesFolder); + if (!stat.children?.length) { + return result; + } + for (const child of stat.children) { + if (child.isDirectory) { + continue; + } + const workbenchModeConfiguration = await this.resolveWorkbenchModeConfiguration(child.resource); + if (workbenchModeConfiguration) { + result.push(workbenchModeConfiguration); + } + } + } catch (error) { + this.logService.error(`Error while reading workbench mode files from ${workbenchModesFolder.toString()}`, error); + } + return result; + } + + private async resolveWorkbenchModeConfiguration(workbenchConfigurationModeFile: URI): Promise { + if (this.uriIdentityService.extUri.extname(workbenchConfigurationModeFile) !== '.code-workbench-mode') { + return undefined; + } + try { + const content = (await this.fileService.readFile(workbenchConfigurationModeFile)).value.toString(); + const name = this.uriIdentityService.extUri.basename(workbenchConfigurationModeFile); + const workbenchModeConfiguration: IWorkbenchModeConfiguration = { + id: name.substring(0, name.length - '.code-workbench-mode'.length), + ...parse(content) + }; + return workbenchModeConfiguration; + } catch (error) { + this.logService.error(`Error while reading workbench mode file from ${workbenchConfigurationModeFile.toString()}`, error); + return undefined; + } + } + + async setWorkbenchMode(modeId: string | undefined): Promise { + if (this._workbenchMode === modeId) { + return; + } + + this._workbenchMode = modeId; + this.updateWorkbenchModeConfiguration(); + this.watchCurrentModeFile(); + this._onDidChangeWorkbenchMode.fire(modeId); + } +} diff --git a/src/vs/workbench/services/layout/common/workbenchModeService.ts b/src/vs/workbench/services/layout/common/workbenchModeService.ts new file mode 100644 index 00000000000..87b5530d99d --- /dev/null +++ b/src/vs/workbench/services/layout/common/workbenchModeService.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { IStringDictionary } from '../../../../base/common/collections.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IWorkbenchModeService = createDecorator('workbenchModeService'); + +export interface IWorkbenchModeConfiguration { + readonly id: string; + readonly name: string; + readonly settings: IStringDictionary; +} + +export interface IWorkbenchModeService { + readonly _serviceBrand: undefined; + + /** + * The currently active workbench mode id, or undefined if using default settings + */ + readonly workbenchMode: string | undefined; + + /** + * Event fired when the workbench mode changes + */ + readonly onDidChangeWorkbenchMode: Event; + + /** + * Resolve a workbench mode by its id + * @param id The id of the workbench mode to resolve + */ + getWorkbenchModeConfiguration(id: string): Promise; + + /** + * Get all workbench modes + */ + getWorkbenchModeConfigurations(): Promise; + + /** + * Set the active workbench mode. Pass undefined to clear the mode and return to defaults. + */ + setWorkbenchMode(workbenchMode: string | undefined): Promise; +} + +export class DefaultWorkbenchModeService implements IWorkbenchModeService { + + readonly _serviceBrand: undefined; + readonly workbenchMode: string | undefined = undefined; + readonly onDidChangeWorkbenchMode: Event = Event.None; + + getWorkbenchModeConfiguration(_id: string): Promise { + return Promise.resolve(undefined); + } + + getWorkbenchModeConfigurations(): Promise { + return Promise.resolve([]); + } + + setWorkbenchMode(_workbenchMode: string | undefined): Promise { + return Promise.resolve(); + } +}